iOS 箭頭彈窗實現(xiàn)

iOS 箭頭彈窗樣式

iOS 在日常開發(fā)中竞阐,可能會遇到這種箭頭彈窗闸昨,于是自己手擼了一個這樣的控件饺著。

要求

  • 箭頭的位置椰苟,始終對著被點擊的 view 的中心位置(箭頭位置不確定性)
  • 彈窗顯示完全(不能超出界面)

創(chuàng)建類宾抓,并且聲明屬性

.h 文件

.h 文件中子漩,聲明一個 showForm: 方法,傳入的參數(shù)是當前被點擊的 view石洗。

//
//  YXCPopOverView.h
//  YXCTools
//
//  Created by GGT on 2020/10/23.
//  Copyright ? 2020 GGT. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface YXCPopOverView : UIView

#pragma mark - Property


#pragma mark - Method

/// 從 view 展示彈窗
/// @param view 被點擊的 view
- (void)showForm:(UIView *)view;

@end

.m 文件

在 .m 文件中進行屬性的聲明和邏輯實現(xiàn)

屬性聲明
@property (nonatomic, strong) UIView *contentView; /**< 真實的黑色部分 view */
@property (nonatomic, strong) UIColor *yxc_backgroundColor; /**< 背景顏色 */
@property (nonatomic, assign) CGFloat triangleWidth; /**< 三角形寬度 */
@property (nonatomic, assign) CGFloat triangleHeight; /**< 三角形高度 */
@property (nonatomic, assign) CGPoint startPoint; /**< 三角形起始位置 */
@property (nonatomic, assign) CGPoint middlePoint; /**< 三角形中點位置 */
@property (nonatomic, assign) CGPoint endPoint; /**< 三角形結(jié)束位置 */
@property (nonatomic, strong) UITableView *tableView; /**< tableView */
邏輯實現(xiàn)
繪制三角形

利用 CoreGraphicdrawRect: 進行繪制三角形

- (void)drawRect:(CGRect)rect {
    
    // 獲取當前上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 起始位置
    CGContextMoveToPoint(context, self.startPoint.x, self.startPoint.y);
    // 中點位置
    CGContextAddLineToPoint(context, self.middlePoint.x, self.middlePoint.y);
    // 結(jié)束位置
    CGContextAddLineToPoint(context, self.endPoint.x, self.endPoint.y);
    CGContextClosePath(context);
    // 設置線的顏色
    [self.yxc_backgroundColor setStroke];
    // 設置填充顏色
    [self.yxc_backgroundColor setFill];
    // 繪畫
    CGContextDrawPath(context, kCGPathFillStroke);
}
展示邏輯

showForm: 方法中幢泼,進行邏輯操作,展示出效果

- (void)showForm:(UIView *)view {
    
    // 在這里展示邏輯
    // 1. self 添加到一個 Windows 上面
    // 2. contentView 作為實際展示黑色部分
    // 3. 箭頭的中心位置始終對著傳入的 view 的中心 x
    // 4. contentView 的 x 如果超過界線,那么直接設置成界線值
    // 5. contentView 的 y 如果超過界線,那么將彈窗的方向改變,默認是在上面,如果 y 小于界限值,則彈窗在 view 的下方
    NSArray *windows = [UIApplication sharedApplication].windows;
    for (UIWindow *window in windows) {
        if (window.height == IPHONE_HEIGHT && window.width == IPHONE_WIDTH) {
            // 先將彈窗的大小,放入中間變量
            CGSize contentSize = self.size;
            // 將 self 的 frame 直接設置成 window 的 bounds
            // 為什么要這么做?
            // 為了實現(xiàn)點擊位置在 contentView 外,移除當前界面(在這之前,直接使用一個 view 添加到 windows,然后再將 self 添加到 view 上面,并且給 view 增加了一個點擊手勢,最后發(fā)現(xiàn) tableView 的點擊方法不再調(diào)用,所以采用了這種方式)
            self.frame = window.bounds;
            // 設置 contentView 的 size
            self.contentView.size = CGSizeMake(contentSize.width, contentSize.height - self.triangleHeight);
            // 將傳入的 view 進行坐標系轉(zhuǎn)換,轉(zhuǎn)換成相對于 Windows 的坐標
            CGRect convertFrame = [view convertRect:view.bounds toView:window];
            // 獲取到傳入的 view 在 Windows 上面的 centerX,作為三角形箭頭的 centerX
            CGFloat centerX = convertFrame.size.width * 0.5 + convertFrame.origin.x;
            // 獲取到 contentView 的 y 值
            CGFloat y = convertFrame.origin.y - contentSize.height - 2;
            // contentView 的 centerX 與 傳入的 view 對齊
            self.contentView.centerX = centerX;
            CGFloat xGap = 5;
            // 判斷當前 x 是否小于 xGap,如果小于 xGap ,x 直接設置成 xGap;
            if (self.contentView.x < xGap) {
                self.contentView.x = xGap;
            }
            // 判斷當前視圖右邊是否超過 IPHONE_WIDTH - 20
            if (self.contentView.right > IPHONE_WIDTH - xGap) {
                self.contentView.right = IPHONE_WIDTH - xGap;
            }
            CGFloat yGap = 10;
            // 設置 Y 值
            self.contentView.y = y;
            // 判斷當前 y 是否小于 yGap,如果小于 yGap,在下方顯示
            if (self.contentView.y < yGap) {
                y = CGRectGetMaxY(convertFrame) + 2 + self.triangleHeight;
                self.contentView.y = y;
                // 計算三角形的三個點
                self.middlePoint  = CGPointMake(centerX, CGRectGetMaxY(convertFrame) + 2);
                self.startPoint = CGPointMake(centerX - self.triangleWidth * 0.5, self.middlePoint.y + self.triangleHeight);
                self.endPoint = CGPointMake(centerX + self.triangleWidth * 0.5, self.middlePoint.y + self.triangleHeight);
            } else {
                self.middlePoint = CGPointMake(centerX, convertFrame.origin.y - 2);
                self.startPoint = CGPointMake(centerX - self.triangleWidth * 0.5, self.middlePoint.y - self.triangleHeight);
                self.endPoint = CGPointMake(centerX + self.triangleWidth * 0.5, self.middlePoint.y - self.triangleHeight);
            }
            
            [window addSubview:self];
            return;
        }
        
    }
}

