84

I want to make an Animation for when a View gets it's visibility set to GONE. Instead of just dissapearing, the View should 'collapse'. I tried this with a ScaleAnimation but then the View is collapse, but the layout will only resize it's space after (or before) the Animation stops (or starts).

How can I make the Animation so that, while animating, the lower Views will stay directly below the content, instead of having a blank space?

MrSnowflake
  • 4,724
  • 3
  • 29
  • 32
  • I've used the same technique, as Andy here has presented, on my ExpandAnimation: http://udinic.wordpress.com/2011/09/03/expanding-listview-items/ I didn't use a scale animation, I just built a new Animation class for that. – Udinic Sep 03 '11 at 22:14
  • This was very useful while I was trying to do this. Thank you – atraudes Sep 23 '11 at 19:42
  • Great, i need to adapt to my problem, but in the end it works. For me this solution was better than the other answer. – Derzu Apr 20 '12 at 17:48

4 Answers4

100

Put the view in a layout if it's not and set android:animateLayoutChanges="true" for that layout.

Jeff Brateman
  • 3,229
  • 2
  • 20
  • 26
Vinay W
  • 9,912
  • 8
  • 41
  • 47
51

There doesn't seem to be an easy way to do this through the API, because the animation just changes the rendering matrix of the view, not the actual size. But we can set a negative margin to fool LinearLayout into thinking that the view is getting smaller.

So I'd recommend creating your own Animation class, based on ScaleAnimation, and overriding the "applyTransformation" method to set new margins and update the layout. Like this...

public class Q2634073 extends Activity implements OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.q2634073);
        findViewById(R.id.item1).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        view.startAnimation(new MyScaler(1.0f, 1.0f, 1.0f, 0.0f, 500, view, true));
    }

    public class MyScaler extends ScaleAnimation {

        private View mView;

        private LayoutParams mLayoutParams;

        private int mMarginBottomFromY, mMarginBottomToY;

        private boolean mVanishAfter = false;

        public MyScaler(float fromX, float toX, float fromY, float toY, int duration, View view,
                boolean vanishAfter) {
            super(fromX, toX, fromY, toY);
            setDuration(duration);
            mView = view;
            mVanishAfter = vanishAfter;
            mLayoutParams = (LayoutParams) view.getLayoutParams();
            int height = mView.getHeight();
            mMarginBottomFromY = (int) (height * fromY) + mLayoutParams.bottomMargin - height;
            mMarginBottomToY = (int) (0 - ((height * toY) + mLayoutParams.bottomMargin)) - height;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            if (interpolatedTime < 1.0f) {
                int newMarginBottom = mMarginBottomFromY
                        + (int) ((mMarginBottomToY - mMarginBottomFromY) * interpolatedTime);
                mLayoutParams.setMargins(mLayoutParams.leftMargin, mLayoutParams.topMargin,
                    mLayoutParams.rightMargin, newMarginBottom);
                mView.getParent().requestLayout();
            } else if (mVanishAfter) {
                mView.setVisibility(View.GONE);
            }
        }

    }

}

The usual caveat applies: because we are overriding a protected method (applyTransformation), this is not guaranteed to work in future versions of Android.

Ram kiran Pachigolla
  • 20,897
  • 15
  • 57
  • 78
Andy
  • 4,039
  • 1
  • 20
  • 15
  • 3
    Why the hell didn't I think of this?! Thank you. Also I don't get the: "The usual caveat applies: because we are overriding a protected method (applyTransformation), this is not guaranteed to work in future versions of Android." - Why would protected functions differ between API versions? Those are not hidden and are implemented protected so that you can override them (otherwise they would be package scoped). – MrSnowflake Oct 18 '10 at 17:16
  • You're probably right about the protected method. I tend to be over-cautious about accessing them in an API. – Andy Oct 18 '10 at 21:42
  • Atleast then you are pretty sure API updates won't wreck you apps :). – MrSnowflake Oct 19 '10 at 13:53
  • 1
    This worked great for me, except that to get "collapsing" to work (fromY=0.0f, toY=1.0f), I had to remove the `0 - ` in the `marginBottomToY` calculation. – dmon Oct 25 '11 at 17:57
  • 2
    I would suggest using the generic type `MarginLayoutParams` instead of casting it to a specific `LayoutParam` type. – Paul Lammertsma Feb 08 '13 at 17:09
  • There IS an easy way to do this, see my answer – Vinay W May 21 '14 at 15:37
  • 3
    Suppose I have to do toggle animation, then how could do it in reverse. Please advice. – Umesh Apr 01 '15 at 06:24
7

I used the same technique as Andy here has presented. I wrote my own Animation class for that, that animate the margin's value, causing the effect of the item to disappear/appear. It looks like this:

public class ExpandAnimation extends Animation {

// Initializations...

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    super.applyTransformation(interpolatedTime, t);

    if (interpolatedTime < 1.0f) {

        // Calculating the new bottom margin, and setting it
        mViewLayoutParams.bottomMargin = mMarginStart
                + (int) ((mMarginEnd - mMarginStart) * interpolatedTime);

        // Invalidating the layout, making us seeing the changes we made
        mAnimatedView.requestLayout();
    }
}
}

I have a full example that works on my blog post http://udinic.wordpress.com/2011/09/03/expanding-listview-items/

Udinic
  • 3,014
  • 2
  • 25
  • 32
2

I used the same technique as Andy here, and refined it so that it can be used for expanding and collapsing without glitches, also using a technique described here: https://stackoverflow.com/a/11426510/1317564

import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.ScaleAnimation;
import android.view.animation.Transformation;
import android.widget.LinearLayout;

class LinearLayoutVerticalScaleAnimation extends ScaleAnimation {
    private final LinearLayout view;
    private final LinearLayout.LayoutParams layoutParams;

    private final float beginY;
    private final float endY;
    private final int originalBottomMargin;

    private int expandedHeight;
    private boolean marginsInitialized = false;
    private int marginBottomBegin;
    private int marginBottomEnd;

    private ViewTreeObserver.OnPreDrawListener preDrawListener;

    LinearLayoutVerticalScaleAnimation(float beginY, float endY,
            LinearLayout linearLayout) {
        super(1f, 1f, beginY, endY);

        this.view = linearLayout;
        this.layoutParams = (LinearLayout.LayoutParams) linearLayout.getLayoutParams();

        this.beginY = beginY;
        this.endY = endY;
        this.originalBottomMargin = layoutParams.bottomMargin;

        if (view.getHeight() != 0) {
            expandedHeight = view.getHeight();
            initializeMargins();
        }
    }

    private void initializeMargins() {
        final int beginHeight = (int) (expandedHeight * beginY);
        final int endHeight = (int) (expandedHeight * endY);

        marginBottomBegin = beginHeight + originalBottomMargin - expandedHeight;
        marginBottomEnd = endHeight + originalBottomMargin - expandedHeight;
        marginsInitialized = true;
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);     

        if (!marginsInitialized && preDrawListener == null) {                       
            // To avoid glitches, don't draw until we've initialized everything.
            preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {                    
                    if (view.getHeight() != 0) {
                        expandedHeight = view.getHeight();
                        initializeMargins();
                        adjustViewBounds(0f);
                        view.getViewTreeObserver().removeOnPreDrawListener(this);                               
                    }

                    return false;
                }
            };

            view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);                   
        }

        if (interpolatedTime < 1.0f && view.getVisibility() != View.VISIBLE) {          
            view.setVisibility(View.VISIBLE);           
        }

        if (marginsInitialized) {           
            if (interpolatedTime < 1.0f) {
                adjustViewBounds(interpolatedTime);
            } else if (endY <= 0f && view.getVisibility() != View.GONE) {               
                view.setVisibility(View.GONE);
            }
        }
    }

    private void adjustViewBounds(float interpolatedTime) {
        layoutParams.bottomMargin = 
                marginBottomBegin + (int) ((marginBottomEnd - marginBottomBegin) * interpolatedTime);       

        view.getParent().requestLayout();
    }
}
Community
  • 1
  • 1
Learn OpenGL ES
  • 4,759
  • 1
  • 36
  • 38
  • Is it possible to use this to first collapse an existing LinearLayout and then expand the same LinearLayout again afterwards? When I try to do this, it just collapses and won't expand again (probably because the height of the view is now 0 or something like that). – AHaahr Jun 27 '13 at 21:50
  • I found that it works more reliably when the linear layout contains more than one view. If it contains only one view, then it won't always expand. – Learn OpenGL ES Aug 13 '13 at 19:58