源码阅读计划-liquid-swipe

liquid-swipe是一个翻页效果,最近在git trending榜上排名很高,所以笔者就下下来看一下

翻页中,前后页会沿着一个曲线显示,这是用了layer.mask属性 + CAShapeLayer 来实现。

  • 曲线的绘制

圆泡示意图.jpg

无论是刚开始盖住按钮的小圆泡,还是手动翻页没松手时跟随手指的大圆泡,还是后面松手后回弹的反向曲线,都是一个根据宽、高来按比例计算的一个类似sin(x)函数(0, π)段的曲线。

//圆泡绘制需要的四个参数
internal class WaveLayer: CAShapeLayer {
    var waveCenterY: CGFloat
    var waveHorRadius: CGFloat
    var waveVertRadius: CGFloat
    var sideWidth: CGFloat
}
wave参数说明.jpg

曲线的生成是用数字来实现的,感觉像是用贝塞尔模仿sin函数的(0, ∏)段,不知道具体是啥曲线

        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.1561501458,
                                  y: curveStartY - waveVertRadius * 0.3322374268),
                      control1: CGPoint(x: maskWidth,
                                        y: curveStartY - waveVertRadius * 0.1346194756),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.05341339583,
                                        y: curveStartY - waveVertRadius * 0.2412779634))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.5012484792,
                                  y: curveStartY - waveVertRadius * 0.5350576951),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.2361659167,
                                        y: curveStartY - waveVertRadius * 0.4030805244),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.3305285625,
                                        y: curveStartY - waveVertRadius * 0.4561193293))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.574934875,
                                  y: curveStartY - waveVertRadius * 0.5689655122),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.515878125,
                                        y: curveStartY - waveVertRadius * 0.5418222317),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.5664134792,
                                        y: curveStartY - waveVertRadius * 0.5650349878))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.8774032292,
                                  y: curveStartY - waveVertRadius * 0.7399037439),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.7283715208,
                                        y: curveStartY - waveVertRadius * 0.6397387195),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.8086618958,
                                        y: curveStartY - waveVertRadius * 0.6833456585))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius, y: curveStartY - waveVertRadius),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.9653464583,
                                        y: curveStartY - waveVertRadius * 0.8122605122),
                      control2: CGPoint(x: maskWidth - waveHorRadius,
                                        y: curveStartY - waveVertRadius * 0.8936183659))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.8608411667,
                                  y: curveStartY - waveVertRadius * 1.270484439),
                      control1: CGPoint(x: maskWidth - waveHorRadius,
                                        y: curveStartY - waveVertRadius * 1.100142878),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.9595746667,
                                        y: curveStartY - waveVertRadius * 1.1887991951))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.5291125625,
                                  y: curveStartY - waveVertRadius * 1.4665102805),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.7852123333,
                                        y: curveStartY - waveVertRadius * 1.3330544756),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.703382125,
                                        y: curveStartY - waveVertRadius * 1.3795848049))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.5015305417,
                                  y: curveStartY - waveVertRadius * 1.4802616098),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.5241858333,
                                        y: curveStartY - waveVertRadius * 1.4689677195),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.505739125,
                                        y: curveStartY - waveVertRadius * 1.4781625854))
        path.addCurve(to: CGPoint(x: maskWidth - waveHorRadius * 0.1541165417,
                                  y: curveStartY - waveVertRadius * 1.687403),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.3187486042,
                                        y: curveStartY - waveVertRadius * 1.5714239024),
                      control2: CGPoint(x: maskWidth - waveHorRadius * 0.2332057083,
                                        y: curveStartY - waveVertRadius * 1.6204116463))
        path.addCurve(to: CGPoint(x: maskWidth, y: curveStartY - waveVertRadius * 2),
                      control1: CGPoint(x: maskWidth - waveHorRadius * 0.0509933125,
                                        y: curveStartY - waveVertRadius * 1.774752061),
                      control2: CGPoint(x: maskWidth, y: curveStartY - waveVertRadius * 1.8709256829))
  • 右滑动画

动画分成两段,松手前和松手后。

松手前是根据手势x方向的滑动距离来计算出progress,进而使用progress计算出waveHorRadius和waveVertRadius。所以调整maxChange,可以看到小圆泡和手指分离的不同情况

松手时如果手指滑动距离超出屏幕1/3,就会继续翻页动画,否则反弹回去。作者用shouldFinish和shouldCancel来标记是否继续翻页动画。看起来一个标识位就够了,不知道为啥要用两个标志位

松手后则用时间来确定progress

let change = -gesture.translation(in: view).x
let maxChange: CGFloat = self.view.bounds.width * (1.0/0.45) // 手势移动距离过整个屏幕宽的progress为0.45
if !(self.shouldFinish || self.shouldCancel) {
    let progress: CGFloat = min(1.0, max(0, change / maxChange))
    self.animate(view: view, forProgress: progress, waveCenterY: centerY)
    switch gesture.state {
    case .began, .changed:
        return true
    default:
        if progress >= 0.15 {
            // 0.15 / 0.45 = 1/3,手指x方向移动距离超过屏幕1/3,就会继续翻页
            self.shouldFinish = true
            self.shouldCancel = false
            // 因为松手后要根据时间来调整动画进度,所以需要把animationStartTime往前拨一些,以保证松手前后进度的连续性
            self.animationStartTime = CACurrentMediaTime() - CFTimeInterval(CGFloat(self.duration) * progress)
        } else {
            self.shouldFinish = false
            self.shouldCancel = true
            self.animationProgress = progress
            self.animationStartTime = CACurrentMediaTime()
        }
    }
}
  • 使用progress计算圆泡参数

    • waveHorRadius的计算

    waveHorRadius的计算按照progress分成两部分,0.4之前和0.4之后。0.4之前的计算公式是

    waveHorRadius = initialHorRadius + progress/p1*initialHorRadius // initialHorRadius = 48
    

    0.4之后的计算公式是

     let t: CGFloat = (progress - p1)/(1.0 - p1)
     let A: CGFloat = maxHorRadius
     let r: CGFloat = 40
     let m: CGFloat = 9.8
     let beta: CGFloat = r/(2*m)
     let k: CGFloat = 50
     let omega0: CGFloat = k/m
     let omega: CGFloat = pow(-pow(beta,2)+pow(omega0,2), 0.5)
     
     waveHorRadius = A * exp(-beta * t) * cos( omega * t)
    

    换成数学语言就是 screenwidth * 0.8 * exp(-2 * t) * cos(4.7 * t)

    图形大概是这样的

wavehor计算函数.jpg

这个函数之前见过,应该是某种场景下经常需要用到的函数,但是想不起来了。看到这里不得不感叹,作者的数学不错呀,如果是我,我肯定不知道要用这个函数

  • waveVertRadius和sideWidth的计算

这个计算就比较简单,都是线性运算,直接看源码一目了然,这里就不抄录了

你可能感兴趣的:(源码阅读计划-liquid-swipe)