2

我有一个“U”形,我UIBezierPath用它来制作动画。我也有一个滚动视图。我的目标是制作一个自定义的“拉动刷新”动画。pathmyImage.layer

我遇到的问题是我希望我myImage.layer根据滚动视图滚动的程度进行更新。

随着滚动视图被拉下,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位置应该是50% path,我不知道CGPoint 值会是什么。

  • 等等...

我尝试UIBezierPath根据滚动视图的百分比获取 CGPoint 值,将 CGPoint 值分配给它,但不知道如何执行此操作。我也看过这篇文章,但我无法让它为我工作。

编辑问题1:

通过使用这个扩展,我能够得到一个数组,CGPoints其中包含基于我的 10 个值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]

    }

}

如果我很慢地拉下滚动视图,myImage.layer实际上会遵循路径。唯一的问题是,如果我非常快速地下拉滚动视图,那么会myImage.layer跳转到最后一点。可能是因为我写if statement上面的方式吗?

有任何想法吗?

4

2 回答 2

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
}

再次感谢各位的帮助!

于 2016-04-19T22:59:41.287 回答
1

您的代码依赖于scrollViewDidScroll足够频繁地调用委托回调以达到所有关键帧点。当您快速拉动滚动视图时,它不会足够频繁地调用该方法,从而导致跳转。

您可能想尝试根据表示当前位置和所需位置之间的路径的弧段计算自定义路径。在此基础上制作动画,而不是解构您的自定义路径(看起来非常接近只是一条弧线),可能会更容易。

CGPathAddArc()在 x、y 和 r 保持不变的情况下,应该让你 90% 达到你现在的路径。您还可以像在路径开头那样添加该线段的路径。只需要更多的工作才能让部分路径正确地出现在所有“我在这个位置,让我有一条通往另一个位置的路径”的逻辑。

此外,您会发现该CAKeyframeAnimation课程很有用。你可以给它一个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 将所有内容颠倒过来,将原点放在左上角,而 CG 的原点在左下角

// 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

全弧段

于 2016-04-19T02:12:14.487 回答