需實(shí)現(xiàn)效果:
我們在日常開發(fā)中經(jīng)常會用到這樣的氣泡控件,以前都是直接在GitHub里面找一個(gè)典勇,最近有時(shí)間就想著自己寫一個(gè)羹奉。
思路&實(shí)現(xiàn)路線
1.獲取必要參數(shù)
首先就是頂部的三角形,它的頂點(diǎn)是在我們點(diǎn)擊的view
中心點(diǎn)的下方煤辨,所以要先拿到點(diǎn)擊的view
的frame
裳涛,因此我們就需要一個(gè)這樣的必要參數(shù):pointView
,把這個(gè)參數(shù)寫到init
方法里面,參數(shù):
///lineHeight : 每一行的高度众辨, titles:標(biāo)題端三,image:圖片,要與titles數(shù)量想的鹃彻,target:響應(yīng)時(shí)間郊闯,需要?jiǎng)?chuàng)建一個(gè)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 {
_="圖片和文字的數(shù)量必須要相等!"
abort()
}
}
if let view = sender as? UIView {
self.pointView = view
}else if let view = sender.value(forKey: "view") as? UIView {
//sender如果是UIBarButtonItem的時(shí)候
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
}
以上參數(shù)需要解釋的是target
欢摄,這是之前我的一個(gè)大佬同事教我的熬丧,目的是使用perform
這個(gè)方法來替代block
,降低因使用block
而引起循環(huán)引用的幾率怀挠。當(dāng)然析蝴,也并不是適用替換所有的使用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
,你點(diǎn)的是誰不重要吞滞,重要的是你把誰當(dāng)做pointView
傳過來佑菩,就以誰為標(biāo)準(zhǔn)來顯示。
在init
方法里面獲取到了我們需要的所有必要參數(shù)裁赠,需要顯示的標(biāo)題數(shù)組:titles
殿漠,點(diǎn)擊的view:pointView
。拿到這兩個(gè)參數(shù)我們就可以確定氣泡的具體位置了组贺。至于其他的參數(shù)都是可有可無凸舵,直接給個(gè)默認(rèn)值就行。當(dāng)然失尖,暴露出來給調(diào)用者選擇更好啊奄。
2.準(zhǔn)備畫圖
拿到數(shù)組以后,我們首先要做的要看一下這個(gè)數(shù)組中最長的字符串的長度是多少掀潮。因?yàn)槲覀兊倪@個(gè)氣泡肯定是要按照最長的長度來畫菇夸。
于是我選擇循環(huán)來拿到最大長度,并將兩邊留出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
的位置
//這個(gè)參數(shù)作用是計(jì)算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
的數(shù)量薯鼠,那就可以直接確定氣泡的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太靠邊的話择诈,我們也要適當(dāng)調(diào)整一下位置
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)
然后開始畫三角形:
//以視圖的中心點(diǎn)為原點(diǎn)找位置
//就是以pointView的center.x為原點(diǎn),獲取x軸坐標(biāo)點(diǎn)出皇,并適當(dāng)調(diào)整位置羞芍,不要太靠邊,可參照下圖理解其作用
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()
//點(diǎn)擊的視圖下方間距是否足夠氣泡
let isBottomSpaceEnough = kBottomSapce >= bubbleView.ln_height
if !isBottomSpaceEnough {
//下方位置不夠時(shí)郊艘,氣泡的位置也要變一下荷科,箭頭需要反過來唯咬,列表就在上面了
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)
箭頭畫完了畏浆,開始寫列表了胆胰,我就直接用了一個(gè)循環(huán):
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]?
//點(diǎn)擊到的view
fileprivate var pointView:UIView!
//展示整個(gè)氣泡的父容器
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:標(biāo)題刻获,image:圖片蜀涨,要與titles數(shù)量想的,target:響應(yīng)時(shí)間将鸵,需要?jiǎng)?chuàng)建一個(gè)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 {
_="圖片和文字的數(shù)量必須要相等挑胸!"
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太靠邊的話痒筒,我們也要適當(dāng)調(diào)整一下位置
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)
//以視圖的中心點(diǎn)為原點(diǎn)找位置
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()
//點(diǎn)擊的視圖下方間距是否足夠顯示氣泡
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
}
}