基于scrollView动态改变位置

Posted

技术标签:

【中文标题】基于scrollView动态改变位置【英文标题】:Dynamically change position based on scrollView 【发布时间】:2016-04-18 22:03:03 【问题描述】:

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

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

当下拉滚动视图时,myImage.layer 沿“U”形 path 动画。这是我创建为UIBezierPath 的代码中的path

这就是我计算滚动视图被拉下多远的方法:

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的起点。

1234563 >

等等……

我尝试沿 UIBezierPath 获取 CGPoint 值,并根据滚动视图的百分比,将 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]

    


如果我非常缓慢地拉下滚动视图,myImage.layer 实际上会跟随路径。唯一的问题是,如果我非常快地下拉滚动视图,那么myImage.layer 会跳到最后一点。会不会是因为我写上面if statement的方式?

有什么想法吗?

【问题讨论】:

我认为这个 SO 问题可能会有所帮助:***.com/questions/26112497/…(如果您能以特定百分比生成该点,您的问题将得到解决) @Cheng-YuHsu 感谢您的回复!我无法按照您发送的链接进行操作。你能帮忙实现 Swift 或者其他链接吗? 【参考方案1】:

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

再次感谢大家的帮助!

【讨论】:

【参考方案2】:

您的代码依赖于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 将所有内容颠倒过来,将原点放在 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 定义的完整弧段。

【讨论】:

看起来你对 scrollViewDidScroll 调用不够的权利,我打印了我创建的 progress 变量,它从 0.0 跳到 0.55,然后跳到 0.90,跳过了中间值。感谢那!我知道如何使用CAKeyFrameAnimation 执行此操作,但不结合基于用户在滚动视图上下拉的动态值。您能否通过编写一些代码或链接到答案/博客文章来提供帮助?在这一点上我已经尝试了很多东西。 我知道如何在scrollViewDidScroll 之外使用CAKeyframeAnimation,但这是否会导致当您拉动滚动视图时spaceShipImage.layer 向前移动而当您松开时spaceShipImage.layer 向后移动? 如果您向endPercent 输入的百分比低于startPercent,则路径应该反向绘制,这会导致您的动画倒退。 有了你的回答,它帮助我找到了如何使用我创建的自定义路径来解决这个问题。我在这里添加了我的答案,并提到了您提供的所有帮助并引用了有用的内容。谢谢!! 很高兴您能够让它与自定义路径一起使用,并且我可以提供帮助。您觉得我的回答很有帮助,您介意点赞吗?

以上是关于基于scrollView动态改变位置的主要内容,如果未能解决你的问题,请参考以下文章

C# wpf datagrid 动态加载数据后改变单元格颜色bug

ugui学习——可动态改变Content大小的ScrollView

UITapGestureRecognizer 正在覆盖 Scrollview 上的 UIButton 操作

如何确定垂直 ScrollView 的方向?

将选择器添加到动态添加的 UIScrollView 视图上的按钮

ScrollView与ListView嵌套使用,导致ListView下拉失效