Anyone has an idea how to control two scroll views while one in on the top of the other. One scrolls, the other one scrolls too. Same with zoom, gesture recognisers, etc ... Kinda like passing replica of the touches received by the first view onto the the one underneath. Subclass of the top scroll view has got a weak reference to the "dependant" scroll view underneath. Very important is to get the delegate methods working for both scrollviews as there is a lot of logic in these ...
use the scrollView Delegate methood
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
} else {
CGPoint offset = scrollViewA.contentOffset;
offset.y = scrollViewB.contentOffset.y;
[scrollViewA setContentOffset:offset];
}
}
or simply in the same method for both horizontal an vertical scrolling
if([scrollView isEqual:scrollViewA]) {
scrollViewB.contentOffset = scrollViewA.contentOffset;
}
and viceVersa
Related
I have two scrollview, one scrollview inside another and I want to scroll my main scrollview content first after that only I want to scroll subview scroll
CGFloat scrollOffset = texscrl.contentOffset.y;
if (scrollOffset == 0)
{
//This condition will be true when scrollview will reach to bottom
self.ArtistScroll.scrollEnabled=YES;
texscrl.scrollEnabled=YES;
}else
{
self.ArtistScroll.scrollEnabled=NO;
texscrl.scrollEnabled=YES;
}
Here, am using content offset for this.. so can anyone help me?
You can achieve the affect you describe by using a third scrollview to actually handle the touch gestures and manually set the contentOffset of the other scrollviews.
Here is how to achieve this for vertically scrolling content, which is I think what you are describing. In the code, outerScrollView is the main scrollview, innerScrollView is the sub-scrollview that is contained by the outer scrollview, and trackingScrollView is the third scrollview that only handles the touches - it has no content.
Create the three scrollviews such that trackingScrollView exactly covers outerScrollView. (I assume you will do this in XIB or storyboard so there is no code.)
Once your scrollviews have content (and whenever their contentSize or bounds change) you should set the contentSize of trackingScrollView:
self.trackingScrollView.contentSize = CGSizeMake(self.trackingScrollView.bounds.size.width,
self.outerScrollView.contentSize.height + self.innerScrollView.contentSize.height - self.innerScrollView.bounds.height);
This makes the content height be that of outerScrollView plus the scrollable distance of innerScrollView, allowing trackingScrollView to scroll over the total travel distance of both scrollviews.
Delegate for trackingScrollView and implement scrollViewDidScroll:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.trackingScrollView) {
CGFloat const offsetY = self.trackingScrollView.contentOffset.y;
// Calculate the maximum outer scroll offset
CGFloat const maxOuterOffsetY = self.outerScrollView.contentSize.height - self.outerScrollView.bounds.height;
// Calculate the maximum inner scroll offset
CGFloat const maxInnerOffsetY = self.innerScrollView.contentSize.height - self.innerScrollView.bounds.height;
if (offsetY < maxOuterOffsetY) {
// Scroll is within the outer scroll area or the top bounce zone
self.outerScrollView.contentOffset = CGPointMake(0, offsetY);
self.innerScrollView.contentOffset = CGPointZero;
} else if (offsetY < maxOuterOffsetY + maxInnerOffsetY) {
// Scroll is within the inner scroll area
self.outerScrollView.contentOffset = CGPointMake(0, maxOuterOffsetY);
self.innerScrollView.contentOffset = CGPointMake(0, offsetY - maxOuterOffsetY);
} else {
// Scroll is within the bottom bounce zone
self.outerScrollView.contentOffset = CGPointMake(0, offsetY - maxInnerOffsetY);
self.innerScrollView.contentOffset = CGPointMake(0, maxInnerOffsetY);
}
} else {
// Handle other scrollviews as required
}
}
Effectively we divide the tracking scroll area into two parts. The top part controls the scrolling within the outer scrollview and the bottom part controls the inner scrollview.
Assuming bounce is turned on (which generally it should be) we also need to handle scrolling outside the scroll area. We want the bounce to show on the outer scrollview so the top bounce is handled implicitly by the first conditional. However, the bottom bounce has to be handled explicitly by the third conditional, otherwise we would see the bounce on the inner scrollview.
To be clear, in this solution the two content scrollviews (outerScrollView and innerScrollView) do not receive any touches at all; all input is going to trackingScrollView. If you want input to subviews of the content scrollviews you will need a more advanced solution. I believe this can be done by putting trackingScrollView behind outerScrollView, removing outerScrollView's gesture recognizers and replacing them with those from trackingScrollView. I have seen this technique presented in an old WWDC session on UIScrollView but I have not tried it myself.
My collection view begins life short, at the bottom of it's view controller's view. When the user scrolls up a little bit, I'd like the collection to fill the view. I'd like it to shrink again when the user pulls down into the bounce area.
Here's the code that I'd like to make work (and I think should work):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.collectionView) {
CGFloat offsetY = self.collectionView.contentOffset.y;
if (offsetY > 20.0) {
[self setCollectionViewExpanded:YES];
} else if (offsetY < -10.0) {
[self setCollectionViewExpanded:NO];
}
}
}
- (void)setCollectionViewExpanded:(BOOL)expanded {
// avoid starting a do-nothing animation
BOOL currentlyExpanded = self.collectionView.frame.origin.y == 0.0;
if (expanded == currentlyExpanded) return;
// note: the collection view is the uppermost subview of the vc's view
// expanding to view bounds makes it cover every other view
CGRect newFrame = (expanded)? self.view.bounds : CGRectMake(0.0, 240.0, 320.0, self.view.bounds.size.height-240.0);
[UIView animateWithDuration:0.3 animations:^{
self.collectionView.frame = newFrame;
}];
}
But after the expand happens, the cells remain in the same position relative to the parent view (still near the bottom) and the collection view stops responding to panning touches and sends no further didScroll notification.
I can get the content repositioned properly by doing a reloadData in an animation completion block, but the scrolling touches stop working either way.
A while back, I tried something like this with a table view (or another kind of scroll view) and ran into a similar hangup. Much obliged if anyone has this solved...
Can you describe your view hierarchy a bit better? Is the UICollectionView a subview of the UIScrollView?
Is self.view a UIScrollView?
In that case, the bounds origin is the scrollview's contentOffset (not 0,0). That could give your UICollectionView an unexpected frame.
If the UICollectionView is a subview of the scrollview, the frame origin is the origin of the view in the scrollview-space. So don't change the frame's origin (just its size) and set the contentOffset of the scrollView to make it visible.
I am having an horizontal scrollview in an UIViewController, where i have many images in small sizes. I am keeping the images in scrollview because the images are more, so user can scroll horizontally and choose images. But, the problem is, i have to select an image and drag and drop to that UIViewController view. But, since the images are in scrollview, drag and drop images into UIViewcontroller's view is not working, not detecting the touch events too.
Please NOTE: If i don't have scrollview but just keeping the images also into UIViewcontroller's view itself, drag and drop the images on the same screen, is working very well.
How can I resolve this when I need to have scrollview and drag and drop images, any advice/help please?
Hi Getsy,
I am not going to provide you code directly but give idea how to manage this.
You can manage this way, When you get touch on your object in scrollView at that time or when you move that object by draging at that time disable scroll by myScroll.scrollEnabled = NO;
Then When on endTouch you can enable Scroll by myScroll.scrollEnabled = YES; So by this you can manage you object moving in scroll hope you got logic.
Here is the demo code : Drag and Drop with ScrollView. which has same logic of Disabling scroll view on touchesMoved: and Enabling scroll view on touchesEnded:.
I did implement that behaviour before without any subclassing.
I used canCancelContentTouches = NO of the UIScrollView to make sure the subviews handle there touches on their own. If a subview (in your case an image) was touched, i moved the view out of the scrollview onto the superview and started tracking it's dragging. (You have to calculate the correct coordinates within the new superview, so it stays in place).
After dragging finishes, i checked if the target area was reached, otherwise I moved it back into the scrollview. If that's not detailed enough I could post some code.
Well here is my example code: Github: JDDroppableView
Getsy,
Try the code for drag and drop the objects :
-(void)dragAndDropWithGesture {
UILongPressGestureRecognizer *downwardGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(dragGestureChanged:)];
[scrollViewAlfabeto addGestureRecognizer:downwardGesture];
for (UIGestureRecognizer *gestureRecognizer in myscrollView.gestureRecognizers)
{
[gestureRecognizer requireGestureRecognizerToFail:downwardGesture];
}
}
- (void) dragGestureChanged:(UIPanGestureRecognizer*)gesture
{
CGPoint point = [gesture locationInView:scrollViewAlfabeto];
if (gesture.state == UIGestureRecognizerStateBegan)
{
[imageViewToMove removeFromSuperview];
[self.view addSubview:imageViewToMove];
UIView *draggedView = [myscrollView hitTest:point withEvent:nil];
if ([draggedView isKindOfClass:[UIImageView class]])
{
imageViewToMove = (UIImageView*)draggedView;
}
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
imageToMove.center = point;
}
else if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed)
{
// Determine if dragged view is in an OK drop zone
// If so, then do the drop action, if not, return it to original location
NSLog(#"point.x final:%f", point.x);
NSLog(#"point.y final:%f", point.y);
if (CGRectContainsPoint(goal.frame, point)){
imageToMove.frame = CGRectMake(167, 159, 100, 100);
}
else{
[imageToMove removeFromSuperview];
[myscrollView addSubview:imageToMove];
[imageToMove setFrame:CGRectMake(12, 38, 100, 100)];
imageToMove = nil;
}
}
}
May this code will help you out.
For a similar problem, I made UIScrollView subclass like the following: PoliteScrollView passes touch messages to it's subviews when it determines that they are being dragged.
#interface PoliteScrollView : UIScrollView
// number of pixels perpendicular to the scroll views orientation to make a drag start counting as a subview drag
// defaults to 1/10 of the scroll view's width or height, whichever is smaller
#property(nonatomic,assign) CGFloat subviewDraggingThreshold;
// yes when a subview is being dragged
#property(nonatomic,assign) BOOL isDraggingSubview;
// the subview being dragged
#property(nonatomic,strong) UIView *draggingSubview;
#end
#protocol PoliteScrollViewDelegate <NSObject>
#optional
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesBegan:(NSSet *)touches;
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesMoved:(NSSet *)touches;
- (void)scrollView:(PoliteScrollView *)scrollView subviewTouchesEnded:(NSSet *)touches;
#end
The key design idea is that dragging in a scroll view is ambiguous. Is the user scrolling or dragging a subview? PoliteScroll view handles this by providing two things: (1) a notion of orientation (horizontal if it's longer than it is wide, vertical otherwise), and (2) a threshold distance for what constitutes a drag in the direction perpendicular to it's orientation. (defaults to 1/10 the width or height).
I pasted this and several other files are in a paste bin, containing the following:
PoliteScrollView .h and .m
DraggableImageView.h and .m - that changes it's position when it gets touch messages.
ViewController.m - that demonstrates thetwo in combination.
To combine these in a project, paste the paste bin into files appropriately named, add a storyboard with a PoliteScrollView (be sure to set it's delegate), add in some images (the ViewController tries to add puppy0.jpeg to puppy4.jpeg.
I created an example which illustrates how to drag and drop between two or more views:
http://www.ancientprogramming.com/2012/04/05/drag-and-drop-between-multiple-uiviews-in-ios/
I found it a good idea to register the gesture recognizer to a different view than the actual views being dragged. This will make sure the gestures continue even though the dragged view changes its 'parent' view.
Maybe it can give some inspiration
I have a UIScrollView that is scrolling a fairly large UIView.
At certain times I want to limit the area the user can scroll around in. For example, I may only want to allow them to view the bottom quarter of the view.
I am able to limit the area by overriding scrollViewDidScroll and then calling setContentOffset if the view has scrolled too far. But this way I can't get it bounce back as smoothly as the UIScrollView can naturally do when scrolling beyond the bounds of the UIView.
Is there a better way to limit the scrollable area in a UIScrollView?
I would change the contentSize property of the scroll view to the size of the area you want the user to be able to scroll around in and adjust the frame.origin of the subview such the upper left boundary you want appears at (0, 0) relative to the scroll view. For example, if your view is 800 points tall and you want to show the bottom quarter, set the height of contentSize to 200 and set the y component of view.frame.origin to -600.
I've found something that works for me. It let's you scroll to point 0,0 but no further:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x <= -1) {
[scrollView setScrollEnabled:NO];
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
[scrollView setScrollEnabled:YES];
}
}
You could do the same for top, bottom or right (x or y)
a small improvement on Yoko's answer in Swift 4 will be
override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > 600 {
let anim = UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) {
scrollView.isScrollEnabled = false
scrollView.setContentOffset(CGPoint(x: 0, y: 600), animated: false)
scrollView.isScrollEnabled = true
}
anim.startAnimation()
}
}
which will make the scrollview animate really similar to what its supposed to do. The slower drag when you are in the "bounce" area will not work and animation duration has to depend on the distance (not constant like here) if you want to be exact. You can also try to do this logic in scrollViewDidScroll and see how it differs. The key thing is that setContentOffset(_:,animated:) has to be with animated: false so that the UIViewPropertyAnimator's block can capture it and animate it
Another approach is to override the UIScrollView's method:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event.
Returning YES will allow the user to scroll. Returning NO will not.
NOTE: This will disable all touches to any views imbedded inside the UIScrollView that pointInside returns NO to. Useful if the area you don't want to scroll from doesn't have any interaction.
This example only allows the UIScrollView to scroll when the user is scrolling over a UITableView. (A UITableView and two UIViews are imbedded inside the UIScrollView)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event] && ![subview isKindOfClass:[UITableView class]]) {
return NO;
}
}
return YES;
}
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];
}
}