根据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)

}

基本上,这就是我想要的:

我尝试获取沿 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(也许是一个基于要移动的弧段)和动画的时间,它可以让你的图层跟随路径。

来源:https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGPath/index.html#//apple_ref/c/func/CGPathAddArc

来源:https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAKeyframeAnimation_class/index.html

编辑:

下面是一些示例代码,说明如何在 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)

这是由常量 minArcmaxArc 定义的完整弧段。

感谢@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
}

再次感谢大家的帮助!