仿抖音点赞按钮

3,103 阅读3分钟

每一个图形,都是通过一点点拼接到一起的,而每一个动画亦然,只需要将动画和图形进行拆解,就不难了。

模仿下抖音点赞按钮的动画效果。

拆解一下动画效果。

  • 红色爱心逐渐变大的过程,并伴随着有左右旋转的效果。
  • 与此同时,伴随着六个扇形块向外扩散的效果。
  • 再次点击时,红色爱心旋转45°,逐渐变小。

具体实现

1、通过 UIBezierPath 实现爱心的上半部分。

let rect = CGRect(x: 10, y: 10, width: frame.width - 20, height: frame.height - 20)
//距离左右两边的距离
let padding: CGFloat = 4
let radius = (rect.size.width - 2 * padding) / 2.0 / (cos(CGFloat.pi / 4) + 1)
let heartPath = UIBezierPath()
//左圆的圆心
let leftCurveCenter = CGPoint(x: padding + radius, y: rect.size.height / 2.8)
//画左圆
heartPath.addArc(withCenter: leftCurveCenter, radius: radius,
                 startAngle: CGFloat.pi, endAngle: CGFloat.pi * -0.25,
                 clockwise: true)
//右圆圆心
let rightCurveCenter = CGPoint(x: rect.width - padding - radius, y: leftCurveCenter.y)
//画右圆
heartPath.addArc(withCenter: rightCurveCenter, radius: radius,
                 startAngle: CGFloat.pi * -0.75, endAngle: 0,
                 clockwise: true)

let shapeLayer = CAShapeLayer()
shapeLayer.path = heartPath.cgPath
shapeLayer.frame = rect

爱心的上半部分是由两个 3/4 圆, 圆半径的计算公式:(width - padding * 2) / 2 = radius + radius * cos45°,可得出 radius = (width - padding * 2) / 2 / (cos45° + 1),此公式是根据两个半圆交点的切线呈 90°

2、通过 UIBezierPath 完成爱心的下半部分。其中爱心尖的位置,为整个 UIButton 的底部中心点

//爱心尖的坐标点
let apexPoint = CGPoint(x: rect.width / 2, y: rect.height - padding)
//画右半边曲线
heartPath.addQuadCurve(to: apexPoint,
                       controlPoint: CGPoint(x: heartPath.currentPoint.x,
                                             y: radius + rect.size.height / 2.8))
//画左半边曲线
heartPath.addQuadCurve(to: CGPoint(x: padding, y: leftCurveCenter.y),
                       controlPoint: CGPoint(x: padding,
                                             y: radius + rect.size.height / 2.8))

3、此处采用两个 layer 层,一个为白色的爱心,一个为红色的爱心。通过白色爱心隐藏,红色爱心逐渐变大,并伴随左右旋转。放大的动画执行完,旋转的动画还未执行完,所以放大的动画时间相对短一些

//放大动画
let animation = CABasicAnimation.init(keyPath: "transform.scale")
animation.duration = 1 * 0.8
animation.fromValue = 0.1
animation.toValue = 1

//旋转动画
let keyAnimation = CAKeyframeAnimation.init(keyPath: "transform.rotation.z")
keyAnimation.values = [CGFloat.pi * -0.25, 0, CGFloat.pi * 0.1, CGFloat.pi * -0.05, 0]
keyAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
keyAnimation.duration = 1

//合成动画组
let groupAnimation = CAAnimationGroup()
groupAnimation.duration = 1
groupAnimation.delegate = self
groupAnimation.animations = [keyAnimation, animation]
redHeartLayer.isHidden = false
hollowHeartLayer.isHidden = true
redHeartLayer.add(groupAnimation, forKey: nil)

4、显示白色的爱心,红色爱心向左旋转45°,并且伴随着缩小,其中旋转动画的时间比缩小动画的时间短

//缩小动画
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 1
animation.fromValue = 1
animation.toValue = 0

//旋转动画
let keyAnimation = CAKeyframeAnimation.init(keyPath: "transform.rotation.z")
keyAnimation.values = [0, CGFloat.pi * -0.25]
keyAnimation.fillMode = .forwards
keyAnimation.duration = 1 * 0.4
let groupAnimation = CAAnimationGroup()

//合成动画组
groupAnimation.duration = 1
groupAnimation.delegate = self
groupAnimation.animations = [keyAnimation, animation]
groupAnimation.fillMode = .forwards
groupAnimation.isRemovedOnCompletion = false
hollowHeartLayer.isHidden = false
redHeartLayer.add(groupAnimation, forKey: nil)

4、六条扇形

 let fanCenter = CGPoint(x: frame.width / 2, y: frame.height / 2)
 for i in 0..<6 {
     let path = UIBezierPath()
     
     //添加扇形圆弧。
     path.addArc(withCenter: fanCenter,
                 radius: frame.width / 2,
                 startAngle: CGFloat.pi / 2 + (CGFloat.pi / 3) * CGFloat(i) - (CGFloat.pi / 72),
                 endAngle: CGFloat.pi / 2 + CGFloat.pi / 3 * CGFloat(i) + CGFloat.pi / 72,
                 clockwise: true)
    
    //与中心点相连
     path.addLine(to: fanCenter)

     path.close()
     let fanLayer = CAShapeLayer()
     fanLayer.path = path.cgPath
     fanLayer.fillColor = UIColor.red.cgColor
     fanLayer.frame = bounds
     layer.addSublayer(fanLayer)
     animationLayers.append(fanLayer)
 }

勾画出六个扇形框,再设置 fillColor

5、扇形扩散动画 通过中心点位置位移到扇形圆弧位置,以达到扩散的效果

let fanAnimation = CABasicAnimation(keyPath: "position")
fanAnimation.duration = animationDuration
fanAnimation.fromValue = CGPoint(x: frame.width / 2, y: frame.height / 2)
fanAnimation.toValue = path.currentPoint
fanAnimation.isRemovedOnCompletion = false
fanAnimation.delegate = self
fanAnimation.fillMode = .both
fanLayer.add(lineAnimation, forKey: nil)

6、看着效果有问题,需要给 UIButton加上蒙层,以达到扇形图层逐渐消失的感觉,并且在动画结束的时候将扇形图层移除

//移除扇形图层
 func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
     for animationLayer in animationLayers {
        animationLayer.removeFromSuperlayer()
        animationLayer.removeAllAnimations()
    }
    animationLayers.removeAll()
 }
 
 //添加蒙层
 fileprivate func configurationMaskLayer() {
    let maskPath = UIBezierPath(roundedRect: bounds,
                                cornerRadius: frame.width / 2)
    let maskLayer = CAShapeLayer()
    maskLayer.path = maskPath.cgPath
    layer.mask = maskLayer
}

7、将爱心动画和扇形动画合在一起

下面附上 完整Demo

如果有更好的方法欢迎讨论。