Description
If you use the Spotify app on iOS or Android (I am developing on iOS), you'll notice that if you select a playlist, and drag the UITableView with your songs up, the following happens:
The table doesn't scroll, it just moves up at the same speed it would scroll, and the images above it move slower than the table, creating a parallax scrolling effect.
Once the tableview reaches the top of the view, however, it behaves like a normal scrolling tableview.
I've tried to achieve this effect a few different ways, which didn't work for me.
Here is a video of this effect: https://www.dropbox.com/s/n7npk4lrzmag0sn/IMG_9331.MOV
What I tried
I wanted the UITableView and the UIScrollView above it, to move upwards when the user scrolled either one, so I used
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
and based on the direction of the scroll, I changed the frame position of the tableview and the UIScrollView above it.
The problem with this method is that the tableview bounces, which ruins the effect.
To get rid of bouncing, I tried the following:
_userTableView.bounces = NO;
However, now because the tableview wouldn't scroll, scrollViewDidScroll is never called.
The other thing I tried was subclassing the UITableView, and overriding the
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
hitTest method to detect a scroll gesture on the tableview, the problems with this are:
Only 1 CGPoint is passed -- it's impossible to know if the UITableView was touched, or scrolled.
Since scrolling is disabled, I can't use self.decelerating to check if the tableview was just scrolled -- the tableview will never be decelerating
Making the table decelerate as it moves up the view, and continue scrolling with the same momentum after its reached the top will be very challenging; In the Spotify app, a user can drag the table with a speed high enough to make the table view move to the top of the view AND continue scrolling, all in one motion.
The tableview should grow in size to accommodate more cells added to the view (without scrolling), as it moves up the view, or it should start out with the same size as the view. After it reaches the top it should begin scrolling like a normal tableview -- with the same momentum it has after the upward movement.
Any suggestions on how I can solve this?
Thank you very much.
For the effect you have shown, I think a tableHeaderView with the parallax content as a subview will suffice.
In -scrollViewDidScroll: (or KVO for contentOffset), you set the center of the parallax subview to the center of the visible bounds of the tableHeaderView. You can also adjust how much the content "sticks" to the center by multiplying a factor.
CGRect tableViewHeaderVisibleBounds = tableViewHeader.bounds;
tableViewHeaderVisibleBounds.origin.y = MAX(0, MIN(CGRectGetHeight(tableViewHeader.bounds), tableView.contentOffset.y));
tableViewHeaderVisibleBounds.size.height -= tableViewHeaderVisibleBounds.origin.y;
CGFloat factor = 1.8f; // less than 0.5:stick to top // greater than 0.5:stick to bottom
parallaxContentView.center = (CGPoint){
.x = parallaxContentView.center.x,
.y = (CGRectGetMinY(tableViewHeaderVisibleBounds)
+ (CGRectGetHeight(tableViewHeaderVisibleBounds) * factor))
};
I coded this inside my head so just make necessary adjustments, but I hope you get the basic idea.
Related
I have an iPad app using Storyboards. I have one scene with a UIView, which has an additional UIView on the top half (which I draw on), and a UIScrollView on the bottom half (I needed to keeep them separate). I'm having a hard time figuring out how to address the UIView with this code that I copied from another SO question:
CGFloat scrollViewHeight = 0.0f;
for (UIView* view in scrollView.subviews)
{
scrollViewHeight += view.frame.size.height;
}
[scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];
The way I read the code above, I'm supposed to use the UIView, but that doesn't make sense, since the UIView covers the entire "scene/window". It makes more sense to me to use the UIScrollView to calculate the ContentSize.
So, what am I supposed to use?
Not completely clear on what you are asking. Once you have a reference to the scroll view, either through tags, properties or some other method, you should set its content size to the width and height of all of its content combined (accounting for overlap, etc). Ex. if you had a single image view that was 600 by 900 points inside the scroll view, you'd set its size to a 600 by 900 CGSize.
That goes through all of the subviews of the scroll view using a for...in loop (aka fast enumeration), adding their heights together, then uses that as the height of the content. I don't think that would be a good way to do it, because that assumes that all the subviews are stacked vertically on top of one another perfectly. It's basically going, "For every subview of my scroll view, add their height to this total and then use that as the content size's height."
The easiest way to manage scroll views is to slap all the content into one UIView, place that in the scroll view, then set the scroll view's content size to the frame of the UIView. If you need a reference to the UIView contained in the scroll view, you can set it to a property or give it a tag and use viewWithTag.
I'm trying to scroll the contents of a UIView using the contentOffset which so far I achieve handling touchesBegan: and touchesMoves: methods of the view, however I get a jumpy effect as I'm changing the bounds of the UIView manually (for example: current position +/- the change of the last position of the finger and the new position)
Is there any easy way to achieve this without manually changing the bounds of the UIView?
IMPORTANT NOTES: I'm not using a UIScrollView because the mentioned UIView has plenty of draggable subviews so if I use a UIScrollView I can't drag because the UIViewScroll scrolling event executes over the subviews events.
Animating the layer of the mentioned UIView causes the contents to move out of control, like if the point of reference had changed somehow.
However I'm always open to suggestions.
I would rather make a UIView container clipping subviews, add the content view which we will drag as a subview and change it's frame. I think it shouldn't have jumpy effect as it must be the same as animating the view frame change.
I have a UITableView which covers half of the screen in iPad portrait mode. Whenever I click, expand button which calculates and animates the tableview's superview's frame to cover the whole screen, scrolling becomes very messy and flickering even with a single row. Especially bounce animation seems to be longer. TableView is masked with autoresizeheight. After I restore superview's frame, tableview scrolling becomes smoother again. Are there any relation between tableview's initial frame and the scrolling speed? What are the possible root causes for this kind of behavior?
Maybe you inserted the resize inside your cellForRowAtIndexPath method, so it sets the size tableview every time you scroll. I inserted this code snippet inside a check in viewDidLoad, then the size will be set just once at the beginning and everything works.
CGPoint tvFrameO = _TableView.frame.origin;
CGSize tvFrameS = _TableView.frame.size;
float valueToResize = 50.0f;
_TableView.frame = CGRectMake(tvFrameO.x,
tvFrameO.y,
tvFrameS.width,
tvFrameS.height + valueToResize);
I have a custom subclass of UIView and a full screen UIButton with a background image on it as you can see in the image,
on the right side, there is a UIView with a UIImageView on it and some other not important labels and buttons. The important part is, there are two scrollviews on that UIView and at the run time, i add UIButtons ( not small ones, 200 x 100 ) on those scrollviews with vertical scrolling enabled.
The situation is really very complex so i will write as items what happens in different conditions. In my application, there are 10 of these complete screens, on a custom subclass of UIView and i use UIPanGestureRecognizer to scroll from one to other.I have a good reason not to use a scrollview for this. All the windows ( the below image, not UIWindow ) populates their content in a loop, so the running code is exact same for all of them and all of them are being created from the same .xib file. And the last information, the bigger UIButton that covers all the window has an action for touchupinside and the smaller buttons in scrollviews have their own touchupinside selectors.
Some of my windows works perfectly, if i touch anywhere on the screen, the bigger buttons action is called, if i touch and drag, the scroll works and i navigate to the next / previous window, if i touch on smaller buttons in scrollviews, their actions fired and lastly i can perfectly scroll within those buttons.
Some of my windows, when i try to scroll the scrollviews, the bigger windows pangesturerecognizer catchs this event not the small scrollviews, and if i touch the small button in that scroll view, the bigger buttons event is fired ( it pass all through uibutton > uiscrollview > uiimageview > uiview to the uibutton at the button like they dont exist )
if i replace the right container view some other position on the big uibutton, it randomly works well or not, sometimes one of the scrollviews works well and the other not.
it has a consistent behaviour, if on a one position, it works, it allways works, and if it not, it never does.
Again, all the views and subclasses have their userinteraction enabled yes, the opposite is already imposible because the behaviour changes only acording to position of container view on the big uibutton and also changes acording to big uibuttons background image.
I've placed a touchesBegan method for just test purposes, when the touches does not work as expected, the event fired with touch.view is the container of the big button, even if i touch on a small button within the scrollviews.
I've spend two days already for that and no result.What can cause this behaviour ?
EDIT : After krumelur's comment I've changed my focus from configuration of those views to animation i give to the container views, and i've noticed that, the problem is about the animation, i'm adding the the code part that animates all ten windows and behaves like a custom scrollview. All windows have their custom layer class and when i catch pangesture recognizer i move windows on the screen with following code. items in that code is an array which holds layers of all the windows. The animation causes that strange situation but i couldnt figure out yet..
- (void)layoutSublayers {
[super layoutSublayers];
[CATransaction begin];
[CATransaction setDisableActions:YES];
float angleDelta = 2 * M_PI / [items count];
float a = angle;
for (UIBaseLayer *l in items) {
l.position = self.position;
CATransform3D translation = CATransform3DMakeTranslation(cosf(a)*(radius.x), 1.0, (sinf(a)*radius.y) - radius.y*1.0);
float dailyAngle = (M_PI_2 - a);
CATransform3D rotation = CATransform3DMakeRotation(dailyAngle, 0, 1.0, 0);
CATransform3D t = CATransform3DConcat(rotation, translation);
l.transform = t;
a += angleDelta;
}
[CATransaction commit];}
http://img836.imageshack.us/img836/296/screenshot20111215at013.png
bringSubviewToFront did not work reliably on 5.0 (on 5.1 the hack was not needed)
I had to add a bigger transparrent UIButton around the button that behaved as if it was disabled to fix this on iOS prior to 5.1.
At last I've figured out that, even I send back views with CATransform3DMakeTranslation and layer.zPosition settings, touch events goes to views according to their frame position. The views that aren't currently even seen (overlayed by others) still catch touches. Writing just one line of code ([self.superview bringSubviewToFront:self]) for the view that is currently on screen solved the problem.
For testing this and understanding what I'm talking about, I've made simple test application that has only two buttons on it.
The blue and yellow buttons.The blue button overlays the yellow button so you can only see the blue one at the beginning.
On the touch up inside code for the blue button, I just send it back with buttons.layer.zPosition = -1. After this, there is no visible part of the blue button remains on screen, instead I see just the yellow button now, but when I touch the yellow button (or I think I am touching it) still blue buttons touch up inside code runs.
I'm not sure if this is the desired logic but it does not seem reasonable to me.
As I mentioned above, writing [blueButton.superview sendSubviewToBack:blueButton] while setting its layers zPosition to -1 works as expected.
I have a NSScrollView with a custom view inside and when I resize the NSScrollView, the height grows and it scrolls upward. Unless there is an easier way, I'll probably have to register a notification to see if the view changes size and then adjust the scrollPoint: to a new point. I'm having trouble getting method to work smoothly.
Thanks!
This question comes up from time to time, because it's not obvious.
The content will pin to the top left if [contentView isFlipped] == YES.