我有一个" U"形状UIBezierPath
,我用path
myImage.layer
来制作动画。我也有一个scrollView。我的目标是定制一个" Pull to Refresh"动画。
我遇到的问题是我希望我的myImage.layer
根据scrollView滚动的内容进行更新。
随着scrollView被拉下,myImage.layer
动画沿着" U"形状path
。这是我在代码中path
创建的UIBezierPath
。
这就是我计算scrollView被拉下来的方式:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
这是动态更新位置的功能(它不起作用):
func redrawFromProgress(progress: CGFloat) {
// PROBLEM: This is not correct. Only the `x` position is dynamic based on scrollView position.
// The `y` position is static.
// I want this to be dynamic based on how much the scrollView scrolled.
myImage.layer.position = CGPoint(x: progress, y: 50)
}
基本上,这就是我想要的:
如果滚动的scrollView为0.0,则myImage.layer
位置应为CGPoint(x:0,y:0)或path
的起点。
如果滚动的scrollView为0.5(50%),那么myImage.layer
位置应该是path
的50%,我不知道CGPoint值会是什么在这里。
依旧......
我尝试沿UIBezierPath
获取CGPoint值,并根据滚动的scrollView的百分比,将CGPoint值分配给它但不知道如何执行此操作。我也看过这个post,但我无法让它为我工作。
编辑问题1:
通过使用此扩展程序,我能够获得一个CGPoints
数组,其中包含基于UIBezierPath
的10个值:
extension CGPath {
func forEachPoint(@noescape body: @convention(block) (CGPathElement) -> Void) {
typealias Body = @convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, Body.self)
body(element.memory)
}
// print(sizeofValue(body))
let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
CGPathApply(self, unsafeBody, callback)
}
func getPathElementsPoints() -> [CGPoint] {
var arrayPoints : [CGPoint]! = [CGPoint]()
self.forEachPoint { element in
switch (element.type) {
case CGPathElementType.MoveToPoint:
arrayPoints.append(element.points[0])
case .AddLineToPoint:
arrayPoints.append(element.points[0])
case .AddQuadCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
case .AddCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
arrayPoints.append(element.points[2])
default: break
}
}
return arrayPoints
}
我还将上面称为redrawFromProgress(progress: CGFloat)
的函数重写为:
func redrawFromProgress(progress: CGFloat) {
let enterPath = paths[0]
let pathPointsArray = enterPath.CGPath
let junctionPoints = pathPointsArray.getPathElementsPoints()
// print(junctionPoints.count) // There are 10 junctionPoints
// progress means how much the scrollView has been pulled down,
// it goes from 0.0 to 1.0.
if progress <= 0.1 {
myImage.layer.position = junctionPoints[0]
} else if progress > 0.1 && progress <= 0.2 {
myImage.layer.position = junctionPoints[1]
} else if progress > 0.2 && progress <= 0.3 {
myImage.layer.position = junctionPoints[2]
} else if progress > 0.3 && progress <= 0.4 {
myImage.layer.position = junctionPoints[3]
} else if progress > 0.4 && progress <= 0.5 {
myImage.layer.position = junctionPoints[4]
} else if progress > 0.5 && progress <= 0.6 {
myImage.layer.position = junctionPoints[5]
} else if progress > 0.6 && progress <= 0.7 {
myImage.layer.position = junctionPoints[6]
} else if progress > 0.7 && progress <= 0.8 {
myImage.layer.position = junctionPoints[7]
} else if progress > 0.8 && progress <= 0.9 {
myImage.layer.position = junctionPoints[8]
} else if progress > 0.9 && progress <= 1.0 {
myImage.layer.position = junctionPoints[9]
}
}
如果我向下拉动scrollView非常慢,myImage.layer
实际上遵循路径。唯一的问题是,如果我非常快地向下拉动scrollView,那么myImage.layer
会跳到最后一点。可能是因为我写上面if statement
的方式?
有什么想法吗?
答案 0 :(得分:2)
感谢@Sam Falconer让我意识到这一点:
您的代码依赖于scrollViewDidScroll委托回调来经常调用以命中所有关键帧点。当你快速拉动滚动视图时,它不会经常调用该方法,导致跳转。
一旦我确认了这一点,他也提到了帮助:
此外,您会发现CAKeyframeAnimation类很有用。
使用CAKeyfraneAnimation
我可以使用以下代码手动控制它的值:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
func redrawFromProgress(progress: CGFloat) {
// Animate image along enter path
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = myPath.CGPath
pathAnimation.calculationMode = kCAAnimationPaced
pathAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
pathAnimation.beginTime = 1e-100
pathAnimation.duration = 1.0
pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
pathAnimation.removedOnCompletion = false
pathAnimation.fillMode = kCAFillModeForwards
imageLayer.addAnimation(pathAnimation, forKey: nil)
imageLayer.position = enterPath.currentPoint
}
再次感谢帮助人员!
答案 1 :(得分:1)
您的代码依赖scrollViewDidScroll
委托回调来经常调用以达到所有关键帧点。当你快速拉动滚动视图时,它不会经常调用该方法,导致跳转。
您可能希望尝试根据表示当前位置与所需位置之间路径的弧段计算自定义路径。在此基础上设置动画,而不是解构自定义路径(看起来非常接近于弧形)可能更容易。
x,y和r保持不变的 CGPathAddArc()
应该让你现在的路径达到90%。您也可以通过添加该路径段的路径来获得更好的路径。只需要做一些工作就可以让所有“我正处于这个位置,让我走向另一个位置”逻辑的部分路径正确。
此外,您会发现CAKeyframeAnimation
类很有用。您可以为它提供一个CGPath(可能是一个基于要移动的弧段),以及动画的时间,它可以使您的图层遵循路径。
以下是如何在CGPath上从当前进度到新进度绘制部分弧的示例代码。我也反过来工作了。您可以使用数字和常量,但这是如何将弧线段从特定百分比绘制到特定百分比的想法。
在查看CoreGraphics数学时,请记住它可能看起来倒退(顺时针与逆时针等)。这是因为UIKit将所有内容颠倒过来将原点放在 upper -left中,其中CG的起源位于 lower -left。
// start out with start percent at zero, but then use the last endPercent instead
let startPercent = CGFloat(0.0)
// end percent is the "progress" in your code
let endPercent = CGFloat(1.0)
// reverse the direction of the path if going backwards
let clockwise = startPercent > endPercent ? false : true
let minArc = CGFloat(M_PI) * 4/5
let maxArc = CGFloat(M_PI) * 1/5
let arcLength = minArc - maxArc
let beginArc = minArc - (arcLength * startPercent)
let endArc = maxArc + (arcLength * (1.0 - endPercent))
let myPath = CGPathCreateMutable()
CGPathAddArc(myPath, nil, view.bounds.width/2, 0, 160, beginArc, endArc, clockwise)
以下是常量minArc
和maxArc
定义的完整弧段。