Is there a way to deactivate the decelerating of a UIScrollView?
I want to allow the user to scroll the canvas, but I don't want that the canvas continues scrolling after the user lifted the finger.
This can be done by utilizing the UIScrollView delegate method scrollViewWillBeginDecelerating to automatically set the content offset to the current screen position.
To implement:
Assign a delegate to your UIScrollView object if you have not already done so.
In your delegate's .m implementation file, add the following lines of code:
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
[scrollView setContentOffset:scrollView.contentOffset animated:YES];
}
Voila! No more auto-scroll.
For iOS 5.0 or later, there is a better method than calling setContentOffset:animated:.
Implement delegate method scrollViewWillEndDragging:withVelocity:targetContentOffset: in your .m file:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
targetContentOffset.pointee = scrollView.contentOffset;
}
Assigning the current offset to targetContentOffset stops the UIScrollView from auto-scrolling.
You can just turn up the deceleration rate very high. With an infinite rate, it would stop immediately. Try setting the rate to these constants:
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
and
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
If fast still isn't fast enough for you, UIScrollViewDecelerationRateFast is just typedef'ed as a float, so you can just multiply it by a factor of 10 or so to speed it up even more.
Just set the decelerationRate property to 0
It will disable the auto scrolling property. But keep in mind the user interaction will become bad if scrollview contentsize is big.
Previous Swift version:↓
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
Current Swift 4.2 version code:↓
scrollView.decelerationRate = UIScrollViewDecelerationRateFast
Related
I've set up an infinite scroll view and when it reaches 0 content offset I set it to max content offset and vice versa.
i.e.
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
This works but it stops the UIScrollView decelerating.
Is there a way to do this but keep the UIScrollView decelerating?
I tried this...
float declerationRate = scrollView.decelerationRate;
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width, 0) animated:NO];
scrollView.decelerationRate = declerationRate;
but it didn't work.
EDIT
I just realised that decelerationRate is the setting to determine how slow/fast the deceleration is.
What I need is to get the current velocity from the scrollView.
Right I had to tweak the idea a bit.
Turns out trying to set the velocity of a UIScrollView is difficult... very difficult.
So anyway, I kind of tweaked it.
This is actually a mini project after answering someone else's SO question and thought I'd try to solve it myself.
I want to create a spinner app that I can swipe to spin an arrow around so it spins and decelerates to a point.
What I did was set up a UIImageView with the arrow pointing up.
Then covering the UIImageView is a UIScrollView.
Then in the code...
#interface MyViewController () <UIScrollViewDelegate>
#property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
#property (nonatomic, weak) IBOutlet UIImageView *arrowView;
#end
#implementation MyViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//make the content size really big so that the targetOffset of the deceleration will never be met.
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * 100, self.scrollView.frame.size.height);
//set the contentOffset of the scroll view to a point in the center of the contentSize.
[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width * 50, 0) animated:NO];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)rotateImageView
{
//Calculate the percentage of one "frame" that is the current offset.
// each percentage of a "frame" equates to a percentage of 2 PI Rads to rotate
float minOffset = self.scrollView.frame.size.width * 50;
float maxOffset = self.scrollView.frame.size.width * 51;
float offsetDiff = maxOffset - minOffset;
float currentOffset = self.scrollView.contentOffset.x - minOffset;
float percentage = currentOffset / offsetDiff;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI * 2 * percentage);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//the scrollView moved so update the rotation of the image
[self rotateImageView];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//the scrollview stopped moving.
//set the content offset back to be in the middle
//but make sure to keep the percentage (used above) the same
//this ensures the arrow is pointing in the same direction as it ended decelerating
float diffOffset = scrollView.contentOffset.x;
while (diffOffset >= scrollView.frame.size.width) {
diffOffset -= scrollView.frame.size.width;
}
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 50 + diffOffset, 0) animated:NO];
}
#end
This gives the desired effect of a spinner like on Wheel of Fortune that will spin endlessly in either direction.
There is a flaw though. If the user keeps spinning and spinning without letting it stop it will only go 50 spins in either direction before coming to a stop.
As I said in the comment, the way you're doing is producing an expected result, one way to do what you want is to set the content offset to the top but then by using the content size height and deceleration value, you could animate the content offset again, check out this answer: https://stackoverflow.com/a/6086521/662605
You will have to play around with some math before it feel right but I think this is a reasonable workaround.
The lower the deceleration, the longer the animation (time) and the more it will animate (distance). Let me know what you think.
As you've said, the deceleration is probably not the only thing you need. So you could try KVO on the contentOffset to calculate the mean velocity over half a second perhaps to get an idea of speed.
There are probably a few different ways to do this. Ideally, you won't have to do any type of calculation to simulate the remaining deceleration physics or mess around at all with UIScrollView's internals. It's error-prone, and it's not likely to perfectly match the UIScrollView physics everyone's used to.
Instead, for cases where your scroll view is merely a driver for a gesture (i.e., you don't actually need to display anything in it), I think it's best to just have a massively wide scroll view. Set its initial content offset to the center of its content size. In scrollViewDidScroll(_:), calculate a percentage of the traversed screen width:
let pageWidth = scrollView.frame.width
let percentage = (scrollView.contentOffset.x % pageWidth) / pageWidth
This will basically loop from 0.0 to 1.0 (moving right) or from 1.0 to 0.0 (moving left) over and over. You can forward those normalized values to some other function that can respond to it, perhaps to drive an animation. Just structure whatever code responds to this such that it appears seamless when jumping from 0.0 to 1.0 or from 1.0 to 0.0.
Of course, if you need whatever you're looping to occur faster or slower than the normal scroll view speed, just use a smaller or larger fraction of the screen width. I just picked that arbitrarily.
If you're worried about hitting the edges of the scrollable content, then when the scroll view comes to a complete rest1, reset its content offset to the same initial center value, plus whatever remainder of the screen width (or whatever you're using) the scroll view was at when it stopped scrolling.
Given the resetting approach above, even for scroll views where you're hosting visible content, you can effectively achieve an infinitely scrolling view as long as you update whatever view frames/model values to take into account the reset scroll offset.
1 To properly capture this, implement scrollViewDidEndDecelerating(_:) and scrollViewDidEndDragging(_:willDecelerate:), only calling your "complete rest" function in the latter case when willDecelerate is false. Always call it in the former case.
I found that if you call
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
at point(0,0), it will trigger bounces action for UIScrollView, then the deceleration will stop. The same situation takes place when you call setContentOffset to bound of the content.
So you can call setContentOffset to point(10,10) or somewhere else to keep the deceleration easily.
I'm using UIScrollView's canceling touch ability with canCancelContentTouches.
However, I 'd like the uiscrollview to attempt to cancel touch when it detected horizontal dragging(not vertical).
(Hope solution would be available under < iOS 3.13)
Thank you
Implement the UIScrollViewDelegate and then use something like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[scrollView setContentOffset: CGPointMake(0, scrollView.contentOffset.y)];
}
Another way would be having a UIScrollView which is smaller or equal to the size of its parent view and with a disabled "Always bounce horizontal".
The safest and most successful method I've found to constrain the movement of a scroll view is to subclass UIScrollView and override setContentOffset:animated: and setContentOffset: methods (code below).
The advantage of overriding these methods is that it directly alters the requested contentOffset before any of the UIKit code starts to act on it, avoiding any of the side effects that can occur when modifying the contentOffset in scrollViewDidScroll: or other UIScrollViewDelegate methods.
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
// restrict movement to horizontal only
CGPoint newOffset = CGPointMake(contentOffset.x, 0);
[super setContentOffset:newOffset animated:animated];
}
- (void)setContentOffset:(CGPoint)contentOffset {
// restrict movement to horizontal only
CGPoint newOffset = CGPointMake(contentOffset.x, 0);
[super setContentOffset:newOffset];
}
i guess i would use the method scrollViewWillBeginDragging
found in the UIScrollViewDelegate
and inside i could control if user is going horizontally or not...
scrollViewWillBeginDragging:
Tells the delegate when the scroll view is about to start scrolling the content.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
Parameters
scrollView
The scroll-view object that is about to scroll the content view.
Discussion
The delegate might not receive this message until dragging has occurred over a small distance.
Availability
Available in iOS 2.0 and later.
I want to use the scrollview as something like a picker in horizontal mode.
The scrollview holds up to seven subviews.
Each subview represents a value.
Always three views are visible and the one in the middle is the selected one.
Scrollview visible at start:
__ | V1 | V2
Scrollview set to view/value two:
V1 | V2 | V3
Scrollview set to last value:
V2 | V3 | __
The real problem I have got is the "pagingEnabled" flag.
If pagingEnabled is set to YES the scrollview pages always three subviews/values instead of only one.
If pagingEnabled is set to NO the scrollview does not clinch.
Is there a nice solution for my problem?
Thanks a lot,
Dan ;)
Change the frame of the scrollview to be as if it were only displaying the middle view (i.e. a third of its original width, and offset by the same amount), but then set its clipsToBounds property to NO.
I found a solution if anyone else is interested.
Assign you view the delegate of a scrollview. Ovveride scrollViewDidEndDecelerating, afterwards get your current index(page you want) by doing something like.
NSNumber* currentIndex = [NSNumber numberWithInt:round(scrollview.Contentoffset.x / PAGE_SIZE)];
//Then just update your scrollviews offset with
[scrollview setContentOffset:CGPointMake([currentIndex intValue] * PAGE_SIZE, 0) animated:YES];
Since iOS 5, there's the scrollViewWillEndDragging:withVelocity:targetContentOffset: delegate method on UIScrollViewDelegate. This allows you to implement arbitrary paging.
For this to work, you first need to set the pagingEnabled property to NO, otherwise the delegate method I'm talking about isn't called. The scroll view now calls this delegate method whenever the user lifts his finger and the scroll view wants to determine where to finish the scrolling.
The magic is the last argument, targetContentOffset: it's a pointer to a CGPoint and used as a in/out variable. This means this variable tells you where the scrollview wants to scroll to. But it allows you modify this target location. The velocity might also be of interest, it can give you an indication whether the user "pushed" the scroll view or moved it, stopped, then lifted his finger.
For example, here's an implementation that rounds the target x location to the nearest multiple of 100, thus making "pages" of 100 points width.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
targetContentOffset->x = round(targetContentOffset->x / 100.0) * 100.0;
}
Setup: I have a UITableView, each UITableViewCell has a UIScrollView. What I am trying to do is to get all of the UIScrollViews to scroll together, such that when you scroll one of them all of the UIScrollViews appear to scroll simultaneously.
What I've done is subclass UITableView so that it has an array of all of the UIScrollViews within its table cells. I then forwarded TouchesBegan, TouchesMoved, TouchesCancelled, and TouchesEnded from the UITableView to all of the UIScrollViews in the array.
This doesn't appear to work. The UIScrollViews do not scroll! The only way I've managed to get this to work is to call the setContentOffset: method on the scrollviews. However, this is a pretty bad solution since it doesn't give you the swiping and deceleration features of the UIScrollView.
Any ideas on why my touches methods aren't getting to the UIScrollViews? Or a better way to implement this?
Ok, got it working. Thanks for the tips Ricki!
2 things to add to Ricki's solution, if you want to avoid an infinite loop, you have to check to see whether the scrollView's tracking or dragged properties are set. This will insure that only the ScrollView that is actually being dragged is calling the delegate.
- (void)scrollViewDidScroll:(UIScrollView *) theScrollView {
if (theScrollView.dragging || theScrollView.tracking)
[self.delegate scrolling:[theScrollView contentOffSet]];
}
Also, in the scrolling method of the delegate, I set animated to NO, this got rid of the delay between the initial swipe and the other scrollviews getting updated.
I did something "similar" where I had 4 scrollViews incased inside a parent view.
I placed a scrollView inside a UIView, this UIView was passed a delegate from its parentView, that was the view who kept track of all the scrollViews. The UIView containing a scrollVIew implemented the UIScrollViewDelegate and this method;
- (void)scrollViewDidScroll:(UIScrollView *) theScrollView {
[self.delegate scrolling:[self.scrollView contentOffSet]];
}
Now the parent view did this on all the scrollViews:
- (void) scrolling:(CGFloat) offset {
for(UIScrollView *s in self) {
[s setContentOffset:offset animated:YES];
}
}
It is of course a bit of a strain on the CPU, but scrolling several views will be that under any circumstances :/
Hope this was something in the direction of what you needed, and that it made any sense.
Added:
I took me 8 different paths and a lot of mass chaos before I made it work. I dropped the touchedBegan approach early, there is just no way to write something that comes close to Apples swipe, flick, scroll algorithms.
I don't know if the tableview and scrollview will "steal" each others touch events, but as I can read from your description you made that part work.
A follow up idea to ease the CPU usage. add each scrollview to a cell, set its tag=14, now when scrolling asked for all visible cells only, ask for viewWithTag=14, set the contentOffset on this. Save the content offset globally so you can assign it to cells being scrolled onto the screen in cellForRowAtIndexPath.
So set the offSet to a global property, in cellForRowAtIndexPath find the view with tag = 14, set its offset. This way you don't even need a reference to the scrollViews only the delegate.
If you have differently sized UIScrollViews and are using paging, this works great:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)_scrollView {
#pragma unused(_scrollView)
categoryPageControlIsChangingPage = NO;
for (UIImageView *iv in [categoryScrollView subviews]) {
iv.alpha = (iv.tag != categoryPageControl.currentPage+1)?0.5f:1.0f;
ILogPlus(#"%i %i", iv.tag, categoryPageControl.currentPage+1);
}
[self scrolling:_scrollView];
}
- (void)scrolling:(UIScrollView *)sv {
CGFloat offsetX = sv.contentOffset.x;
CGFloat ratio = offsetX/sv.contentSize.width;
if ([sv isEqual:categoryScrollView]) {
[categoryScrollViewLarge setContentOffset:CGPointMake(ratio*categoryScrollViewLarge.contentSize.width, 0) animated:YES];
}else {
[categoryScrollView setContentOffset:CGPointMake(ratio*categoryScrollView.contentSize.width, 0) animated:YES];
}
}
I have a UITextView included in a UITableViewCell. The layout is correct when the views are initially displayed, but once I click in the UITextView it automatically scrolls up a bit and the top half of the characters on the first line becomes invisible.
This image is when the UITextView is not active:
UITextView not active http://gerodt.homeip.net/uitextview-notactive.png
And this one is when I clicked in the UITextView to make it active:
UITextView active http://gerodt.homeip.net/uitextview-active.png
I do not the UITextView to scroll up at all, it should simple stay fixed. How can I achieve this? I already tried several settings in Interface Builder, but no luck so far.
Any suggestions are appreciated.
Gero
UITextView is a subclass of UIScrollView, so it has a configurable contentInset property. Unfortunately, if you try to change contentInset on a UITextView instance, the bottom edge inset always gets reset to 32. I've run into this before with short UITextView frames and found this to be an issue. I suspect this is what is causing your problem, but you should check the contentInset of your textview in the debugger to be sure.
The workaround/solution is simple: subclass UITextView and override the contentInset method so that it always returns UIEdgeInsetZero. Try this:
//
// BCTextView
//
// UITextView seems to automatically be resetting the contentInset
// bottom margin to 32.0f, causing strange scroll behavior in our small
// textView. Maybe there is a setting for this, but it seems like odd behavior.
// override contentInset to always be zero.
//
#interface BCZeroEdgeTextView : UITextView
#end
#implementation BCZeroEdgeTextView
- (UIEdgeInsets) contentInset { return UIEdgeInsetsZero; }
#end
This is how UITextView behaves according to Apple's engineer this is intended and UITextView is meant for text that are at least a few lines in height. There is no work around to this, use a UITextField instead or increase your UITextView to at least 3 lines in height.
You can also just do:
textView.contentInset=UIEdgeInsetsZero;
in your delegate file.
UITextView is a subclass of UIScrollView, so the answer involves the contentOffset property, which is what is being changed, not the insets or the content size. If the scroll position is correct when the view first appears, then you can store the content offset for later recall.
YourViewController.h snipped
#interface YourViewController : UIViewController <UITextViewDelegate, UIScrollViewDelegate>
#property(nonatomic, weak) IBOutlet UITextView *textView;
#end
YourViewController.m snippet
#implementation YourViewController {
#private
BOOL _freezeScrolling;
CGFloat _lastContentOffsetY;
}
// UITextViewDelegate
- (void)textViewDidBeginEditing:(UITextView *)textView {
// tell the view to hold the scrolling
_freezeScrolling = YES;
_lastContentOffsetY = self.textView.contentOffset.y;
}
// UITextViewDelegate
- (void)textViewDidEndEditing:(UITextView *)textView {
_freezeScrolling = NO;
}
// UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_freezeScrolling) {
// prevent the scroll view from actually scrolling when we don't want it to
[self repositionScrollView:scrollView newOffset:CGPointMake(scrollView.contentOffset.x, _lastContentOffsetY)];
}
}
// UIScrollViewDelegate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// scroll prevention should only be a given scroll event and turned back off afterward
_freezeScrolling = NO;
}
// UIScrollViewDelegate
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
// when the layout is redrawn, scrolling animates. this ensures that we are freeing the view to scroll
_freezeScrolling = NO;
}
/**
This method allows for changing of the content offset for a UIScrollView without triggering the scrollViewDidScroll: delegate method.
*/
- (void)repositionScrollView:(UIScrollView *)scrollView newOffset:(CGPoint)offset {
CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = offset;
scrollView.bounds = scrollBounds;
}
What's also important to note in the code sample above is the last method. Calling any sort of setContentOffset: will actually trigger scrolling, which results in calling scrollViewDidScroll:. So calling setContentOffset: results in an infinite loop. Setting the scroll bounds is the workaround for this.
In a nutshell, we tell the view controller to prevent the UITextView from scrolling when we detect that the user has selected the text for editing. We also store the current content offset (since we know that the position is what we want). If the UITextView tries to scroll, then we hold the content offset in place until the scroll has stopped (which triggers either scrollViewDidEndDecelerating: or scrollViewDidEndScrollingAnimation:). We also unfreeze the scrolling when the user is done editing.
Remember, this is a basic example, so you'll need to tweak the code based on the exact behavior you want.
I was experiencing a similar issue with undesired UITextView scrolling. I finally managed to fix it by resetting the contentSize at the end of my keyboardDidShow:
- (void)keyboardDidShow:(NSNotification *)notification {
textView.contentSize = CGSizeZero;
}
You also will need to register for the keyboard notification, like so:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
In my case I didn't want any scrolling since I was resetting the frame to the height of the textView's contentSize when textViewDidChange (growing textview inside a UIScrollView).
Try putting in Redraw on the textview instead of Scale to Fill. You still might have to capture the delegate and keep the content offset but it should at least prevent the jump to point (0,0). Also Autoresizes subview must be turned off. It was jumping to top of textview every time on me too and this solved that problem.