iOS开发 气泡和sheet

Demo 地址

需实现效果:

气泡.png

我们在日常开发中经常会用到这样的气泡控件,以前都是直接在GitHub里面找一个,最近有时间就想着自己写一个。

思路&实现路线

1.获取必要参数

首先就是顶部的三角形,它的顶点是在我们点击的view中心点的下方,所以要先拿到点击的viewframe,因此我们就需要一个这样的必要参数:pointView,把这个参数写到init方法里面,参数:

    ///lineHeight : 每一行的高度, titles:标题,image:图片,要与titles数量想的,target:响应时间,需要创建一个Target类型,bubbleStyle:0dark,黑暗色,1light,明亮色
    @objc init(lineHeight: CGFloat = 44, titles: [String], images:[Any]? = nil, target: Target?=nil, bubbleStyle:KLNBubbleStyle = .dark, sender: NSObject) {

        self.lineHeight = lineHeight
        self.titles = titles
        
        if let images = images, images.count > 0 {
            self.images = images
            if titles.count != images.count {
                _="图片和文字的数量必须要相等!"
                abort()
            }
        }
        
        if let view = sender as? UIView {
            self.pointView = view         
        }else if let view = sender.value(forKey: "view") as? UIView {
         //sender如果是UIBarButtonItem的时候
            self.pointView = view
        }
        self.bubbleStyle = bubbleStyle

        let alpha:CGFloat = 0.98
        if bubbleStyle == .dark {
            kTextColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
        }else{
            kTextColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
        }
        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overCurrentContext
    }

以上参数需要解释的是target,这是之前我的一个大佬同事教我的,目的是使用perform这个方法来替代block,降低因使用block而引起循环引用的几率。当然,也并不是适用替换所有的使用block的场景,我贴一下,可选择使用。

@objc public class Target: NSObject {
    @objc weak var target : NSObject?
    @objc var selector : Selector?
    
    @objc func perform(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func doAction(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func perform(object1: Any!, object2: Any!) {
        target?.perform(selector, with: object1, with: object2)
    }
    
    @objc init(target:NSObject?, selector:Selector?) {
        super.init()
        self.selector = selector
        self.target = target
    }
    
}

还有pointView,你点的是谁不重要,重要的是你把谁当做pointView传过来,就以谁为标准来显示。

init方法里面获取到了我们需要的所有必要参数,需要显示的标题数组:titles,点击的view:pointView。拿到这两个参数我们就可以确定气泡的具体位置了。至于其他的参数都是可有可无,直接给个默认值就行。当然,暴露出来给调用者选择更好。

2.准备画图

拿到数组以后,我们首先要做的要看一下这个数组中最长的字符串的长度是多少。因为我们的这个气泡肯定是要按照最长的长度来画。
于是我选择循环来拿到最大长度,并将两边留出8像素的空白,如果有图片的话,再加上24给图片留位置:

        var maxWidth:CGFloat = 0
        for text in titles {
            let width = KGetLabWidth(labelStr: text, font: font, height: lineHeight)
            maxWidth = maxWidth > width ? maxWidth:width
        }
        
        maxWidth = maxWidth + 16 + (images == nil ? 0:24)

然后确定顶部三角形的高度

    //三角的高度
    fileprivate var angleHeight:CGFloat = 12

拿到pointView的位置

        //这个参数作用是计算pointView底部距离屏幕底部的高度是否够用
        var kBottomSapce:CGFloat = 0
        
        var frame = CGRect.zero
        if let window = UIApplication.shared.windows.first {
            frame = pointView.convert(pointView.bounds, to: window)
            kBottomSapce = window.frame.size.height - frame.origin.y
        }

至此,我们拿到了pointView的位置、三角形的高度和titles的数量,那就可以直接确定气泡的frame了:

        bubbleView = UIView.init(frame: CGRect.init(x: 0, y: frame.origin.y + frame.size.height, width: maxWidth, height: CGFloat(titles.count) * lineHeight + angleHeight))
        self.view.addSubview(bubbleView)

        //左右间隙不能太小
        let centerX = frame.midX
        //气泡view和pointView垂直对齐
        bubbleView.center.x = centerX

        //左右间隙不能太小,如果pointView太靠边的话,我们也要适当调整一下位置
        if centerX + maxWidth/2  > UIScreen.width {
            bubbleView.ln_right = UIScreen.width - 5
        }
        if centerX - maxWidth/2  < 0 {
            bubbleView.ln_x = 5
        }

然后在bubbleView里面添加三角形视图和下面的列表:

        let angleView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: bubbleView.ln_width, height: angleHeight))
        bubbleView.addSubview(angleView)
                
        let showView = UIView.init(frame: CGRect.init(x: 0, y: angleHeight, width: bubbleView.ln_width, height: bubbleView.ln_height - angleHeight))
        showView.ln_cornerRadius = 4
        showView.backgroundColor = kBackColor
        bubbleView.addSubview(showView)

