概述
说起这个功能,各大技术论坛已经有比较成熟的博客讲解了如何实现这一功能,笔者今天还要在这里记录一下自己对这个功能的理解以及技术实现,同时也感谢各位大佬的相关文章以及技术思路。
效果展示
先来看一下完成后的拖动效果:
在短距离拖拽的时候,会有一个拉伸的效果出现,如果拖拽的距离大于某个值,拉伸效果消失,被拖拽的消息框随手势移动,当松开手的时候,消息框回到原位置(也可以在松手的时候加一个爆炸效果,然后让消息框消失)。
实现思路
首先,先把动画拆解一下:
- 准备两个相同的圆,放到同一个位置,下面的圆固定不动,上层的圆显示消息数量,并可以拖动。
- 拖动的时候计算关键点,绘制贝塞尔曲线。
- 拖拽的时候下面固定的圆半径逐渐较小。
- 当下面固定的圆半径小于一半的时候,移除贝塞尔曲线,消息圆随手势移动。
- 当松手的时候恢复下面固定的圆,并将消息圆恢复到原位置。
整个动画的难点在于计算绘制贝塞尔曲线的控制点。
下图详细表示了动画过程中两个圆以及各点的关系。
上图中左下角的圆为圆1,右上角的圆为圆2,圆2可以拖动。
图中蓝色区域,即ABFCDEA,则是贝塞尔绘制区域。所以需要计算出A, B, C, D, E, F这6个点。
在计算过程中,有几个关键的值,圆1半径r1,圆2半径r2。两个圆心的距离d,角α:图中左右标注α的角都是相等的,至于为什么,这里就不做具体说明了。
由上述已知计算出A, B, C, D, E, F这6个点。
A点坐标:
B点坐标:
C点坐标:
D点坐标:
E点坐标:, Ax为A点的x坐标点,Ay为A点的y坐标点。
F点的坐标:, Bx为B点的x坐标点,By为B点的y坐标点。
最难的一部分搞定后,下面就看看代码实现吧。
完整代码
class ViewController: UIViewController {
// 两个圆的初始直径
let circleWidth: CGFloat = 40.0
// 圆1,固定圆
var circleView1: UIView = UIView()
// 圆2,拖拽圆
var circleView2: UIView = UIView()
// 贝塞尔图形
var shapeLayer: CAShapeLayer = CAShapeLayer()
// 记录圆1出示位置及大小
var circleOriginFrame: CGRect = .zero
// 记录圆1的半径
var circleR1: CGFloat = .zero
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// 初始化UI
func setupUI() {
// 初始化圆1
circleView1.frame = CGRect(x: (self.view.frame.size.width - circleWidth) / 2.0, y: (self.view.bounds.size.height - circleWidth) / 2, width: circleWidth, height: circleWidth)
circleView1.backgroundColor = UIColor.red
circleView1.layer.cornerRadius = circleWidth / 2.0
circleView1.layer.masksToBounds = true
view.addSubview(circleView1)
// 初始化圆2
circleView2.frame = circleView1.frame
circleView2.backgroundColor = UIColor.red
circleView2.layer.cornerRadius = circleWidth / 2.0
circleView2.layer.masksToBounds = true
view.addSubview(circleView2)
// 添加消息数字
let numberLabel = UILabel(frame: circleView1.bounds)
numberLabel.backgroundColor = circleView2.backgroundColor
numberLabel.text = "99"
numberLabel.textAlignment = .center
numberLabel.textColor = UIColor.white
circleView2.addSubview(numberLabel)
// 给圆2添加平移手势
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
circleView2.addGestureRecognizer(panGestureRecognizer)
// 赋初始值
circleOriginFrame = circleView1.frame
circleR1 = circleOriginFrame.size.width / 2.0
}
@objc func panAction(_ pan: UIPanGestureRecognizer) {
if pan.state == .changed { // 拖拽时
// 计算移动点,并使圆2随之移动。
let point = pan.location(in: self.view)
circleView2.center = point
// 当记录的圆1的半径小于其直径的四分之一的时候(也就是圆1的半径小于一半的时候)
if circleR1 < circleOriginFrame.size.width / 4.0 {
// 移除绘制的贝塞尔图形
shapeLayer.removeFromSuperlayer()
// 隐藏圆1
circleView1.isHidden = true
}else {
// 绘制贝塞尔图形
drawShapeLayer()
}
} else if pan.state == .ended || pan.state == .failed || pan.state == .cancelled { // 松开时
// 隐藏圆1
circleView1.isHidden = true
// 移除绘制的贝塞尔图形
shapeLayer.removeFromSuperlayer()
// 执行圆2回弹动画
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.0, options: .curveEaseInOut) { [weak self] in
if let this = self {
// 圆2回到初始位置
this.circleView2.center = this.circleView1.center
}
} completion: { [weak self] (finished) in
if let this = self {
// 动画结束后,圆1取消隐藏,恢复原始尺寸。
this.circleView1.isHidden = false
this.circleView1.bounds = this.circleOriginFrame
this.circleR1 = this.circleOriginFrame.size.width / 2.0
this.circleView1.layer.cornerRadius = this.circleR1
}
}
}
}
// 绘制贝塞尔图形
func drawShapeLayer() {
// 如果圆1隐藏了,停止绘制贝塞尔图形
if circleView1.isHidden {
return
}
// 计算圆1圆心位置
let center1 = circleView1.center
// 计算圆2圆心位置
let center2 = circleView2.center
// 计算两个圆圆心距离d
let d = CGFloat(sqrtf(Float(pow(center2.x - center1.x, 2) + pow(center2.y - center1.y, 2))))
// 计算sinα、cosα
let sinα = (center2.x - center1.x) / d
let cosα = (center1.y - center2.y) / d
// 计算圆1半径,逐渐变小。
let r1 = circleOriginFrame.size.width / 2.0 - CGFloat(d/20.0)
// 计算圆2半径,固定不变。
let r2 = circleView2.bounds.size.width / 2.0
// 记录圆1半径
circleR1 = r1
// 计算6个控制点坐标。
let pointA = CGPoint(x: center1.x - r1 * cosα, y: center1.y - r1 * sinα)
let pointB = CGPoint(x: center1.x + r1 * cosα, y: center1.y + r1 * sinα)
let pointC = CGPoint(x: center2.x + r2 * cosα, y: center2.y + r2 * sinα)
let pointD = CGPoint(x: center2.x - r2 * cosα, y: center2.y - r2 * sinα)
let pointE = CGPoint(x: pointA.x + d / 2.0 * sinα, y: pointA.y - d / 2.0 * cosα)
let pointF = CGPoint(x: pointB.x + d / 2.0 * sinα, y: pointB.y - d / 2.0 * cosα)
// 绘制贝塞尔曲线
let path = UIBezierPath()
path.move(to: pointA)
path.addLine(to: pointB)
path.addQuadCurve(to: pointC, controlPoint: pointF)
path.addLine(to: pointD)
path.addQuadCurve(to: pointA, controlPoint: pointE)
// 绘制图层并填充颜色
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.red.cgColor
self.view.layer.insertSublayer(shapeLayer, below: circleView2.layer)
// 修改圆1的尺寸
circleView1.bounds = CGRect(x: 0, y: 0, width: r1 * 2, height: r1 * 2)
circleView1.center = center1
circleView1.layer.cornerRadius = r1
}
}
总结
该功能的整体代码不是很多,不过如果是用于复杂界面则另当别论了。主要部分则是在计算控制点的位置,至于松手后要回弹还是加一个爆炸效果,看需求了。
有些时候难的不是代码,不是数学,而是idea。非常佩服想出这个动画并第一个实现的人。
本文如有不妥之处,还望路过的朋友指正。
本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。