iOS 仿QQ消息拖动动画效果

概述

说起这个功能,各大技术论坛已经有比较成熟的博客讲解了如何实现这一功能,笔者今天还要在这里记录一下自己对这个功能的理解以及技术实现,同时也感谢各位大佬的相关文章以及技术思路。

效果展示

先来看一下完成后的拖动效果:

在短距离拖拽的时候,会有一个拉伸的效果出现,如果拖拽的距离大于某个值,拉伸效果消失,被拖拽的消息框随手势移动,当松开手的时候,消息框回到原位置(也可以在松手的时候加一个爆炸效果,然后让消息框消失)。

实现思路

首先,先把动画拆解一下:

  1. 准备两个相同的圆,放到同一个位置,下面的圆固定不动,上层的圆显示消息数量,并可以拖动。
  2. 拖动的时候计算关键点,绘制贝塞尔曲线。
  3. 拖拽的时候下面固定的圆半径逐渐较小。
  4. 当下面固定的圆半径小于一半的时候,移除贝塞尔曲线,消息圆随手势移动。
  5. 当松手的时候恢复下面固定的圆,并将消息圆恢复到原位置。

整个动画的难点在于计算绘制贝塞尔曲线的控制点。

下图详细表示了动画过程中两个圆以及各点的关系。

上图中左下角的圆为圆1,右上角的圆为圆2,圆2可以拖动。

图中蓝色区域,即ABFCDEA,则是贝塞尔绘制区域。所以需要计算出A, B, C, D, E, F这6个点。

在计算过程中,有几个关键的值,圆1半径r1,圆2半径r2。两个圆心的距离d,角α:图中左右标注α的角都是相等的,至于为什么,这里就不做具体说明了。

 d = \sqrt{(x_2-x_1)^2+(y_1-y_2)^2}

sin\partial = \frac{x_2 - x_1}{d}

cos\partial = \frac{y_1-y_2}{d}

由上述已知计算出A, B, C, D, E, F这6个点。

A点坐标:(x_1-r_1\times cos\partial, y_1-r_1\times sin\partial)

B点坐标:(x_1+r_1\times cos\partial, y_1+r_1\times sin\partial)

C点坐标:(x_2+r_2\times cos\partial, y_2+r_2\times sin\partial)

D点坐标:(x_2-r_2\times cos\partial, y_2-r_2\times sin\partial)

E点坐标:(A_x+\frac{d}{2}\times sin\partial, A_y-\frac{d}{2}\times cos\partial), Ax为A点的x坐标点,Ay为A点的y坐标点。

F点的坐标:(B_x+\frac{d}{2}\times sin\partial, B_y-\frac{d}{2}\times cos\partial), 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的博客,如需转载,请标明出处。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值