然后开始画三角形:


        //以视图的中心点为原点找位置
        //就是以pointView的center.x为原点,获取x轴坐标点,并适当调整位置,不要太靠边,可参照下图理解其作用
        func getX(_ value: CGFloat) -> CGFloat {
            
            var x = centerX  - bubbleView.ln_x
            x = x > bubbleView.ln_width - 14 ? bubbleView.ln_width - 14:x
            x = x < 14 ? 14:x
            
            return x  + value
        }

        let bezir = UIBezierPath.init()
        //点击的视图下方间距是否足够气泡
        let isBottomSpaceEnough = kBottomSapce >= bubbleView.ln_height
        if !isBottomSpaceEnough {
            //下方位置不够时,气泡的位置也要变一下,箭头需要反过来,列表就在上面了
            bubbleView.ln_y = frame.origin.y - bubbleView.ln_height
            angleView.ln_y = bubbleView.ln_height - angleHeight
            showView.ln_y = 0
            //箭头向下
            bezir.move(to: CGPoint.init(x: getX(-10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 7.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: 0))
        }else{
            //箭头向上
            bezir.move(to: CGPoint.init(x: getX(-10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 3.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: angleHeight))
        }
        
        let shape = CAShapeLayer.init()
        shape.lineWidth = 1
        shape.fillColor = kBackColor.cgColor
        shape.cornerRadius = 3
        shape.path = bezir.cgPath
        angleView.layer.addSublayer(shape)
当pointView太过靠边的时候,箭头适当往内侧移动.png

箭头画完了,开始写列表了,我就直接用了一个循环:

        for index in 0..<titles.count {
            let buttonItem = UIButton.init(frame: CGRect.init(x: 0, y: CGFloat(index)*lineHeight, width: maxWidth, height: lineHeight))
            buttonItem.setTitle(titles[index], for: .normal)
           
            if images != nil {
                if let string = images?[index] as? String {
                    if string.hasPrefix("http") {
                        //换上你喜欢的加载图片的方式
                        //buttonItem.kf.setImage(with: URL.init(string: string), for: .normal, placeholder: UIImage.init(named: "placeholder_1"))
                    }else{
                        buttonItem.setImage(UIImage.init(named: string), for: .normal)
                    }
                }else if let image = images?[index] as? UIImage {
                    buttonItem.setImage(image, for: .normal)
                }
            }
            buttonItem.titleLabel?.font = font
            buttonItem.setTitleColor(kTextColor, for: .normal)
            buttonItem.addTarget(self, action: #selector(chooseTarget(sender:)), for: .touchUpInside)
            buttonItem.tag = 100+index
            showView.addSubview(buttonItem)
            
            if index == titles.count - 1 {
                break
            }
            let bottomLine = UIView.init(frame: CGRect.init(x: 4, y: buttonItem.ln_height-1, width: buttonItem.ln_width - 8, height: 0.5))
            bottomLine.backgroundColor = kTextColor
            buttonItem.addSubview(bottomLine)
        }

全部文件代码


import UIKit
import LNTools_fyh

@objc public enum KLNBubbleStyle : Int {
    case dark = 0
    case light
}

class BubbleViewController: UIViewController {
    @objc public var target : Target?

    @objc public var bubbleStyle = KLNBubbleStyle.dark
    //每行的高度
    fileprivate var lineHeight:CGFloat = 44
    //title
    fileprivate var titles:[String] = []
    //图片image
    fileprivate var images:[Any]?
    //点击到的view
    fileprivate var pointView:UIView!
    
    //展示整个气泡的父容器
    fileprivate var bubbleView : UIView!
    //字体大小
    var font = UIFont.systemFont(ofSize: 16)
    //三角的高度
    fileprivate var angleHeight:CGFloat = 12
    
    //文字颜色
    private var kTextColor = UIColor.black.withAlphaComponent(0.95)
    //背景颜色
    private var kBackColor = UIColor.white.withAlphaComponent(0.95)

    public typealias LNDidSelectBlock = (_ title:String, _ index:Int) -> Void
    fileprivate var didSelect:LNDidSelectBlock? = nil
    public func didSelectAction(callback:@escaping LNDidSelectBlock) {
        self.didSelect = callback
    }
    
    ///lineHeight : 每一行的高度, titles:标题,image:图片,要与titles数量想的,target:响应时间,需要创建一个Target类型,bubbleStyle:0dark,黑暗色,1light,明亮色
    @objc init(lineHeight: CGFloat = 44, titles: [String], images:[Any]? = nil, target: Target?=nil, bubbleStyle:KLNBubbleStyle = .dark, sender: NSObject) {

        self.lineHeight = lineHeight
        self.titles = titles
        
        if let images = images, images.count > 0 {
            self.images = images
            if titles.count != images.count {
                _="图片和文字的数量必须要相等!"
                abort()
            }
        }
        
        if let view = sender as? UIView {
            self.pointView = view
        }else if let view = sender.value(forKey: "view") as? UIView {
            self.pointView = view
        }
        self.bubbleStyle = bubbleStyle
        
        let alpha:CGFloat = 0.98
        if bubbleStyle == .dark {
            kTextColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
        }else{
            kTextColor = UIColor.init(hex: "#1F1F1F").withAlphaComponent(alpha)
            kBackColor = UIColor.init(hex: "#F7F9Fb").withAlphaComponent(alpha)
        }

        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overCurrentContext
    }

    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = UIColor.black.withAlphaComponent(0.11)
        configSubViews()
    }
    
    
    fileprivate func configSubViews() {
        
        var maxWidth:CGFloat = 0
        for text in titles {
            let width = KGetLabWidth(labelStr: text, font: font, height: lineHeight)
            maxWidth = maxWidth > width ? maxWidth:width
        }
        
        maxWidth = maxWidth + 16 + (images == nil ? 0:24)
        
        var kBottomSapce:CGFloat = 0
        
        var frame = CGRect.zero
        if let window = UIApplication.shared.windows.first {
            frame = pointView.convert(pointView.bounds, to: window)
            kBottomSapce = window.frame.size.height - frame.origin.y
        }
        
        bubbleView = UIView.init(frame: CGRect.init(x: 0, y: frame.origin.y + frame.size.height, width: maxWidth, height: CGFloat(titles.count) * lineHeight + angleHeight))
        self.view.addSubview(bubbleView)
        
        let centerX = frame.midX
        //气泡view和pointView垂直对齐
        bubbleView.center.x = centerX
        
        //左右间隙不能太小,如果pointView太靠边的话,我们也要适当调整一下位置
        if centerX + maxWidth/2  > UIScreen.width {
            bubbleView.ln_right = UIScreen.width - 5
        }
        if centerX - maxWidth/2  < 0 {
            bubbleView.ln_x = 5
        }
        
        let angleView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: bubbleView.ln_width, height: angleHeight))
        bubbleView.addSubview(angleView)
                
        let showView = UIView.init(frame: CGRect.init(x: 0, y: angleHeight, width: bubbleView.ln_width, height: bubbleView.ln_height - angleHeight))
        showView.ln_cornerRadius = 4
        showView.backgroundColor = kBackColor
        bubbleView.addSubview(showView)
        
        //以视图的中心点为原点找位置
        func getX(_ value: CGFloat) -> CGFloat {
            
            var x = centerX  - bubbleView.ln_x
            x = x > bubbleView.ln_width - 14 ? bubbleView.ln_width - 14:x
            x = x < 14 ? 14:x
            
            return x  + value
        }
        
        let bezir = UIBezierPath.init()
        //点击的视图下方间距是否足够显示气泡
        let isBottomSpaceEnough = kBottomSapce >= bubbleView.ln_height
        if !isBottomSpaceEnough {
            bubbleView.ln_y = frame.origin.y - bubbleView.ln_height
            angleView.ln_y = bubbleView.ln_height - angleHeight
            showView.ln_y = 0
            //箭头向下
            bezir.move(to: CGPoint.init(x: getX(-10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 7.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: 0))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: 0))
        }else{
            //箭头向上
            bezir.move(to: CGPoint.init(x: getX(-10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(0), y: 3.5))
            bezir.addLine(to: CGPoint.init(x: getX(10), y: angleHeight))
            bezir.addLine(to: CGPoint.init(x: getX(-10), y: angleHeight))
        }
        
        let shape = CAShapeLayer.init()
        shape.lineWidth = 1
        shape.fillColor = kBackColor.cgColor
        shape.cornerRadius = 3
        shape.path = bezir.cgPath
        angleView.layer.addSublayer(shape)
        
        for index in 0..<titles.count {
            let buttonItem = UIButton.init(frame: CGRect.init(x: 0, y: CGFloat(index)*lineHeight, width: maxWidth, height: lineHeight))
            buttonItem.setTitle(titles[index], for: .normal)
           
            if images != nil {
                if let string = images?[index] as? String {
                    if string.hasPrefix("http") {
                        //换上你喜欢的加载图片的方式
//                        buttonItem.kf.setImage(with: URL.init(string: string), for: .normal, placeholder: UIImage.init(named: "placeholder_1"))
                    }else{
                        buttonItem.setImage(UIImage.init(named: string), for: .normal)
                    }
                }else if let image = images?[index] as? UIImage {
                    buttonItem.setImage(image, for: .normal)
                }
            }
            buttonItem.titleLabel?.font = font
            buttonItem.setTitleColor(kTextColor, for: .normal)
            buttonItem.addTarget(self, action: #selector(chooseTarget(sender:)), for: .touchUpInside)
            buttonItem.tag = 100+index
            showView.addSubview(buttonItem)
            
            if index == titles.count - 1 {
                break
            }
            let bottomLine = UIView.init(frame: CGRect.init(x: 4, y: buttonItem.ln_height-1, width: buttonItem.ln_width - 8, height: 0.5))
            bottomLine.backgroundColor = kTextColor
            buttonItem.addSubview(bottomLine)
        }
    }
    
    
    @objc func chooseTarget(sender: UIButton) {
        
        let index = sender.tag-100
        
        target?.perform(object1: titles[index], object2: "\(index)")
        didSelect?(titles[index],index)
        
        UIView.animate(withDuration: 0.15) {
            self.bubbleView.alpha = 0
        }
        self.dismiss(animated: false, completion: nil)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        UIView.animate(withDuration: 0.15) {
            self.bubbleView.alpha = 0
        }
        self.dismiss(animated: false, completion: nil)
    }

    //MARK:获取字符串的宽度的封装
    func KGetLabWidth(labelStr:String,font:UIFont,height:CGFloat) -> CGFloat {
        
        let statusLabelText: NSString = labelStr as NSString
        
        let size = CGSize(width: 900, height: height)
        
        let dic = NSDictionary(object: font, forKey: NSAttributedString.Key.font as NSCopying)
        
        let strSize = statusLabelText.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: dic as? [NSAttributedString.Key : Any], context:nil).size
        
        return strSize.width
    }
}




@objc public class Target: NSObject {
    @objc weak var target : NSObject?
    @objc var selector : Selector?
    
    @objc func perform(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func doAction(object: Any!) {
        target?.perform(selector, with: object)
    }
    
    @objc func perform(object1: Any!, object2: Any!) {
        target?.perform(selector, with: object1, with: object2)
    }
    
    @objc init(target:NSObject?, selector:Selector?) {
        super.init()
        self.selector = selector
        self.target = target
    }
    
}

Demo 地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335