App中经常会看到各种各样的消息提示框,如下图消息提示一定遇到过:
消息提示框经常需要设置的,文本框宽度,最大高度,箭头指示的高度和宽度,背景颜色,文字颜色,动画,动画时间,消失时间:
public struct FEPreferences {
public struct Drawing {
public var cornerRadius = CGFloat(5)
public var arrowHeight = CGFloat(8)
public var arrowWidth = CGFloat(10)
public var textInset = CGFloat(8)
public var maxTextWidth = CGFloat(180)
public var maxHeight = CGFloat(160)
public var minHeight = CGFloat(40)
public var backgroundColor = UIColor.orange
public var textAlignment = NSTextAlignment.center
public var textColor = UIColor.white
public var textBackGroundColor = UIColor.clear
public var font = UIFont.systemFont(ofSize: 14)
public var message = ""
public var borderWidth = CGFloat(1)
public var borderColor = UIColor.clear
public var hasBorder = false
}
public struct Positioning {
public var targetPoint = CGPoint.zero
public var arrowPosition = UIPopoverArrowDirection.up
}
public struct Animating {
public var showDuration = 0.25
public var delayDuration = 2.0
public var dismissDuration = 0.25
public var shouldDismiss = true
}
public var drawing = Drawing()
public var positioning = Positioning()
public var animating = Animating()
public var hasBorder : Bool {
return drawing.borderWidth > 0 && drawing.borderColor != UIColor.clear
}
public init() {}
}
箭头上下左右的方向事件:
@IBAction func tipAction(_ sender: UIButton) {
var preference:FEPreferences = FEPreferences()
preference.positioning.targetPoint = CGPoint(x: sender.center.x, y: sender.frame.maxY)
preference.drawing.message = "《道德经》是春秋时期老子(李耳)的哲学作品,又称《道德真经》、《老子》、《五千言》、《老子五千文》-FlyElephant"
preference.animating.shouldDismiss = false
let tipView:FETipView = FETipView(preferences: preference)
tipView.show()
}
@IBAction func downAction(_ sender: UIButton) {
var preference:FEPreferences = FEPreferences()
preference.positioning.targetPoint = CGPoint(x: sender.center.x, y: sender.frame.minY)
preference.positioning.arrowPosition = UIPopoverArrowDirection.down
preference.animating.shouldDismiss = false
preference.drawing.message = "《颜氏家训》是汉民族历史上第一部内容丰富,体系宏大的家训,也是一部学术著作。作者颜之推,是南北朝时期著名的文学家、教育家。-FlyElephant"
let tipView:FETipView = FETipView(preferences: preference)
tipView.show()
}
@IBAction func leftAction(_ sender: UIButton) {
var preference:FEPreferences = FEPreferences()
preference.positioning.targetPoint = CGPoint(x: sender.frame.maxX, y: sender.center.y)
preference.positioning.arrowPosition = UIPopoverArrowDirection.left
preference.animating.shouldDismiss = false
preference.drawing.message = "理想国-FlyElephant"
let tipView:FETipView = FETipView(preferences: preference)
tipView.show()
}
@IBAction func rightAction(_ sender: UIButton) {
var preference:FEPreferences = FEPreferences()
preference.positioning.targetPoint = CGPoint(x: sender.frame.minX, y: sender.center.y)
preference.positioning.arrowPosition = UIPopoverArrowDirection.right
preference.animating.shouldDismiss = false
preference.drawing.message = "《解忧杂货店》是日本作家东野圭吾写作的奇幻温情小说。2011年于《小说野性时代》连载,于2012年3月由角川书店发行单行本。该书讲述了在僻静街道旁的一家杂货店,只要写下烦恼投进店前门卷帘门的投信口,第二天就会在店后的牛奶箱里得到回答:因男友身患绝症,年轻女孩月兔在爱情与梦想间徘徊;松冈克郎为了音乐梦想离家漂泊,却在现实中寸步难行;少年浩介面临家庭巨变,挣扎在亲情与未来的迷茫中……他们将困惑写成信投进杂货店,奇妙的事情随即不断发生"
let tipView:FETipView = FETipView(preferences: preference)
tipView.show()
}
完整实现代码:
class FETipView:UIView {
private let screenWidth:CGFloat = UIScreen.main.bounds.width
private let screenHeight:CGFloat = UIScreen.main.bounds.height
private var label:UILabel!
private var message:String = ""
private var preference:FEPreferences!
private var arrowHeight:CGFloat = 0
private var arrowWidth:CGFloat = 0
private var width:CGFloat = 0
private var point:CGPoint = .zero
private var textSize:CGSize = .zero
private var contentSize:CGSize = .zero
private lazy var contenLabel:UILabel = {
var label:UILabel = UILabel.init()
label.lineBreakMode = NSLineBreakMode.byTruncatingTail
label.numberOfLines = 0
return label
}()
convenience init(preferences:FEPreferences) {
self.init()
self.preference = preferences
point = preference.positioning.targetPoint
arrowHeight = preference.drawing.arrowHeight
arrowWidth = preference.drawing.arrowWidth
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up,UIPopoverArrowDirection.down:
width = preference.drawing.maxTextWidth + preference.drawing.textInset * 2
case UIPopoverArrowDirection.left,UIPopoverArrowDirection.right:
width = preference.drawing.maxTextWidth + preference.drawing.textInset * 2 + arrowWidth
default:
width = preference.drawing.maxTextWidth
}
message = preference.drawing.message
self.frame = CGRect(x: point.x - width / 2, y: point.y, width: width, height: preference.drawing.minHeight)
self.backgroundColor = UIColor.clear
}
public func show() {
self.computerTextSize()
self.adjustFrame()
self.contenLabel.text = message
self.contenLabel.font = preference.drawing.font
self.contenLabel.textColor = preference.drawing.textColor
self.contenLabel.backgroundColor = preference.drawing.textBackGroundColor
self.contenLabel.textAlignment = preference.drawing.textAlignment
self.addSubview(contenLabel)
UIApplication.shared.keyWindow?.addSubview(self)
UIView.animate(withDuration: preference.animating.showDuration, delay: 0, options: .curveEaseIn, animations: {
self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
}) { (finished:Bool) in
self.transform = CGAffineTransform.identity
}
if preference.animating.shouldDismiss {
self.perform(#selector(self.dismiss), with: nil, afterDelay: self.preference.animating.delayDuration)
}
}
func dismiss() {
UIView.animate(withDuration: preference.animating.dismissDuration, delay: 0, options: .curveEaseInOut, animations: {
self.alpha = 0
}) { (finished:Bool) in
self.removeFromSuperview()
}
}
// MARK:- Private
private func computerTextSize() {
let textInset:CGFloat = preference.drawing.textInset
let attributes = [NSFontAttributeName : preference.drawing.font]
var textSize = self.message.boundingRect(with: CGSize(width: preference.drawing.maxTextWidth, height: CGFloat.greatestFiniteMagnitude), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: attributes, context: nil).size
textSize.width = ceil(textSize.width)
textSize.height = ceil(textSize.height)
let minHeight:CGFloat = preference.drawing.minHeight
var retainMinHeight:CGFloat = 0
var retainMaxHeight:CGFloat = 0
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up,UIPopoverArrowDirection.down:
retainMinHeight = minHeight - arrowHeight - textInset * 2
retainMaxHeight = preference.drawing.maxHeight - arrowHeight - textInset * 2
break
case UIPopoverArrowDirection.left,UIPopoverArrowDirection.right:
retainMinHeight = minHeight - textInset * 2
retainMaxHeight = preference.drawing.maxHeight - textInset * 2
break
default:
break
}
if textSize.height < retainMinHeight {
textSize.height = retainMinHeight
}
if textSize.height > retainMaxHeight {
textSize.height = retainMaxHeight
}
var contentHeight:CGFloat = 0
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up,UIPopoverArrowDirection.down:
contentHeight = textSize.height + arrowHeight + textInset * 2
break
case UIPopoverArrowDirection.left,UIPopoverArrowDirection.right:
contentHeight = textSize.height + textInset * 2
break
default:
break
}
if textSize.height > retainMinHeight && textSize.height < minHeight {
contentHeight = minHeight
}
self.textSize = textSize
self.contentSize = CGSize(width: width, height: contentHeight)
}
private func adjustFrame() {
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up,UIPopoverArrowDirection.down:
adjustDirectionUpDown()
case UIPopoverArrowDirection.left,UIPopoverArrowDirection.right:
adjustDirectionLeftRight()
default:
break
}
}
private func adjustDirectionUpDown() {
var frameX:CGFloat = point.x - width / 2
var frameY:CGFloat = point.y
let textHInset:CGFloat = ceil(((contentSize.height - arrowHeight) - textSize.height) / 2)
var contentY:CGFloat = textHInset
if (point.x - width / 2) < 0 {
frameX = 1
}
if (point.x + width / 2 > screenWidth) {
frameX = screenWidth - width - 1
}
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up:
contentY = arrowHeight + textHInset
break
case UIPopoverArrowDirection.down:
frameY = point.y - contentSize.height
break
default:
break
}
self.contenLabel.frame = CGRect.init(x: preference.drawing.textInset, y: contentY, width: preference.drawing.maxTextWidth, height: textSize.height)
self.frame = CGRect(x: frameX, y: frameY, width: width, height: contentSize.height)
}
private func adjustDirectionLeftRight() {
var frameX:CGFloat = point.x
var frameY:CGFloat = point.y - contentSize.height / 2
var contentX:CGFloat = preference.drawing.textInset
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.left:
frameX = point.x
contentX = arrowWidth + preference.drawing.textInset
break
case UIPopoverArrowDirection.right:
frameX = point.x - width
contentX = preference.drawing.textInset
break
default:
break
}
if (point.y - contentSize.height / 2) < 0 {
frameY = 1
}
if (point.y + contentSize.height / 2 > screenHeight) {
frameY = screenHeight - contentSize.height - 1
}
self.contenLabel.frame = CGRect.init(x:contentX, y: preference.drawing.textInset, width: preference.drawing.maxTextWidth, height: textSize.height)
self.frame = CGRect(x: frameX, y: frameY, width: width, height: contentSize.height)
}
// MARK:- Override
override func draw(_ rect: CGRect) {
guard UIGraphicsGetCurrentContext() != nil else {
return
}
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
context.setFillColor(preference.drawing.backgroundColor.cgColor)
context.setStrokeColor(preference.drawing.backgroundColor.cgColor)
let contourPath = CGMutablePath()
switch preference.positioning.arrowPosition {
case UIPopoverArrowDirection.up:
drawArrowUp(contourPath: contourPath)
break
case UIPopoverArrowDirection.down:
drawArrowDown(contourPath: contourPath)
break
case UIPopoverArrowDirection.left:
drawArrowLeft(contourPath: contourPath)
break
case UIPopoverArrowDirection.right:
drawArrowRight(contourPath: contourPath)
break
default:
break
}
context.addPath(contourPath)
context.drawPath(using: CGPathDrawingMode.fillStroke)
if preference.drawing.hasBorder {
drawBorder(borderPath: contourPath, context: context)
}
context.restoreGState()
}
private func drawArrowUp(contourPath:CGMutablePath) {
let height:CGFloat = contentSize.height
let radius:CGFloat = preference.drawing.cornerRadius
let beginX:CGFloat = point.x - self.frame.origin.x
contourPath.move(to: CGPoint(x: beginX, y: 0))
contourPath.addLine(to: CGPoint(x: beginX - arrowWidth / 2, y: arrowHeight))
contourPath.addArc(tangent1End:CGPoint(x: 0, y: arrowHeight), tangent2End: CGPoint(x: 0, y:height), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: 0, y: height), tangent2End: CGPoint(x: width, y: height), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: height), tangent2End: CGPoint(x: width, y: arrowHeight), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: arrowHeight), tangent2End: CGPoint(x: 0, y: arrowHeight), radius: radius)
contourPath.addLine(to: CGPoint(x: beginX + arrowWidth / 2, y: arrowHeight))
contourPath.addLine(to: CGPoint(x: beginX, y: 0))
}
private func drawArrowDown(contourPath:CGMutablePath) {
let contentHeight:CGFloat = contentSize.height
let radius:CGFloat = preference.drawing.cornerRadius
let beginX:CGFloat = point.x - self.frame.origin.x
let height:CGFloat = contentHeight - arrowHeight
contourPath.move(to: CGPoint(x: beginX, y: contentHeight))
contourPath.addLine(to: CGPoint(x: beginX - arrowWidth / 2, y: height))
contourPath.addArc(tangent1End:CGPoint(x: 0, y: height), tangent2End: CGPoint(x: 0, y: 0), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: width, y: 0), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: 0), tangent2End: CGPoint(x: width, y: height), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: height), tangent2End: CGPoint(x: 0, y: height), radius: radius)
contourPath.addLine(to: CGPoint(x: beginX + arrowWidth / 2, y: height))
contourPath.addLine(to: CGPoint(x: beginX, y: contentHeight))
}
private func drawArrowLeft(contourPath:CGMutablePath) {
let contentHeight:CGFloat = contentSize.height
let radius:CGFloat = preference.drawing.cornerRadius
let beginY:CGFloat = point.y - self.frame.origin.y
contourPath.move(to: CGPoint(x: 0, y: beginY))
contourPath.addLine(to: CGPoint(x: arrowWidth, y: beginY - arrowHeight / 2))
contourPath.addArc(tangent1End:CGPoint(x: arrowWidth, y: 0), tangent2End: CGPoint(x: width, y: 0), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: 0), tangent2End: CGPoint(x: width, y: contentHeight), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: width, y: contentHeight), tangent2End: CGPoint(x: arrowWidth, y: contentHeight), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: arrowWidth, y: contentHeight), tangent2End: CGPoint(x: arrowWidth, y: 0), radius: radius)
contourPath.addLine(to: CGPoint(x: arrowWidth, y: beginY + arrowHeight / 2))
contourPath.addLine(to: CGPoint(x: 0, y: beginY))
}
private func drawArrowRight(contourPath:CGMutablePath) {
let contentHeight:CGFloat = contentSize.height
let radius:CGFloat = preference.drawing.cornerRadius
let beginY:CGFloat = point.y - self.frame.origin.y
let pathWidth:CGFloat = width - arrowWidth
contourPath.move(to: CGPoint(x: width, y: beginY))
contourPath.addLine(to: CGPoint(x: pathWidth, y: beginY - arrowHeight / 2))
contourPath.addArc(tangent1End:CGPoint(x: pathWidth, y: 0), tangent2End: CGPoint(x: 0, y: 0), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: 0, y: contentHeight), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: 0, y: contentHeight), tangent2End: CGPoint(x: pathWidth, y: contentHeight), radius: radius)
contourPath.addArc(tangent1End:CGPoint(x: pathWidth, y: contentHeight), tangent2End: CGPoint(x: pathWidth, y: 0), radius: radius)
contourPath.addLine(to: CGPoint(x: pathWidth, y: beginY + arrowHeight / 2))
contourPath.addLine(to: CGPoint(x: width, y: beginY))
}
private func drawBorder(borderPath: CGPath, context: CGContext) {
context.addPath(borderPath)
context.setStrokeColor(preference.drawing.borderColor.cgColor)
context.setLineWidth(preference.drawing.borderWidth)
context.strokePath()
}
}