基于scrollView动态更改位置

时间:2016-04-18 22:03:04

标签: ios objective-c swift core-graphics paint-code

我有一个" 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的起点。

    < / LI>
  • 如果滚动的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的方式?

有什么想法吗?

2 个答案:

答案 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(可能是一个基于要移动的弧段),以及动画的时间,它可以使您的图层遵循路径。

来源: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定义的完整弧段。

Full Arc Segment