Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animating a view inside a cell : First init #40

Closed
lluisgerard opened this issue Jan 11, 2014 · 10 comments
Closed

Animating a view inside a cell : First init #40

lluisgerard opened this issue Jan 11, 2014 · 10 comments

Comments

@lluisgerard
Copy link
Contributor

First of all, I don't know if this is really an issue of Masonry or an Auto Layout thing.

I'd been doing some tests animating things with Masonry and after some tweaking it usually works like a charm and is pretty fast. I want to share one of these tests with you which is perfect to show the issue I had and the workaround used.

https://github.com/lluisgerard/Masonry-Animated-Cell

As you can see here. I have a view that starts always at width 0 and grows to the pixel equivalent percentage (from 0 to 300 px).

The basic code for this animation is where self.animateConstraint is animated:

        float width = self.frame.size.width / grade;
        self.animateConstraint.equalTo(@(width));
        [UIView animateWithDuration:1.0f
                              delay:0.0f
             usingSpringWithDamping:0.4f
              initialSpringVelocity:0.9f
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             [self layoutIfNeeded];
                         } completion:^(BOOL finished) {

                         }];

What happens is that when you start the app for the first time (aka load the tableview) the growing view doesn't animate at all. It just appears on the cell with the end state of the animation.

I don't know the best way to approach this, maybe there is something that I'm missing like a trigger when the view is completely loaded and constraints are set (delegates?). I even found other issues that I cannot replicate now, like the bar started to grow while the whole view was being set by autolayout (I think I was using updateConstraints then).

Ok, the solution (or workaround) I found was to do this only the first init:

        double delayInSeconds = 0.0;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            growBlock(); // this block is the above animation code
        });

As you can see, delay is set to 0 seconds but the dispatch_after is enough to activate the first animation. You can download my example project, comment the dispatch, you will see that nothing is animated on the first load then.

You can see the whole code here https://github.com/lluisgerard/Masonry-Animated-Cell/blob/master/AnimateLayouts/LLGAnimatedView.m

I'm sure I'm missing something here to know when the autolayout finished doing it's job (maybe tracking self.frame width and height?). Anyway, I hope that at least somebody can enjoy my example :)

@lluisgerard
Copy link
Contributor Author

Ok, noticed that first rows were pretty low to see the animation on first run. I inverted the numbers so rows can go from 100% to 0%.

@cloudkite
Copy link
Contributor

@lluisgerard thanks for the very detailed issue info!

I think adding [self setNeedsLayout] before the animation block should fix your issue.
Because you are changing a subview constraint the superview does not realise it needs laying out until the next runloop which will be after the animation block.

Let me know if that helps

@lluisgerard
Copy link
Contributor Author

Thanks @cloudkite, I will post a GIF here showing both cases because is hard to explain in words.

With [self setNeedsLayout] seems that the animation blocks triggers in the middle of the layout and the whole view grows from top to bottom (not horizontally).

Dispatch After VS [self setNeedsLayout]
dispatch_after setneedslayout_

@lluisgerard
Copy link
Contributor Author

In fact, when you set the percentage the first thing the code does is [self layoutIfNeeded] after equalTo(@0). This is key or the frame.size will be zero the first time you run the app:

    self.animateConstraint.equalTo(@0);
    [self layoutIfNeeded];

Using setNeedsLayout here doesn't work and you never see the bar growing from 0 but the last percentage (because is reusing each cell).

According to documentation:
layoutIfNeeded:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy.

setNeedsLayout:
Call this method on your application’s main thread when you want to adjust the layout of a view’s subviews. This method makes a note of the request and returns immediately. Because this method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.

I will continue searching for more information about this to find an elegant way, will post something asap, thanks.

@lluisgerard
Copy link
Contributor Author

Ok, here is what I found:

  • The main problem is that the cell width is still growing while the first child animation triggers.

With this in mind, I removed the dispatch_after workaround and used [self setNeedsLayout] as @cloudkite suggested #40 (comment).

What I did then is call [self layoutIfNeeded] in the cell before setting the bar percentage:
Cell's method:

- (void)setPercentage:(float)percentage {
    _percentage = percentage;
    [self layoutIfNeeded]; // NEW!
    [self.animatedView setPercentage:_percentage];
}

Result:
cell1

  • Here you can see that the black background covers the whole width and height, so the animatedView is set before the animation 👍
  • The green view still grows/animates in height but is not so annoying as before.

⚠️ Now I have another issue with the numberLabel that doesn't grows with the bar the first load. Everything works fine when reusing the cell.

    [self.numberLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.viewThatGrows);
        make.left.equalTo(self.viewThatGrows).offset(5).priorityHigh();
        make.right.equalTo(self.viewThatGrows).offset(-5).priorityLow();
        make.bottom.equalTo(self.viewThatGrows);
    }];

⚠️ Guess I will have to tweak a little bit this label to make it never disappear on the left but stick to the right as the bar grows to make it work on first and reuse loads. It works well when I just set the the right constraint (but obviously it disappear if the bar is too small).

(man, this thread is getting large, sorry 🐙)

@cloudkite
Copy link
Contributor

@lluisgerard I think you having problems as setting percentage could happen before the cell has a frame. Try the following:

in LLGCell.m

- (void)layoutSubviews {
    [super layoutSubviews];
    [self.animatedView setPercentage:_percentage];
}

- (void)setPercentage:(float)percentage {
    _percentage = percentage;
    [self setNeedsLayout];
}

in LLGAnimatedView.m

- (void)setPercentage:(float)percentage {
    _percentage = percentage;

    self.numberLabel.text = [NSString stringWithFormat:@"%i%%", (int)_percentage];

    self.animateConstraint.equalTo(@0);
    [self layoutIfNeeded];

    float width = CGRectGetWidth(self.frame) * (_percentage / 100.0);
    self.animateConstraint.equalTo(@(width));
    [UIView animateWithDuration:1.0f
                          delay:0.0f
         usingSpringWithDamping:0.4f
          initialSpringVelocity:0.9f
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         [self layoutIfNeeded];
                     } completion:nil];
}

@cloudkite
Copy link
Contributor

I tested it with above code, seems fix the issues as far as I can tell.

BTW what are you using for creating gifs?

@lluisgerard
Copy link
Contributor Author

Amazing! that's what I was missing the whole time! layoutSubviews 👍 thanks!!!!

Gifrocket: http://www.gifrocket.com Is the first time I use it but works very well. I think you can record the video with QuickTime but I'm using a payed app iShowU.

lluisgerard added a commit to lluisgerard/Masonry-Animated-Cell that referenced this issue Jan 12, 2014
Thanks to @cloudkite I finally found that you can start the animation in
layoutSubviews cell's method so it always starts when everything is set.
Related thread on Masonry SnapKit/Masonry#40
@lluisgerard
Copy link
Contributor Author

Just pushed these new changes to the example. 🎉 ✌️

@lluisgerard
Copy link
Contributor Author

I removed the layoutSubviews method from the cell to use it in the same animatedView 👍
When iOS calls layoutSubviews everything is already set on the parents views. I think is better to have the animation related stuff on the same place (animatedView), I'm glad it works also 💃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants