根据scrollView动态改变位置
Dynamically change position based on scrollView
我有一个 "U" 形状的 UIBezierPath
,我将其用作 path
以便我的 myImage.layer
进行动画处理。我也有一个滚动视图。我的目标是制作自定义 "Pull to Refresh" 动画。
我遇到的问题是我希望 myImage.layer
根据滚动视图的滚动量进行更新。
当下拉 scrollView 时,myImage.layer
沿着 "U" 形状 path
进行动画处理。这是我创建的 UIBezierPath
代码中的 path
。
这是我计算 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
的写法问题?
有什么想法吗?
您的代码依赖于 scrollViewDidScroll
委托回调的调用频率足以达到您所有的关键帧点。当您在滚动视图上快速拉动时,它不会足够频繁地调用该方法,从而导致跳转。
您可能想尝试根据代表您当前位置和所需位置之间路径的一段弧计算自定义路径。以此为基础制作动画,而不是解构您的自定义路径(看起来非常接近于只是一条弧线),可能会更容易。
CGPathAddArc()
x、y 和 r 保持不变,应该让你 90% 达到现在的路径。您还可以像在路径开头那样添加该线段的路径。只需要做更多的工作才能使部分路径适合所有 "I'm at this position, get me a path to this other position" 逻辑。
此外,您会发现 CAKeyframeAnimation
class 很有用。你可以给它一个 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
定义的完整弧段。
感谢@Sam Falconer 让我意识到这一点:
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
我确认后,他还帮忙提到:
Additionally, you will find the CAKeyframeAnimation class to be useful.
使用 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
}
再次感谢大家的帮助!
我有一个 "U" 形状的 UIBezierPath
,我将其用作 path
以便我的 myImage.layer
进行动画处理。我也有一个滚动视图。我的目标是制作自定义 "Pull to Refresh" 动画。
我遇到的问题是我希望 myImage.layer
根据滚动视图的滚动量进行更新。
当下拉 scrollView 时,myImage.layer
沿着 "U" 形状 path
进行动画处理。这是我创建的 UIBezierPath
代码中的 path
。
这是我计算 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
:
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
的写法问题?
有什么想法吗?
您的代码依赖于 scrollViewDidScroll
委托回调的调用频率足以达到您所有的关键帧点。当您在滚动视图上快速拉动时,它不会足够频繁地调用该方法,从而导致跳转。
您可能想尝试根据代表您当前位置和所需位置之间路径的一段弧计算自定义路径。以此为基础制作动画,而不是解构您的自定义路径(看起来非常接近于只是一条弧线),可能会更容易。
CGPathAddArc()
x、y 和 r 保持不变,应该让你 90% 达到现在的路径。您还可以像在路径开头那样添加该线段的路径。只需要做更多的工作才能使部分路径适合所有 "I'm at this position, get me a path to this other position" 逻辑。
此外,您会发现 CAKeyframeAnimation
class 很有用。你可以给它一个 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
定义的完整弧段。
感谢@Sam Falconer 让我意识到这一点:
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
我确认后,他还帮忙提到:
Additionally, you will find the CAKeyframeAnimation class to be useful.
使用 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
}
再次感谢大家的帮助!