以上就是主要代碼

看下效果

三角箭頭彈窗最終展示.gif

最后附上 .m 代碼

//
//  YXCPopOverView.m
//  YXCTools
//
//  Created by GGT on 2020/10/23.
//  Copyright ? 2020 GGT. All rights reserved.
//

#import "YXCPopOverView.h"

@interface YXCPopOverView ()<UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) UIView *contentView; /**< 真實的黑色部分 view */
@property (nonatomic, strong) UIColor *yxc_backgroundColor; /**< 背景顏色 */
@property (nonatomic, assign) CGFloat triangleWidth; /**< 三角形寬度 */
@property (nonatomic, assign) CGFloat triangleHeight; /**< 三角形高度 */
@property (nonatomic, assign) CGPoint startPoint; /**< 三角形起始位置 */
@property (nonatomic, assign) CGPoint middlePoint; /**< 三角形中點位置 */
@property (nonatomic, assign) CGPoint endPoint; /**< 三角形結(jié)束位置 */
@property (nonatomic, strong) UITableView *tableView; /**< tableView */

@end

@implementation YXCPopOverView

#pragma mark - Lifecycle

/// 刷新UI
- (void)injected {
    
}

- (instancetype)initWithFrame:(CGRect)frame {
    
    if (self = [super initWithFrame:frame]) {
        
        self.triangleWidth = 10.0f;
        self.triangleHeight = 10.0f;
        self.backgroundColor = UIColor.clearColor;
        self.yxc_backgroundColor = kColorFromHexCode(0x0E0F10);

        [self setupUI];
        [self setupConstraints];
    }
    
    return self;
}

- (void)drawRect:(CGRect)rect {
    
    // 獲取當前上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 起始位置
    CGContextMoveToPoint(context, self.startPoint.x, self.startPoint.y);
    // 中點位置
    CGContextAddLineToPoint(context, self.middlePoint.x, self.middlePoint.y);
    // 結(jié)束位置
    CGContextAddLineToPoint(context, self.endPoint.x, self.endPoint.y);
    CGContextClosePath(context);
    // 設置線的顏色
    [self.yxc_backgroundColor setStroke];
    // 設置填充顏色
    [self.yxc_backgroundColor setFill];
    // 繪畫
    CGContextDrawPath(context, kCGPathFillStroke);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    [self dismiss];
}

- (void)dealloc {
    
    YXCLog(@"%s", __func__);
}

#pragma mark - Custom Accessors (Setter 與 Getter 方法)


#pragma mark - IBActions


#pragma mark - Public

