I've got a scenario I'm trying to implement in my app where I'd like to make a UIScrollView
behave more 'stiff' once you start forcing it to scroll past its normal content bounds.
What I mean by this, is when you're either at the top or bottom of a scroll view, if you tap down and keep dragging, you can usually get the scroll view to keep scroling beyond its bounds, but it gradually builds up resistance, until it stops usually about half way in the middle of the view's bounds. When you lift your finger, it snaps back to the bounds of the scroll region.
What I'm trying to achieve, is I'd like to make that 'out-of-bounds' dragging effect a lot more heavy, so instead of the user dragging the scroll view and it 'bottoming-out' mid way through the scroll view bounds, it completely stops around 20% or so past its scrolling bounds instead.
I've been experimenting with overriding the scroll view's contentOffset
inside the scrollViewDidScroll:
delegate method, but that doesn't seem to work since re-setting the contentOffset in there seems to screw up further delegate calls of the same method.
My next idea was to monitor the UIPanGestureRecognizer
associated with the scrollview and try and determine the proper UIScrollView
contentOffset based off events coming out of that. That being said, I thought that might start getting on the hacky side, so I thought I'd ask here for any other solutions I hadn't considered before I try something that could potentially be messy.
Thanks!
I am spiking on the following code in Swift. This is for horizontal scrolling and can easily be adapted to vertical one. Solutions differ depending on whether paging is enabled or not. Both are given below.
class ScrollViewDelegate : NSObject, UIScrollViewDelegate
{
let maxOffset: CGFloat // offset of the rightmost content (scrollview contentSize.width - frame.width)
var prevOffset: CGFloat = 0 // previous offset (after adjusting the value)
var totalDistance: CGFloat = 0 // total distance it would have moved (had we not restricted)
let reductionFactor: CGFloat = 0.2 // percent of total distance it will be allowed to move (under restriction)
let scaleFactor: CGFloat = UIScreen.mainScreen().scale // pixels per point, for smooth translation in respective devices
init(maxOffset: CGFloat)
{
self.maxOffset = maxOffset // scrollView.contentSize.width - scrollView.frame.size.width
}
func scrollViewDidScroll(scrollView: UIScrollView)
{
let flipped = scrollView.contentOffset.x >= maxOffset // dealing with left edge or right edge rubber band
let currentOffset = flipped ? maxOffset - scrollView.contentOffset.x : scrollView.contentOffset.x // for right edge, flip the values as if screen is folded in half towards the left
if(currentOffset <= 0) // if dragging/moving beyond the edge
{
if(currentOffset <= prevOffset) // if dragging/moving beyond previous offset
{
totalDistance += currentOffset - prevOffset // add the "proposed delta" move to total distance
prevOffset = round(scaleFactor * totalDistance * reductionFactor) / scaleFactor // set the prevOffset to fraction of total distance
scrollView.contentOffset.x = flipped ? maxOffset - prevOffset : prevOffset // set the target offset, after negating any flipping
}
else // if dragging/moving is reversed, though still beyond the edge
{
totalDistance = currentOffset / reductionFactor // set totalDistance from offset (reverse of prevOffset calculation above)
prevOffset = currentOffset // set prevOffset
}
}
else // if dragging/moving inside the edge
{
totalDistance = 0 // reset the values
prevOffset = 0
}
}
}
When paging is enabled, the bounce back to the resting point doesn't seem to work as expected. Instead of stopping at the page boundary, the rubber band is overshooting it and halting at a non-page offset. This happens if the pull from the edge is with a fast flick such that it continues to move in that direction even after you lift the finger, before reversing the direction and coming back to the resting point. It seems to work fine if you simply pause and leave or even flick it back towards the resting point. To address this, in the below code I try to identify the possibility of overshooting and forcefully stop it while it returns and attempts to cross the expected page boundary.
class PageScrollViewDelegate : NSObject, UIScrollViewDelegate
{
let maxOffset: CGFloat // offset of the rightmost content (scrollview contentSize.width - frame.width)
var prevOffset: CGFloat = 0 // previous offset (after adjusting the value)
var totalDistance: CGFloat = 0 // total distance it would have moved (had we not restricted)
let reductionFactor: CGFloat = 0.2 // percent of total distance it will be allowed to move (under restriction)
let scaleFactor: CGFloat = UIScreen.mainScreen().scale // pixels per point, for smooth translation in respective devices
var draggingOver: Bool = false // finger dragging is over or not
var overshoot: Bool = false // is there a chance for page to overshoot page boundary while falling back
init(maxOffset: CGFloat)
{
self.maxOffset = maxOffset // scrollView.contentSize.width - scrollView.frame.size.width
}
func scrollViewWillBeginDragging(scrollView: UIScrollView)
{
draggingOver = false // reset the flags
overshoot = false
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
draggingOver = true // finger dragging is over
}
func scrollViewDidScroll(scrollView: UIScrollView)
{
let flipped = scrollView.contentOffset.x >= 0.5 * maxOffset // dealing with left edge or right edge rubber band
let currentOffset = flipped ? maxOffset - scrollView.contentOffset.x : scrollView.contentOffset.x // for right edge, flip the values as if screen is folded in half towards the left
if(currentOffset <= 0) // if dragging/moving beyond the edge
{
if(currentOffset <= prevOffset) // if dragging/moving beyond previous offset
{
overshoot = draggingOver // is content moving farther away even after dragging is over (caused by fast flick, which can cause overshooting page boundary while falling back)
totalDistance += currentOffset - prevOffset // add the "proposed delta" move to total distance
prevOffset = round(scaleFactor * totalDistance * reductionFactor) / scaleFactor // set the prevOffset to fraction of total distance
scrollView.contentOffset.x = flipped ? maxOffset - prevOffset : prevOffset // set the target offset, after negating any flipping
}
else // if dragging/moving is reversed, though still beyond the edge
{
totalDistance = currentOffset / reductionFactor // set totalDistance from offset (reverse of prevOffset calculation above)
prevOffset = currentOffset // set prevOffset
}
}
else // if dragging/moving inside the edge
{
if(overshoot) // if this movement is a result of overshooting
{
scrollView.setContentOffset(CGPointMake(flipped ? maxOffset : 0, scrollView.contentOffset.y), animated: false) // bring it to resting point and stop further scrolling (this is a patch to control overshooting)
}
totalDistance = 0 // reset the values
prevOffset = 0
}
}
}
This isn't going to be easy.
If this is just a one-off scroll view, I might suggest using UIKit Dynamics with some push, attachment, and spring behaviors to get exactly the effect you want.
If you're looking to do this for every table view in your app I think watching the pan gesture recognizer is a reasonable enough approach. Just off the top of my head I would observe the gesture's state, when it ends I would capture the vertical velocity of the view, use the UIScrollView
method to calculate it's stopping position and then animate it from its current position to its resting position with a spring animation. You'll have to calculate the duration yourself using the ending velocity of the pan and the remaining distance + the overshoot amount.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With