美文网首页
核心动画系列(三): 实现 SwipeRefreshLayout

核心动画系列(三): 实现 SwipeRefreshLayout

作者: Lin__Chuan | 来源:发表于2019-01-21 21:01 被阅读294次

    在这篇文章里我们来实现一个 Android 里官方提供的一个下拉刷新控件 SwipeRefreshLayout.

    整体效果如下:


    iOS版 SwipeRefreshLayout

    要实现这样的一个效果, 有以下几个问题需要解决.
    动画部分

    • 三角和圆弧线是怎么绘制的? 怎么控制它的运动轨迹?
    • 这种三段式的圆弧线, 它的淡入淡出的动画效果是怎么实现的.

    刷新部分

    • 不通过ScrollView 的滚动偏移值, 如何在 ScrollView 的顶部触发刷新操作?

    动画部分

    整个动画部分我们都是通过 CAShapeLayer 实现的.

    CALayer 的子类 CAShapeLayer 是一个通过矢量图形而不是 bitmap 来绘制的图层类. 绘制路径即可以用 CAShapeLayer, 也可以用 Core Graphics 直接向原始的 CALayer 的内容中绘制一个路径. 相比之下, 使用 CAShapeLayer 有以下一些优点:

    • 渲染快速. CAShapeLayer 使用了硬件加速, 绘制同一图形会比用 Core Graphics 快很多.
    • 高效使用内存. 一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形, 所以无论有多大, 都不会占用太多的内存.
    • 不会被图层边界剪裁掉. 一个 CAShapeLayer 可以在边界之外绘制. 你的图层路径不会像在使用 Core Graphics 的普通 CALayer 一样被剪裁掉.
    • 不会出现像素化. 当你把 CAShapeLayer 放大, 或是用3D透视变换将其离相机更近时, 它不像一个有寄宿图的普通图层一样变得像素化.

    下面画一个火柴人演示 CAShapeLayer 的使用:

    火柴人演示

    代码实现

    //创建路径
    let path = UIBezierPath()
        
    // 定义起点
    path.move(to: CGPoint(x: 175, y: 100))
        
    // 绘制圆
    path.addArc(withCenter: CGPoint(x: 150, y: 100), radius: 25, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        
    // 定义起点
    path.move(to: CGPoint(x: 150, y: 125))
    // 绘制直线
    path.addLine(to: CGPoint(x: 150, y: 175))
    path.addLine(to: CGPoint(x: 125, y: 225))
        
    path.move(to: CGPoint(x: 150, y: 175))
    path.addLine(to: CGPoint(x: 175, y: 225))
        
    path.move(to: CGPoint(x: 100, y: 150))
    path.addLine(to: CGPoint(x: 200, y: 150))
        
    //创建shape layer
    let shapeLayer = CAShapeLayer()
    shapeLayer.strokeColor = UIColor.red.cgColor  // 路径颜色
    shapeLayer.fillColor = UIColor.clear.cgColor  // 封闭区间填充颜色
    shapeLayer.lineWidth = 5                      // 线条宽度
    shapeLayer.lineJoin = .round                  // 线条终点样式
    shapeLayer.lineCap = .round                   // 线条拐点样式
    shapeLayer.path = path.cgPath
    //添加layer
    view.layer.addSublayer(shapeLayer)
    
    1. 如何通过 shape layer 动态画圆?
    let pathLayer = CAShapeLayer()
    pathLayer.strokeStart = 0
    pathLayer.strokeEnd = 10
    pathLayer.fillColor = nil
    pathLayer.lineWidth = 2.5
        
    // 绘制圆形
    let path = UIBezierPath(arcCenter: CGPoint(x: 20, y: 20), radius: 9, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
    pathLayer.path = path.cgPath
    pathLayer.lineCap = .square
    view.layer.addSublayer(pathLayer)
    

    通过改变 strokeStart / strokeEnd 可以实现动态画圆.


    2. 如何绘制一个三角型

    还是通过贝塞尔线来绘制路径

    let arrowLayer = CAShapeLayer()
    arrowLayer.lineWidth = 10
    arrowLayer.strokeColor = UIColor.blue.cgColor
        
    arrowLayer.transform = CATransform3DMakeTranslation(100, 100, 0)
        
    // 目标点
    let points = [CGPoint(x: 0, y: 1),
                  CGPoint(x: 1, y: 0),
                  CGPoint(x: 0, y: -1)]
        
    // 旋转90度
    let transform: CGAffineTransform = CGAffineTransform(rotationAngle: .pi / 2)
        
    let cgPath = CGMutablePath()
    cgPath.addLines(between: points, transform: transform)
    cgPath.closeSubpath()![image02.png](https://img.haomeiwen.com/i1208639/3dca87508db1fa8f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    arrowLayer.path = cgPath
    view.layer.addSublayer(arrowLayer)
    

    效果如下


    通过旋转父视图, 结合三角, 圆的路径, 就很容易实现下面这种效果.

    3. 这种三段式的动画怎么实现的.

    主要是利用基础动画的组合动画实现的.

    let beginHeadAnimation = CABasicAnimation()
    beginHeadAnimation.keyPath = "strokeStart"
    beginHeadAnimation.duration = 0.5
    beginHeadAnimation.fromValue = 0.25
    beginHeadAnimation.toValue = 1.0
    beginHeadAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        
    let beginTailAnimation = CABasicAnimation()
    beginTailAnimation.keyPath = "strokeEnd"
    beginTailAnimation.duration = 0.5
    beginTailAnimation.fromValue = 1.0
    beginTailAnimation.toValue = 1.0
    beginTailAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        
    let endHeadAnimation = CABasicAnimation()
    endHeadAnimation.keyPath = "strokeStart"
    endHeadAnimation.beginTime = 0.5
    endHeadAnimation.duration = 1.0
    endHeadAnimation.fromValue = 0.0
    endHeadAnimation.toValue = 0.25
    endHeadAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
    let endTailAnimation = CABasicAnimation()
    endTailAnimation.keyPath = "strokeEnd"
    endTailAnimation.beginTime = 0.5
    endTailAnimation.duration = 1.0
    endTailAnimation.fromValue = 0.0
    endTailAnimation.toValue = 1.0
    endTailAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        
    let animations = CAAnimationGroup()
    animations.duration = 1.5
    animations.isRemovedOnCompletion = false
    animations.animations = [beginHeadAnimation, beginTailAnimation, endHeadAnimation, endTailAnimation]
    animations.repeatCount = .infinity
    pathLayer.add(animations, forKey: "stroke_animation")
    
    let timer = Timer(timeInterval: 0.5, target: self, selector: #selector(changeColor), userInfo: nil, repeats: false)
    RunLoop.current.add(timer, forMode: .common)
    
    @objc func changeColor() {    
        // 变化颜色
        colorIndex += 1
        if colorIndex > colors.count - 1 {
            colorIndex = 0
        }
        
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        pathLayer.strokeColor = colors[colorIndex]
        CATransaction.commit()
        
        let timer = Timer(timeInterval: 1.5, target: self, selector: #selector(changeColor), userInfo: nil, repeats: false)
        RunLoop.current.add(timer, forMode: .common)
    }
    

    效果如下


    刷新部分

    一般的刷新框架都是采用的 KVO 或者代理获取 ScrollView 的纵向偏移值来显示动画的. 但是 SwipeRefreshLayout 刷新框架并不是这样的, 我猜测应该是使用手势, 通过 pan 手势存储滑动的纵向值来达到 ScrollView 类似的效果. 这种也是可行的.

    参考

    iOS UIBezierPath贝塞尔曲线常用方法

    相关文章

      网友评论

          本文标题:核心动画系列(三): 实现 SwipeRefreshLayout

          本文链接:https://www.haomeiwen.com/subject/qzuxjqtx.html