- (void)showForm:(UIView *)view {
    
    // 在這里展示邏輯
    // 1. self 添加到一個 Windows 上面
    // 2. contentView 作為實際展示黑色部分
    // 3. 箭頭的中心位置始終對著傳入的 view 的中心 x
    // 4. contentView 的 x 如果超過界線,那么直接設置成界線值
    // 5. contentView 的 y 如果超過界線,那么將彈窗的方向改變,默認是在上面,如果 y 小于界限值,則彈窗在 view 的下方
    NSArray *windows = [UIApplication sharedApplication].windows;
    for (UIWindow *window in windows) {
        if (window.height == IPHONE_HEIGHT && window.width == IPHONE_WIDTH) {
            // 先將彈窗的大小,放入中間變量
            CGSize contentSize = self.size;
            // 將 self 的 frame 直接設置成 window 的 bounds
            // 為什么要這么做?
            // 為了實現(xiàn)點擊位置在 contentView 外,移除當前界面(在這之前,直接使用一個 view 添加到 windows,然后再將 self 添加到 view 上面,并且給 view 增加了一個點擊手勢,最后發(fā)現(xiàn) tableView 的點擊方法不再調(diào)用,所以采用了這種方式)
            self.frame = window.bounds;
            // 設置 contentView 的 size
            self.contentView.size = CGSizeMake(contentSize.width, contentSize.height - self.triangleHeight);
            // 將傳入的 view 進行坐標系轉(zhuǎn)換,轉(zhuǎn)換成相對于 Windows 的坐標
            CGRect convertFrame = [view convertRect:view.bounds toView:window];
            // 獲取到傳入的 view 在 Windows 上面的 centerX,作為三角形箭頭的 centerX
            CGFloat centerX = convertFrame.size.width * 0.5 + convertFrame.origin.x;
            // 獲取到 contentView 的 y 值
            CGFloat y = convertFrame.origin.y - contentSize.height - 2;
            // contentView 的 centerX 與 傳入的 view 對齊
            self.contentView.centerX = centerX;
            CGFloat xGap = 5;
            // 判斷當前 x 是否小于 xGap,如果小于 xGap ,x 直接設置成 xGap;
            if (self.contentView.x < xGap) {
                self.contentView.x = xGap;
            }
            // 判斷當前視圖右邊是否超過 IPHONE_WIDTH - 20
            if (self.contentView.right > IPHONE_WIDTH - xGap) {
                self.contentView.right = IPHONE_WIDTH - xGap;
            }
            CGFloat yGap = 10;
            // 設置 Y 值
            self.contentView.y = y;
            // 判斷當前 y 是否小于 yGap,如果小于 yGap,在下方顯示
            if (self.contentView.y < yGap) {
                y = CGRectGetMaxY(convertFrame) + 2 + self.triangleHeight;
                self.contentView.y = y;
                // 計算三角形的三個點
                self.middlePoint  = CGPointMake(centerX, CGRectGetMaxY(convertFrame) + 2);
                self.startPoint = CGPointMake(centerX - self.triangleWidth * 0.5, self.middlePoint.y + self.triangleHeight);
                self.endPoint = CGPointMake(centerX + self.triangleWidth * 0.5, self.middlePoint.y + self.triangleHeight);
            } else {
                self.middlePoint = CGPointMake(centerX, convertFrame.origin.y - 2);
                self.startPoint = CGPointMake(centerX - self.triangleWidth * 0.5, self.middlePoint.y - self.triangleHeight);
                self.endPoint = CGPointMake(centerX + self.triangleWidth * 0.5, self.middlePoint.y - self.triangleHeight);
            }
            
            [window addSubview:self];
            return;
        }
        
    }
}


#pragma mark - Private

- (void)dismiss {
    
    [self removeFromSuperview];
}


#pragma mark - Protocol

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    cell.textLabel.text = [NSString stringWithFormat:@"%ld", indexPath.row];
    cell.backgroundColor = [UIColor clearColor];
    cell.textLabel.textColor = UIColor.whiteColor;
    
    return cell;
}


#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    YXCLog(@"%s", __func__);
}

#pragma mark - UI

- (void)setupUI {
    
    self.contentView = [[UIView alloc] init];
    self.contentView.backgroundColor = self.yxc_backgroundColor;
    self.contentView.layer.cornerRadius = 10.0f;
    self.contentView.layer.masksToBounds = YES;
    self.contentView.clipsToBounds = YES;
    [self addSubview:self.contentView];
    
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    self.tableView.rowHeight = 30;
    [self.tableView registerClass:[UITableViewCell class]
           forCellReuseIdentifier:@"Cell"];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    self.tableView.backgroundColor = [UIColor clearColor];
    [self addSubview:self.tableView];
}


#pragma mark - Constraints

- (void)setupConstraints {
    
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
}


#pragma mark - 懶加載

@end

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末讲衫,一起剝皮案震驚了整個濱河市缕棵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖招驴,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篙程,死亡現(xiàn)場離奇詭異,居然都是意外死亡别厘,警方通過查閱死者的電腦和手機虱饿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來触趴,“玉大人氮发,你說我怎么就攤上這事〉癖危” “怎么了折柠?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長批狐。 經(jīng)常有香客問我扇售,道長,這世上最難降的妖魔是什么嚣艇? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任承冰,我火速辦了婚禮,結(jié)果婚禮上食零,老公的妹妹穿的比我還像新娘困乒。我一直安慰自己,他們只是感情好贰谣,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布娜搂。 她就那樣靜靜地躺著,像睡著了一般吱抚。 火紅的嫁衣襯著肌膚如雪百宇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天秘豹,我揣著相機與錄音携御,去河邊找鬼。 笑死既绕,一個胖子當著我的面吹牛啄刹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凄贩,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼誓军,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了疲扎?” 一聲冷哼從身側(cè)響起昵时,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤廓译,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后债查,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡瓜挽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年盹廷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片久橙。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡俄占,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出淆衷,到底是詐尸還是另有隱情缸榄,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布祝拯,位于F島的核電站甚带,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏佳头。R本人自食惡果不足惜鹰贵,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望康嘉。 院中可真熱鬧碉输,春花似錦、人聲如沸亭珍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肄梨。三九已至阻荒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間峭范,已是汗流浹背财松。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纱控,地道東北人辆毡。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像甜害,于是被迫代替她去往敵國和親舶掖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355