问题
I have a "U" shaped UIBezierPath
which I use as the path
for my myImage.layer
to animate on. I also have a scrollView. My goal is to have a custom "Pull to Refresh" animation.
The problem I am having is that I want my myImage.layer
to update based on how much the scrollView scrolled.
As the scrollView is pulled down, the myImage.layer
animates along a "U" shape path
. This is the path
in my code which I created as a UIBezierPath
.
This is how I calculate how far the scrollView is pulled down:
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)
}
}
This is the function to dynamically update the position (it is not working):
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)
}
Basically, this is what I want:
If the scrollView scrolled is 0.0, then the
myImage.layer
position should be CGPoint(x: 0, y: 0) or the starting point of thepath
.If the scrollView scrolled is 0.5 (50%), then the
myImage.layer
position should be at 50% of thepath
, I don't know what the CGPoint value would be here.and so on...
I tried getting the CGPoint values along the UIBezierPath
and based on the % of the scrollView scrolled, assign that CGPoint value to it but don't know how to do this. I also looked at this post but I can't get it to work for me.
EDIT QUESTION 1:
By using this extension, I was able to get an array of CGPoints
which contain 10 values based on my 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
}
I also rewrote the function above called redrawFromProgress(progress: CGFloat)
to this:
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]
}
}
If I pull down the scrollView very slow, the myImage.layer
actually follows the path. The only problem is that if I pull down on the scrollView very fast, then the myImage.layer
jumps to the last point. Could it be because of the way I wrote the if statement
above?
Any ideas?
回答1:
Thanks to @Sam Falconer for making me aware of this:
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.
Once I confirmed this, he also helped by mentioning:
Additionally, you will find the CAKeyframeAnimation class to be useful.
With CAKeyfraneAnimation
I am able to manually control it's value with this code:
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
}
Thanks again for the help guys!
回答2:
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.
You may want to try calculating a custom path based on a segment of an arc representing the path between your current position, and your desired position. Basing an animation on this, instead of deconstructing your custom path (which looks very close to just being an arc), may be easier.
CGPathAddArc()
with x, y, and r being constant, should get you 90% to what your path is now. You could also get fancier with the path to add that line segment like you have at the beginning of your path. It would just take a bit more work to get the partial path to come out right for all the "I'm at this position, get me a path to this other position" logic.
Additionally, you will find the CAKeyframeAnimation
class to be useful. You can feed it a CGPath (perhaps one based on the arc segment to travel), and the timing for the animation, and it can make your layer follow the path.
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGPath/index.html#//apple_ref/c/func/CGPathAddArc
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAKeyframeAnimation_class/index.html
Edit:
Here is some example code for how to draw a partial arc on a CGPath from the current progress to the new progress. I made it work in reverse too. You can play with the numbers and constants, but this is the idea of how to draw an arc segment from a certain percentage to a certain percentage.
Please keep in mind when looking at the CoreGraphics math that it may seem backwards (clockwise vs counterclockwise, etc). This is because UIKit flips everything upside down to put the origin in the upper-left, where CG has its origin in the 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)
Here is the full arc segment as defined by the constants minArc
and maxArc
.
来源:https://stackoverflow.com/questions/36705072/dynamically-change-position-based-on-scrollview