liquid-swipe是一个翻页效果,最近在git trending榜上排名很高,所以笔者就下下来看一下
翻页中,前后页会沿着一个曲线显示,这是用了layer.mask属性 + CAShapeLayer 来实现。
-
曲线的绘制
无论是刚开始盖住按钮的小圆泡,还是手动翻页没松手时跟随手指的大圆泡,还是后面松手后回弹的反向曲线,都是一个根据宽、高来按比例计算的一个类似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)
图形大概是这样的
这个函数之前见过,应该是某种场景下经常需要用到的函数,但是想不起来了。看到这里不得不感叹,作者的数学不错呀,如果是我,我肯定不知道要用这个函数
- waveVertRadius和sideWidth的计算
这个计算就比较简单,都是线性运算,直接看源码一目了然,这里就不抄录了
网友评论