Android expandable pull down bar - SlidingDrawer like [duplicate] - android
Let's say I have a vertical linearLayout with :
[v1]
[v2]
By default v1 has visibily = GONE. I would like to show v1 with an expand animation and push down v2 at the same time.
I tried something like this:
Animation a = new Animation()
{
int initialHeight;
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final int newHeight = (int)(initialHeight * interpolatedTime);
v.getLayoutParams().height = newHeight;
v.requestLayout();
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
initialHeight = height;
}
#Override
public boolean willChangeBounds() {
return true;
}
};
But with this solution, I have a blink when the animation starts. I think it's caused by v1 displaying full size before the animation is applied.
With javascript, this is one line of jQuery! Any simple way to do this with android?
I see that this question became popular so I post my actual solution. The main advantage is that you don't have to know the expanded height to apply the animation and once the view is expanded, it adapts height if content changes. It works great for me.
public static void expand(final View v) {
int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getWidth(), View.MeasureSpec.EXACTLY);
int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
v.measure(matchParentMeasureSpec, wrapContentMeasureSpec);
final int targetHeight = v.getMeasuredHeight();
// Older versions of android (pre API 21) cancel animations for views with a height of 0.
v.getLayoutParams().height = 1;
v.setVisibility(View.VISIBLE);
Animation a = new Animation()
{
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
v.getLayoutParams().height = interpolatedTime == 1
? LayoutParams.WRAP_CONTENT
: (int)(targetHeight * interpolatedTime);
v.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
// Expansion speed of 1dp/ms
a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
public static void collapse(final View v) {
final int initialHeight = v.getMeasuredHeight();
Animation a = new Animation()
{
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if(interpolatedTime == 1){
v.setVisibility(View.GONE);
}else{
v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime);
v.requestLayout();
}
}
#Override
public boolean willChangeBounds() {
return true;
}
};
// Collapse speed of 1dp/ms
a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
As mentioned by #Jefferson in the comments, you can obtain a smoother animation by changing the duration (and hence the speed) of the animation. Currently, it has been set at a speed of 1dp/ms
I stumbled over the same problem today and I guess the real solution to this question is this
<LinearLayout android:id="#+id/container"
android:animateLayoutChanges="true"
...
/>
You will have to set this property for all topmost layouts, which are involved in the shift. If you now set the visibility of one layout to GONE, the other will take the space as the disappearing one is releasing it. There will be a default animation which is some kind of "fading out", but I think you can change this - but the last one I have not tested, for now.
If using this in a RecyclerView item, set the visibility of the view to expand/collapse in onBindViewHolder and call notifyItemChanged(position) to trigger the transformation.
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
...
holder.list.visibility = data[position].listVisibility
holder.expandCollapse.setOnClickListener {
data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE
notifyItemChanged(position)
}
}
If you perform expensive operations in onBindViewHolder you can optimize for partial changes using notifyItemChanged(position, payload)
private const val UPDATE_LIST_VISIBILITY = 1
override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(UPDATE_LIST_VISIBILITY)) {
holder.list.visibility = data[position].listVisibility
} else {
onBindViewHolder(holder, position)
}
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
...
holder.list.visibility = data[position].listVisibility
holder.expandCollapse.setOnClickListener {
data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE
notifyItemChanged(position, UPDATE_LIST_VISIBILITY)
}
}
I was trying to do what I believe was a very similar animation and found an elegant solution. This code assumes that you are always going from 0->h or h->0 (h being the maximum height). The three constructor parameters are view = the view to be animated (in my case, a webview), targetHeight = the maximum height of the view, and down = a boolean which specifies the direction (true = expanding, false = collapsing).
public class DropDownAnim extends Animation {
private final int targetHeight;
private final View view;
private final boolean down;
public DropDownAnim(View view, int targetHeight, boolean down) {
this.view = view;
this.targetHeight = targetHeight;
this.down = down;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newHeight;
if (down) {
newHeight = (int) (targetHeight * interpolatedTime);
} else {
newHeight = (int) (targetHeight * (1 - interpolatedTime));
}
view.getLayoutParams().height = newHeight;
view.requestLayout();
}
#Override
public void initialize(int width, int height, int parentWidth,
int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
#Override
public boolean willChangeBounds() {
return true;
}
}
I took #LenaYan 's solution that didn't work properly
to me (because it was transforming the View to a 0 height view before collapsing and/or expanding) and made some changes.
Now it works great, by taking the View's previous height and start expanding with this size. Collapsing is the same.
You can simply copy and paste the code below:
public static void expand(final View v, int duration, int targetHeight) {
int prevHeight = v.getHeight();
v.setVisibility(View.VISIBLE);
ValueAnimator valueAnimator = ValueAnimator.ofInt(prevHeight, targetHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(duration);
valueAnimator.start();
}
public static void collapse(final View v, int duration, int targetHeight) {
int prevHeight = v.getHeight();
ValueAnimator valueAnimator = ValueAnimator.ofInt(prevHeight, targetHeight);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(duration);
valueAnimator.start();
}
Usage:
//Expanding the View
expand(yourView, 2000, 200);
// Collapsing the View
collapse(yourView, 2000, 100);
Easy enough!
Thanks LenaYan for the initial code!
An alternative is to use a scale animation with the following scaling factors for expanding:
ScaleAnimation anim = new ScaleAnimation(1, 1, 0, 1);
and for collapsing:
ScaleAnimation anim = new ScaleAnimation(1, 1, 1, 0);
#Tom Esterez's answer, but updated to use view.measure() properly per Android getMeasuredHeight returns wrong values !
// http://easings.net/
Interpolator easeInOutQuart = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f);
public static Animation expand(final View view) {
int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY);
int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(matchParentMeasureSpec, wrapContentMeasureSpec);
final int targetHeight = view.getMeasuredHeight();
// Older versions of android (pre API 21) cancel animations for views with a height of 0 so use 1 instead.
view.getLayoutParams().height = 1;
view.setVisibility(View.VISIBLE);
Animation animation = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
view.getLayoutParams().height = interpolatedTime == 1
? ViewGroup.LayoutParams.WRAP_CONTENT
: (int) (targetHeight * interpolatedTime);
view.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
animation.setInterpolator(easeInOutQuart);
animation.setDuration(computeDurationFromHeight(view));
view.startAnimation(animation);
return animation;
}
public static Animation collapse(final View view) {
final int initialHeight = view.getMeasuredHeight();
Animation a = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1) {
view.setVisibility(View.GONE);
} else {
view.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime);
view.requestLayout();
}
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setInterpolator(easeInOutQuart);
int durationMillis = computeDurationFromHeight(view);
a.setDuration(durationMillis);
view.startAnimation(a);
return a;
}
private static int computeDurationFromHeight(View view) {
// 1dp/ms * multiplier
return (int) (view.getMeasuredHeight() / view.getContext().getResources().getDisplayMetrics().density);
}
Ok, I just found a VERY ugly solution :
public static Animation expand(final View v, Runnable onEnd) {
try {
Method m = v.getClass().getDeclaredMethod("onMeasure", int.class, int.class);
m.setAccessible(true);
m.invoke(
v,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(((View)v.getParent()).getMeasuredHeight(), MeasureSpec.AT_MOST)
);
} catch (Exception e){
Log.e("test", "", e);
}
final int initialHeight = v.getMeasuredHeight();
Log.d("test", "initialHeight="+initialHeight);
v.getLayoutParams().height = 0;
v.setVisibility(View.VISIBLE);
Animation a = new Animation()
{
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final int newHeight = (int)(initialHeight * interpolatedTime);
v.getLayoutParams().height = newHeight;
v.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration(5000);
v.startAnimation(a);
return a;
}
Feel free to propose a better solution !
public static void expand(final View v, int duration, int targetHeight) {
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
v.getLayoutParams().height = 0;
v.setVisibility(View.VISIBLE);
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, targetHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(duration);
valueAnimator.start();
}
public static void collapse(final View v, int duration, int targetHeight) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, targetHeight);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(duration);
valueAnimator.start();
}
If you don't want to expand or collapse all the way - here is a simple HeightAnimation -
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class HeightAnimation extends Animation {
protected final int originalHeight;
protected final View view;
protected float perValue;
public HeightAnimation(View view, int fromHeight, int toHeight) {
this.view = view;
this.originalHeight = fromHeight;
this.perValue = (toHeight - fromHeight);
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
view.getLayoutParams().height = (int) (originalHeight + perValue * interpolatedTime);
view.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
}
Usage:
HeightAnimation heightAnim = new HeightAnimation(view, view.getHeight(), viewPager.getHeight() - otherView.getHeight());
heightAnim.setDuration(1000);
view.startAnimation(heightAnim);
I adapted the currently accepted answer by Tom Esterez, which worked but had a choppy and not very smooth animation. My solution basically replaces the Animation with a ValueAnimator, which can be fitted with an Interpolator of your choice to achieve various effects such as overshoot, bounce, accelerate, etc.
This solution works great with views that have a dynamic height (i.e. using WRAP_CONTENT), as it first measures the actual required height and then animates to that height.
public static void expand(final View v) {
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int targetHeight = v.getMeasuredHeight();
// Older versions of android (pre API 21) cancel animations for views with a height of 0.
v.getLayoutParams().height = 1;
v.setVisibility(View.VISIBLE);
ValueAnimator va = ValueAnimator.ofInt(1, targetHeight);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (Integer) animation.getAnimatedValue();
v.requestLayout();
}
});
va.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
v.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
}
#Override public void onAnimationStart(Animator animation) {}
#Override public void onAnimationCancel(Animator animation) {}
#Override public void onAnimationRepeat(Animator animation) {}
});
va.setDuration(300);
va.setInterpolator(new OvershootInterpolator());
va.start();
}
public static void collapse(final View v) {
final int initialHeight = v.getMeasuredHeight();
ValueAnimator va = ValueAnimator.ofInt(initialHeight, 0);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (Integer) animation.getAnimatedValue();
v.requestLayout();
}
});
va.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
v.setVisibility(View.GONE);
}
#Override public void onAnimationStart(Animator animation) {}
#Override public void onAnimationCancel(Animator animation) {}
#Override public void onAnimationRepeat(Animator animation) {}
});
va.setDuration(300);
va.setInterpolator(new DecelerateInterpolator());
va.start();
}
You then simply call expand( myView ); or collapse( myView );.
Making use of Kotlin Extension Functions this is tested and shortest answer
Just call animateVisibility(expand/collapse) on any View.
fun View.animateVisibility(setVisible: Boolean) {
if (setVisible) expand(this) else collapse(this)
}
private fun expand(view: View) {
view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val initialHeight = 0
val targetHeight = view.measuredHeight
// Older versions of Android (pre API 21) cancel animations for views with a height of 0.
//v.getLayoutParams().height = 1;
view.layoutParams.height = 0
view.visibility = View.VISIBLE
animateView(view, initialHeight, targetHeight)
}
private fun collapse(view: View) {
val initialHeight = view.measuredHeight
val targetHeight = 0
animateView(view, initialHeight, targetHeight)
}
private fun animateView(v: View, initialHeight: Int, targetHeight: Int) {
val valueAnimator = ValueAnimator.ofInt(initialHeight, targetHeight)
valueAnimator.addUpdateListener { animation ->
v.layoutParams.height = animation.animatedValue as Int
v.requestLayout()
}
valueAnimator.addListener(object : Animator.AnimatorListener {
override fun onAnimationEnd(animation: Animator) {
v.layoutParams.height = targetHeight
}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
valueAnimator.duration = 300
valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.start()
}
Adding to Tom Esterez's excellent answer and Erik B's excellent update to it, I thought I'd post my own take, compacting the expand and contract methods into one. This way, you could for example have an action like this...
button.setOnClickListener(v -> expandCollapse(view));
... which calls the method below and letting it figure out what to do after each onClick()...
public static void expandCollapse(View view) {
boolean expand = view.getVisibility() == View.GONE;
Interpolator easeInOutQuart = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f);
view.measure(
View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
);
int height = view.getMeasuredHeight();
int duration = (int) (height/view.getContext().getResources().getDisplayMetrics().density);
Animation animation = new Animation() {
#Override protected void applyTransformation(float interpolatedTime, Transformation t) {
if (expand) {
view.getLayoutParams().height = 1;
view.setVisibility(View.VISIBLE);
if (interpolatedTime == 1) {
view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
view.getLayoutParams().height = (int) (height * interpolatedTime);
}
view.requestLayout();
} else {
if (interpolatedTime == 1) {
view.setVisibility(View.GONE);
} else {
view.getLayoutParams().height = height - (int) (height * interpolatedTime);
view.requestLayout();
}
}
}
#Override public boolean willChangeBounds() {
return true;
}
};
animation.setInterpolator(easeInOutQuart);
animation.setDuration(duration);
view.startAnimation(animation);
}
For Smooth animation please use Handler with run method.....And Enjoy Expand /Collapse animation
class AnimUtils{
public void expand(final View v) {
int ANIMATION_DURATION=500;//in milisecond
v.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
final int targtetHeight = v.getMeasuredHeight();
v.getLayoutParams().height = 0;
v.setVisibility(View.VISIBLE);
Animation a = new Animation()
{
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
v.getLayoutParams().height = interpolatedTime == 1
? LayoutParams.WRAP_CONTENT
: (int)(targtetHeight * interpolatedTime);
v.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
// 1dp/ms
a.setDuration(ANIMATION_DURATION);
// a.setDuration((int)(targtetHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
public void collapse(final View v) {
final int initialHeight = v.getMeasuredHeight();
Animation a = new Animation()
{
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if(interpolatedTime == 1){
v.setVisibility(View.GONE);
}else{
v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime);
v.requestLayout();
}
}
#Override
public boolean willChangeBounds() {
return true;
}
};
// 1dp/ms
a.setDuration(ANIMATION_DURATION);
// a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density));
v.startAnimation(a);
}
}
And Call using this code:
private void setAnimationOnView(final View inactive ) {
//I am applying expand and collapse on this TextView ...You can use your view
//for expand animation
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
new AnimationUtililty().expand(inactive);
}
}, 1000);
//For collapse
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
new AnimationUtililty().collapse(inactive);
//inactive.setVisibility(View.GONE);
}
}, 8000);
}
Other solution is:
public void expandOrCollapse(final View v,String exp_or_colpse) {
TranslateAnimation anim = null;
if(exp_or_colpse.equals("expand"))
{
anim = new TranslateAnimation(0.0f, 0.0f, -v.getHeight(), 0.0f);
v.setVisibility(View.VISIBLE);
}
else{
anim = new TranslateAnimation(0.0f, 0.0f, 0.0f, -v.getHeight());
AnimationListener collapselistener= new AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationRepeat(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
v.setVisibility(View.GONE);
}
};
anim.setAnimationListener(collapselistener);
}
// To Collapse
//
anim.setDuration(300);
anim.setInterpolator(new AccelerateInterpolator(0.5f));
v.startAnimation(anim);
}
I would like to add something to the very helpful answer above. If you don't know the height you'll end up with since your views .getHeight() returns 0 you can do the following to get the height:
contentView.measure(DUMMY_HIGH_DIMENSION, DUMMY_HIGH_DIMENSION);
int finalHeight = view.getMeasuredHeight();
Where DUMMY_HIGH_DIMENSIONS is the width/height (in pixels) your view is constrained to ... having this a huge number is reasonable when the view is encapsulated with a ScrollView.
This is a snippet that I used to resize the width of a view (LinearLayout) with animation.
The code is supposed to do expand or shrink according the target size. If you want a fill_parent width, you will have to pass the parent .getMeasuredWidth as target width while setting the flag to true.
Hope it helps some of you.
public class WidthResizeAnimation extends Animation {
int targetWidth;
int originaltWidth;
View view;
boolean expand;
int newWidth = 0;
boolean fillParent;
public WidthResizeAnimation(View view, int targetWidth, boolean fillParent) {
this.view = view;
this.originaltWidth = this.view.getMeasuredWidth();
this.targetWidth = targetWidth;
newWidth = originaltWidth;
if (originaltWidth > targetWidth) {
expand = false;
} else {
expand = true;
}
this.fillParent = fillParent;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (expand && newWidth < targetWidth) {
newWidth = (int) (newWidth + (targetWidth - newWidth) * interpolatedTime);
}
if (!expand && newWidth > targetWidth) {
newWidth = (int) (newWidth - (newWidth - targetWidth) * interpolatedTime);
}
if (fillParent && interpolatedTime == 1.0) {
view.getLayoutParams().width = -1;
} else {
view.getLayoutParams().width = newWidth;
}
view.requestLayout();
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
#Override
public boolean willChangeBounds() {
return true;
}
}
Yes, I agreed with the above comments. And indeed, it does seem like the right (or at least the easiest?) thing to do is to specify (in XML) an initial layout height of "0px" -- and then you can pass in another argument for "toHeight" (i.e. the "final height") to the constructor of your custom Animation sub-class, e.g. in the example above, it would look something like so:
public DropDownAnim( View v, int toHeight ) { ... }
Anyways, hope that helps! :)
combined solutions from #Tom Esterez and #Geraldo Neto
public static void expandOrCollapseView(View v,boolean expand){
if(expand){
v.measure(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT);
final int targetHeight = v.getMeasuredHeight();
v.getLayoutParams().height = 0;
v.setVisibility(View.VISIBLE);
ValueAnimator valueAnimator = ValueAnimator.ofInt(targetHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(500);
valueAnimator.start();
}
else
{
final int initialHeight = v.getMeasuredHeight();
ValueAnimator valueAnimator = ValueAnimator.ofInt(initialHeight,0);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
v.getLayoutParams().height = (int) animation.getAnimatedValue();
v.requestLayout();
if((int)animation.getAnimatedValue() == 0)
v.setVisibility(View.GONE);
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.setDuration(500);
valueAnimator.start();
}
}
//sample usage
expandOrCollapseView((Your ViewGroup),(Your ViewGroup).getVisibility()!=View.VISIBLE);
Here is my solution. I think it is simpler. It only expands the view but can easy be extended.
public class WidthExpandAnimation extends Animation
{
int _targetWidth;
View _view;
public WidthExpandAnimation(View view)
{
_view = view;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t)
{
if (interpolatedTime < 1.f)
{
int newWidth = (int) (_targetWidth * interpolatedTime);
_view.layout(_view.getLeft(), _view.getTop(),
_view.getLeft() + newWidth, _view.getBottom());
}
else
_view.requestLayout();
}
#Override
public void initialize(int width, int height, int parentWidth, int parentHeight)
{
super.initialize(width, height, parentWidth, parentHeight);
_targetWidth = width;
}
#Override
public boolean willChangeBounds() {
return true;
}
}
I think the easiest solution is to set android:animateLayoutChanges="true" to your LinearLayout and then just show/hide view by seting its visibility. Works like a charm, but you have no controll on the animation duration
You can use Transition or Animator that changes visibility of section to be expanded/collapsed, or ConstraintSet with different layouts.
Easiest one is to use motionLayout with 2 different layouts and constraintSets to change from one layout to another on button click. You can change between layouts with
val constraintSet = ConstraintSet()
constraintSet.clone(this, R.layout.layout_collapsed)
val transition = ChangeBounds()
transition.interpolator = AccelerateInterpolator(1.0f)
transition.setDuration(300)
TransitionManager.beginDelayedTransition(YOUR_VIEW, transition)
constraintSet.applyTo(YOUR_VIEW)
With Transition api
RotateX.kt
I created the one in gif using Transitions api that change rotationX.
class RotateX : Transition {
#Keep
constructor() : super()
#Keep
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun getTransitionProperties(): Array<String> {
return TRANSITION_PROPERTIES
}
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun createAnimator(
sceneRoot: ViewGroup,
startValues: TransitionValues?,
endValues: TransitionValues?
): Animator? {
if (startValues == null || endValues == null) return null
val startRotation = startValues.values[PROP_ROTATION] as Float
val endRotation = endValues.values[PROP_ROTATION] as Float
if (startRotation == endRotation) return null
val view = endValues.view
// ensure the pivot is set
view.pivotX = view.width / 2f
view.pivotY = view.height / 2f
return ObjectAnimator.ofFloat(view, View.ROTATION_X, startRotation, endRotation)
}
private fun captureValues(transitionValues: TransitionValues) {
val view = transitionValues.view
if (view == null || view.width <= 0 || view.height <= 0) return
transitionValues.values[PROP_ROTATION] = view.rotationX
}
companion object {
private const val PROP_ROTATION = "iosched:rotate:rotation"
private val TRANSITION_PROPERTIES = arrayOf(PROP_ROTATION)
}
}
create xml file that targets expand button
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:interpolator/fast_out_slow_in">
<transition class="com.smarttoolfactory.tutorial3_1transitions.transition.RotateX">
<targets>
<target android:targetId="#id/ivExpand" />
</targets>
</transition>
<autoTransition android:duration="200" />
</transitionSet>
My layout to be expanded or collapsed
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="2dp"
android:clickable="true"
android:focusable="true"
android:transitionName="#string/transition_card_view"
app:cardCornerRadius="0dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/ivAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="#drawable/avatar_1_raster" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/ivExpand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_baseline_expand_more_24" />
<TextView
android:id="#+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:text="Some Title"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="#+id/ivAvatar"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="#+id/tvTitle"
app:layout_constraintTop_toBottomOf="#id/tvTitle"
tools:text="Tuesday 7pm" />
<TextView
android:id="#+id/tvBody"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
android:text="#string/bacon_ipsum_short"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="#+id/ivAvatar"
app:layout_constraintTop_toBottomOf="#id/tvDate" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
android:overScrollMode="never"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvBody"
tools:listitem="#layout/item_image_destination" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
And set up visibility of items to collapse or expand
private fun setUpExpandedStatus() {
if (isExpanded) {
binding.recyclerView.visibility = View.VISIBLE
binding.ivExpand.rotationX = 180f
} else {
binding.recyclerView.visibility = View.GONE
binding.ivExpand.rotationX = 0f
}
}
And start transition with
val transition = TransitionInflater.from(itemView.context)
.inflateTransition(R.transition.icon_expand_toggle)
TransitionManager.beginDelayedTransition(parent, transition)
isExpanded = !isExpanded
setUpExpandedStatus()
I created animation and transitions samples including the one on the gif, you can check them out there.
You are on the right track. Make sure you have v1 set to have a layout height of zero right before the animation starts. You want to initialize your setup to look like the first frame of the animation before starting the animation.
This was my solution, my ImageView grows from 100% to 200% and return to his original size, using two animation files inside res/anim/ folder
anim_grow.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/accelerate_interpolator">
<scale
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:duration="3000"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="2000" />
</set>
anim_shrink.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="#android:anim/accelerate_interpolator">
<scale
android:fromXScale="2.0"
android:toXScale="1.0"
android:fromYScale="2.0"
android:toYScale="1.0"
android:duration="3000"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="2000" />
</set>
Send an ImageView to my method setAnimationGrowShrink()
ImageView img1 = (ImageView)findViewById(R.id.image1);
setAnimationGrowShrink(img1);
setAnimationGrowShrink() method:
private void setAnimationGrowShrink(final ImageView imgV){
final Animation animationEnlarge = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.anim_grow);
final Animation animationShrink = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.anim_shrink);
imgV.startAnimation(animationEnlarge);
animationEnlarge.setAnimationListener(new AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationRepeat(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
imgV.startAnimation(animationShrink);
}
});
animationShrink.setAnimationListener(new AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationRepeat(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
imgV.startAnimation(animationEnlarge);
}
});
}
This is a proper working solution, I have tested it:
Exapnd:
private void expand(View v) {
v.setVisibility(View.VISIBLE);
v.measure(View.MeasureSpec.makeMeasureSpec(PARENT_VIEW.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final int targetHeight = v.getMeasuredHeight();
mAnimator = slideAnimator(0, targetHeight);
mAnimator.setDuration(800);
mAnimator.start();
}
Collapse:
private void collapse(View v) {
int finalHeight = v.getHeight();
mAnimator = slideAnimator(finalHeight, 0);
mAnimator.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animator) {
}
#Override
public void onAnimationEnd(Animator animator) {
//Height=0, but it set visibility to GONE
llDescp.setVisibility(View.GONE);
}
#Override
public void onAnimationCancel(Animator animator) {
}
#Override
public void onAnimationRepeat(Animator animator) {
}
});
mAnimator.start();
}
Value Animator:
private ValueAnimator slideAnimator(int start, int end) {
ValueAnimator mAnimator = ValueAnimator.ofInt(start, end);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//Update Height
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams = llDescp.getLayoutParams();
layoutParams.height = value;
v.setLayoutParams(layoutParams);
}
});
return mAnimator;
}
View v is the view to be animated, PARENT_VIEW is the container view containing the view.
Based on solutions by #Tom Esterez and #Seth Nelson (top 2) I simlified them. As well as original solutions it doesn't depend on Developer options (animation settings).
private void resizeWithAnimation(final View view, int duration, final int targetHeight) {
final int initialHeight = view.getMeasuredHeight();
final int distance = targetHeight - initialHeight;
view.setVisibility(View.VISIBLE);
Animation a = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1 && targetHeight == 0) {
view.setVisibility(View.GONE);
}
view.getLayoutParams().height = (int) (initialHeight + distance * interpolatedTime);
view.requestLayout();
}
#Override
public boolean willChangeBounds() {
return true;
}
};
a.setDuration(duration);
view.startAnimation(a);
}
This is really simple with droidQuery. For starts, consider this layout:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<LinearLayout
android:id="#+id/v1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View 1" />
</LinearLayout>
<LinearLayout
android:id="#+id/v2"
android:layout_width="wrap_content"
android:layout_height="0dp" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View 2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View 3" />
</LinearLayout>
</LinearLayout>
We can animate the height to the desired value - say 100dp - using the following code:
//convert 100dp to pixel value
int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
Then use droidQuery to animate. The simplest way is with this:
$.animate("{ height: " + height + "}", new AnimationOptions());
To make the animation more appealing, consider adding an easing:
$.animate("{ height: " + height + "}", new AnimationOptions().easing($.Easing.BOUNCE));
You can also change the duration on AnimationOptions using the duration() method, or handle what happens when the animation ends. For a complex example, try:
$.animate("{ height: " + height + "}", new AnimationOptions().easing($.Easing.BOUNCE)
.duration(1000)
.complete(new Function() {
#Override
public void invoke($ d, Object... args) {
$.toast(context, "finished", Toast.LENGTH_SHORT);
}
}));
Best solution for expand/collapse view's:
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
View view = buttonView.getId() == R.id.tb_search ? fSearch : layoutSettings;
transform(view, 200, isChecked
? ViewGroup.LayoutParams.WRAP_CONTENT
: 0);
}
public static void transform(final View v, int duration, int targetHeight) {
int prevHeight = v.getHeight();
v.setVisibility(View.VISIBLE);
ValueAnimator animator;
if (targetHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
animator = ValueAnimator.ofInt(prevHeight, v.getMeasuredHeight());
} else {
animator = ValueAnimator.ofInt(prevHeight, targetHeight);
}
animator.addUpdateListener(animation -> {
v.getLayoutParams().height = (animation.getAnimatedFraction() == 1.0f)
? targetHeight
: (int) animation.getAnimatedValue();
v.requestLayout();
});
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(duration);
animator.start();
}
You can use a ViewPropertyAnimator with a slight twist. To collapse, scale the view to a height of 1 pixel, then hide it. To expand, show it, then expand it to its height.
private void collapse(final View view) {
view.setPivotY(0);
view.animate().scaleY(1/view.getHeight()).setDuration(1000).withEndAction(new Runnable() {
#Override public void run() {
view.setVisibility(GONE);
}
});
}
private void expand(View view, int height) {
float scaleFactor = height / view.getHeight();
view.setVisibility(VISIBLE);
view.setPivotY(0);
view.animate().scaleY(scaleFactor).setDuration(1000);
}
The pivot tells the view where to scale from, default is in the middle. The duration is optional (default = 1000). You can also set the interpolator to use, like .setInterpolator(new AccelerateDecelerateInterpolator())
I created version in which you don't need to specify layout height, hence it's a lot easier and cleaner to use. The solution is to get the height in the first frame of the animation (it's available at that moment, at least during my tests). This way you can provide a View with an arbitrary height and bottom margin.
There's also one little hack in the constructor - the bottom margin is set to -10000 so that the view stays hidden before the transformation (prevents flicker).
public class ExpandAnimation extends Animation {
private View mAnimatedView;
private ViewGroup.MarginLayoutParams mViewLayoutParams;
private int mMarginStart, mMarginEnd;
public ExpandAnimation(View view) {
mAnimatedView = view;
mViewLayoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
mMarginEnd = mViewLayoutParams.bottomMargin;
mMarginStart = -10000; //hide before viewing by settings very high negative bottom margin (hack, but works nicely)
mViewLayoutParams.bottomMargin = mMarginStart;
mAnimatedView.setLayoutParams(mViewLayoutParams);
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
//view height is already known when the animation starts
if(interpolatedTime==0){
mMarginStart = -mAnimatedView.getHeight();
}
mViewLayoutParams.bottomMargin = (int)((mMarginEnd-mMarginStart) * interpolatedTime)+mMarginStart;
mAnimatedView.setLayoutParams(mViewLayoutParams);
}
}
Use ValueAnimator:
ValueAnimator expandAnimation = ValueAnimator.ofInt(mainView.getHeight(), 400);
expandAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(final ValueAnimator animation) {
int height = (Integer) animation.getAnimatedValue();
RelativeLayout.LayoutParams lp = (LayoutParams) mainView.getLayoutParams();
lp.height = height;
}
});
expandAnimation.setDuration(500);
expandAnimation.start();
public static void slide(View v, int speed, int pos) {
v.animate().setDuration(speed);
v.animate().translationY(pos);
v.animate().start();
}
// slide down
slide(yourView, 250, yourViewHeight);
// slide up
slide(yourView, 250, 0);
Related
more button between two recycler view
I have two recycler views in one screen and in between of them, there is one more button like this. I want to update the height of recycler view of the upper one to MATCH PARENT with animation, So I had tried this. binding.moreLl.setOnClickListener(new View.OnClickListener() { #Override public void onClick(View v) { ValueAnimator anim = ValueAnimator.ofInt(); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = binding.fragmentWalletRv.getLayoutParams(); layoutParams.height = val; binding.fragmentWalletRv.setLayoutParams(layoutParams); } }); anim.setDuration(500); anim.start(); } }); But unfortunately i cant get the expected result. So please guide me how i can do that thing. Thanks in advance.
If you just want to have an animation when layout changes, no need to use a custom animation. Try adding default layout change animation by enabling android:animateLayoutChanges="true" to the parent layout in xml. On your more button click, only change the LayoutParams of the Recyclerview. To change the speed of animation LinearLayout layout = mContentView.findViewById(R.id.parent_layout); LayoutTransition lt = layout.getLayoutTransition(); lt.setDuration(2000);
public class SlideAnimation extends Animation { int mFromHeight; int mToHeight; View mView; public SlideAnimation(View view, int fromHeight, int toHeight) { this.mView = view; this.mFromHeight = fromHeight; this.mToHeight = toHeight; } #Override protected void applyTransformation(float interpolatedTime, Transformation transformation) { int newHeight; if (mView.getHeight() != mToHeight) { newHeight = (int) (mFromHeight + ((mToHeight - mFromHeight) * interpolatedTime)); mView.getLayoutParams().height = newHeight; mView.requestLayout(); } } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } #Override public boolean willChangeBounds() { return true; } } Use above animation like below rv1 = findViewById(R.id.rv1); Animation animation = new SlideAnimation(rv1, rv1.getHeight(), 500); animation.setInterpolator(new AccelerateInterpolator()); animation.setDuration(500); rv1.setAnimation(animation); rv1.startAnimation(animation);
How to animate a layout within a CardView (i.e. SlideUp & SlideDown) in android
I have a RelativeLayout having the visibility gone containing 3 action buttons. I want to animate this layout to SlideUp and SlideDown on clicking on an dropdown image.
Here is a utility class that could do this for you: public class AnimExpandCollapse { public static void expand(final View v) { v.measure( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT ); final int targetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LayoutParams.WRAP_CONTENT : (int)(targetHeight * interpolatedTime); v.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } public static void setViewToWrappedHeight(View v) { v.getLayoutParams().height = LayoutParams.WRAP_CONTENT; } public static void collapse(final View v) { collapse(v, null, true, 0); } public static void collapse(final View v, Animation.AnimationListener animationListener, boolean autoDuration, int duration) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime == 1){ v.setVisibility(View.GONE); } else { v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime); v.requestLayout(); } } #Override public boolean willChangeBounds() { return true; } }; // 1dp/ms if (autoDuration) { a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density)); } else { a.setDuration(duration); } if (animationListener!=null) a.setAnimationListener(animationListener); v.startAnimation(a); }}
animate a view from a particular point to fill whole screen and the re size it back to that point
I Know how to animate a view to whole screen and animate back to its original size. here is the link to do this Re sizing through animation But the problem is that this technique works only when if my view is place at the start of the screen. What i want is that if my view of height and width (50,50) is placed in center of screen below some button and i click that view it should animate to fill the whole screen and when clicked again it animates back to its original size (50,50)
If you're using LinearLayout, try to set AnimationListener for this animation and toggle the visibility of the button appropriately. Set button's visibility to View.GONE onAnimationStart when 'expanding' view and to View.VISIBLE onAnimationEnd when 'collapsing' it. Using RelativeLayout can be the solution for this problem too. For animation: public class ExpandCollapseViewAnimation extends Animation { int targetWidth; int targetHeight; int initialWidth; int initialHeight; boolean expand; View view; public ExpandCollapseViewAnimation(View view, int targetWidth, int targetHeight ,boolean expand) { this.view = view; this.targetWidth = targetWidth; this.targetHeight = targetHeight; this.initialWidth = view.getWidth(); this.initialHeight = view.getHeight(); this.expand = expand; } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { int newWidth, newHeight; if (expand) { newWidth = this.initialWidth + (int) ((this.targetWidth - this.initialWidth) * interpolatedTime); newHeight = this.initialHeight + (int) ((this.targetHeight - this.initialHeight) * interpolatedTime); } else { newWidth = this.initialWidth - (int) ((this.initialWidth - this.targetWidth) * interpolatedTime); newHeight = this.initialHeight - (int) ((this.initialHeight - this.targetHeight) * interpolatedTime); } view.getLayoutParams().width = newWidth; view.getLayoutParams().height = newHeight; view.requestLayout(); } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } #Override public boolean willChangeBounds() { return true; } } And layout XML: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="#+id/btn" android:layout_width="50dp" android:layout_height="50dp" android:layout_gravity="center_horizontal" android:text="Test"/> <View android:id="#+id/centered_view" android:layout_width="50dp" android:layout_height="50dp" android:clickable="true" android:layout_gravity="center_horizontal" android:background="#FF0000"/> </LinearLayout> This code works: animatedView.setOnClickListener(new View.OnClickListener() { #Override public void onClick(View v) { if (!expand) expandView(); else collapseView(); } }); private void expandView() { expand = true; animatedView.clearAnimation(); Display display = this.getWindowManager().getDefaultDisplay(); int maxWidth = display.getWidth(); int maxHeight = display.getHeight(); ExpandCollapseViewAnimation animation = new ExpandCollapseViewAnimation(animatedView, maxWidth,maxHeight, expand); animation.setDuration(500); animation.setAnimationListener(new AnimationListener() { #Override public void onAnimationStart(Animation animation) { btn.setVisibility(View.GONE); } #Override public void onAnimationRepeat(Animation animation) { } #Override public void onAnimationEnd(Animation animation) { } }); animatedView.startAnimation(animation); animatedView.invalidate(); } private void collapseView() { expand = false; animatedView.clearAnimation(); ExpandCollapseViewAnimation animation = new ExpandCollapseViewAnimation( animatedView, dpToPx(this, 50),dpToPx(this, 50), expand); animation.setDuration(500); animation.setAnimationListener(new AnimationListener() { #Override public void onAnimationStart(Animation animation) { } #Override public void onAnimationRepeat(Animation animation) { } #Override public void onAnimationEnd(Animation animation) { btn.setVisibility(View.VISIBLE); } }); animatedView.startAnimation(animation); animatedView.invalidate(); }
How to make a smooth animation on a list in Android?
I have implemented an animation for a Layout that must flow downwards and upwards. The animation precisely must flow in both low that high. This gridview is located above a ExpandbleListView. So, when I close all the groups the animation is very fluid. When the groups on the list are open, with many elements, however the animation is triggered, it is not as fluid as I would like it to be. I'll post the code of my animation: public void expand(final View v) { v.measure(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); final int targtetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LayoutParams.WRAP_CONTENT : (int) (targtetHeight * interpolatedTime); v.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; Interpolator i = AnimationUtils.loadInterpolator(this, android.R.anim.linear_interpolator); a.setInterpolator(i); a.setDuration(200); v.startAnimation(a); } and : public static void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime == 1) { v.setVisibility(View.GONE); } else { v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); v.requestLayout(); } } #Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration(200); v.startAnimation(a); } How can I do to make the animation smoother? --> Edit: I was wrong code, I corrected
How to make expandable panel layout in android? [duplicate]
Let's say I have a vertical linearLayout with : [v1] [v2] By default v1 has visibily = GONE. I would like to show v1 with an expand animation and push down v2 at the same time. I tried something like this: Animation a = new Animation() { int initialHeight; #Override protected void applyTransformation(float interpolatedTime, Transformation t) { final int newHeight = (int)(initialHeight * interpolatedTime); v.getLayoutParams().height = newHeight; v.requestLayout(); } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); initialHeight = height; } #Override public boolean willChangeBounds() { return true; } }; But with this solution, I have a blink when the animation starts. I think it's caused by v1 displaying full size before the animation is applied. With javascript, this is one line of jQuery! Any simple way to do this with android?
I see that this question became popular so I post my actual solution. The main advantage is that you don't have to know the expanded height to apply the animation and once the view is expanded, it adapts height if content changes. It works great for me. public static void expand(final View v) { int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) v.getParent()).getWidth(), View.MeasureSpec.EXACTLY); int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); v.measure(matchParentMeasureSpec, wrapContentMeasureSpec); final int targetHeight = v.getMeasuredHeight(); // Older versions of android (pre API 21) cancel animations for views with a height of 0. v.getLayoutParams().height = 1; v.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LayoutParams.WRAP_CONTENT : (int)(targetHeight * interpolatedTime); v.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; // Expansion speed of 1dp/ms a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } public static void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if(interpolatedTime == 1){ v.setVisibility(View.GONE); }else{ v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime); v.requestLayout(); } } #Override public boolean willChangeBounds() { return true; } }; // Collapse speed of 1dp/ms a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } As mentioned by #Jefferson in the comments, you can obtain a smoother animation by changing the duration (and hence the speed) of the animation. Currently, it has been set at a speed of 1dp/ms
I stumbled over the same problem today and I guess the real solution to this question is this <LinearLayout android:id="#+id/container" android:animateLayoutChanges="true" ... /> You will have to set this property for all topmost layouts, which are involved in the shift. If you now set the visibility of one layout to GONE, the other will take the space as the disappearing one is releasing it. There will be a default animation which is some kind of "fading out", but I think you can change this - but the last one I have not tested, for now. If using this in a RecyclerView item, set the visibility of the view to expand/collapse in onBindViewHolder and call notifyItemChanged(position) to trigger the transformation. override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { ... holder.list.visibility = data[position].listVisibility holder.expandCollapse.setOnClickListener { data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE notifyItemChanged(position) } } If you perform expensive operations in onBindViewHolder you can optimize for partial changes using notifyItemChanged(position, payload) private const val UPDATE_LIST_VISIBILITY = 1 override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) { if (payloads.contains(UPDATE_LIST_VISIBILITY)) { holder.list.visibility = data[position].listVisibility } else { onBindViewHolder(holder, position) } } override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { ... holder.list.visibility = data[position].listVisibility holder.expandCollapse.setOnClickListener { data[position].listVisibility = if (data[position].listVisibility == View.GONE) View.VISIBLE else View.GONE notifyItemChanged(position, UPDATE_LIST_VISIBILITY) } }
I was trying to do what I believe was a very similar animation and found an elegant solution. This code assumes that you are always going from 0->h or h->0 (h being the maximum height). The three constructor parameters are view = the view to be animated (in my case, a webview), targetHeight = the maximum height of the view, and down = a boolean which specifies the direction (true = expanding, false = collapsing). public class DropDownAnim extends Animation { private final int targetHeight; private final View view; private final boolean down; public DropDownAnim(View view, int targetHeight, boolean down) { this.view = view; this.targetHeight = targetHeight; this.down = down; } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { int newHeight; if (down) { newHeight = (int) (targetHeight * interpolatedTime); } else { newHeight = (int) (targetHeight * (1 - interpolatedTime)); } view.getLayoutParams().height = newHeight; view.requestLayout(); } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } #Override public boolean willChangeBounds() { return true; } }
I took #LenaYan 's solution that didn't work properly to me (because it was transforming the View to a 0 height view before collapsing and/or expanding) and made some changes. Now it works great, by taking the View's previous height and start expanding with this size. Collapsing is the same. You can simply copy and paste the code below: public static void expand(final View v, int duration, int targetHeight) { int prevHeight = v.getHeight(); v.setVisibility(View.VISIBLE); ValueAnimator valueAnimator = ValueAnimator.ofInt(prevHeight, targetHeight); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(duration); valueAnimator.start(); } public static void collapse(final View v, int duration, int targetHeight) { int prevHeight = v.getHeight(); ValueAnimator valueAnimator = ValueAnimator.ofInt(prevHeight, targetHeight); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(duration); valueAnimator.start(); } Usage: //Expanding the View expand(yourView, 2000, 200); // Collapsing the View collapse(yourView, 2000, 100); Easy enough! Thanks LenaYan for the initial code!
An alternative is to use a scale animation with the following scaling factors for expanding: ScaleAnimation anim = new ScaleAnimation(1, 1, 0, 1); and for collapsing: ScaleAnimation anim = new ScaleAnimation(1, 1, 1, 0);
#Tom Esterez's answer, but updated to use view.measure() properly per Android getMeasuredHeight returns wrong values ! // http://easings.net/ Interpolator easeInOutQuart = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f); public static Animation expand(final View view) { int matchParentMeasureSpec = View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY); int wrapContentMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(matchParentMeasureSpec, wrapContentMeasureSpec); final int targetHeight = view.getMeasuredHeight(); // Older versions of android (pre API 21) cancel animations for views with a height of 0 so use 1 instead. view.getLayoutParams().height = 1; view.setVisibility(View.VISIBLE); Animation animation = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { view.getLayoutParams().height = interpolatedTime == 1 ? ViewGroup.LayoutParams.WRAP_CONTENT : (int) (targetHeight * interpolatedTime); view.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; animation.setInterpolator(easeInOutQuart); animation.setDuration(computeDurationFromHeight(view)); view.startAnimation(animation); return animation; } public static Animation collapse(final View view) { final int initialHeight = view.getMeasuredHeight(); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime == 1) { view.setVisibility(View.GONE); } else { view.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); view.requestLayout(); } } #Override public boolean willChangeBounds() { return true; } }; a.setInterpolator(easeInOutQuart); int durationMillis = computeDurationFromHeight(view); a.setDuration(durationMillis); view.startAnimation(a); return a; } private static int computeDurationFromHeight(View view) { // 1dp/ms * multiplier return (int) (view.getMeasuredHeight() / view.getContext().getResources().getDisplayMetrics().density); }
Ok, I just found a VERY ugly solution : public static Animation expand(final View v, Runnable onEnd) { try { Method m = v.getClass().getDeclaredMethod("onMeasure", int.class, int.class); m.setAccessible(true); m.invoke( v, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(((View)v.getParent()).getMeasuredHeight(), MeasureSpec.AT_MOST) ); } catch (Exception e){ Log.e("test", "", e); } final int initialHeight = v.getMeasuredHeight(); Log.d("test", "initialHeight="+initialHeight); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { final int newHeight = (int)(initialHeight * interpolatedTime); v.getLayoutParams().height = newHeight; v.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; a.setDuration(5000); v.startAnimation(a); return a; } Feel free to propose a better solution !
public static void expand(final View v, int duration, int targetHeight) { v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); ValueAnimator valueAnimator = ValueAnimator.ofInt(0, targetHeight); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(duration); valueAnimator.start(); } public static void collapse(final View v, int duration, int targetHeight) { ValueAnimator valueAnimator = ValueAnimator.ofInt(0, targetHeight); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(duration); valueAnimator.start(); }
If you don't want to expand or collapse all the way - here is a simple HeightAnimation - import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; public class HeightAnimation extends Animation { protected final int originalHeight; protected final View view; protected float perValue; public HeightAnimation(View view, int fromHeight, int toHeight) { this.view = view; this.originalHeight = fromHeight; this.perValue = (toHeight - fromHeight); } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { view.getLayoutParams().height = (int) (originalHeight + perValue * interpolatedTime); view.requestLayout(); } #Override public boolean willChangeBounds() { return true; } } Usage: HeightAnimation heightAnim = new HeightAnimation(view, view.getHeight(), viewPager.getHeight() - otherView.getHeight()); heightAnim.setDuration(1000); view.startAnimation(heightAnim);
I adapted the currently accepted answer by Tom Esterez, which worked but had a choppy and not very smooth animation. My solution basically replaces the Animation with a ValueAnimator, which can be fitted with an Interpolator of your choice to achieve various effects such as overshoot, bounce, accelerate, etc. This solution works great with views that have a dynamic height (i.e. using WRAP_CONTENT), as it first measures the actual required height and then animates to that height. public static void expand(final View v) { v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); final int targetHeight = v.getMeasuredHeight(); // Older versions of android (pre API 21) cancel animations for views with a height of 0. v.getLayoutParams().height = 1; v.setVisibility(View.VISIBLE); ValueAnimator va = ValueAnimator.ofInt(1, targetHeight); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (Integer) animation.getAnimatedValue(); v.requestLayout(); } }); va.addListener(new Animator.AnimatorListener() { #Override public void onAnimationEnd(Animator animation) { v.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; } #Override public void onAnimationStart(Animator animation) {} #Override public void onAnimationCancel(Animator animation) {} #Override public void onAnimationRepeat(Animator animation) {} }); va.setDuration(300); va.setInterpolator(new OvershootInterpolator()); va.start(); } public static void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); ValueAnimator va = ValueAnimator.ofInt(initialHeight, 0); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (Integer) animation.getAnimatedValue(); v.requestLayout(); } }); va.addListener(new Animator.AnimatorListener() { #Override public void onAnimationEnd(Animator animation) { v.setVisibility(View.GONE); } #Override public void onAnimationStart(Animator animation) {} #Override public void onAnimationCancel(Animator animation) {} #Override public void onAnimationRepeat(Animator animation) {} }); va.setDuration(300); va.setInterpolator(new DecelerateInterpolator()); va.start(); } You then simply call expand( myView ); or collapse( myView );.
Making use of Kotlin Extension Functions this is tested and shortest answer Just call animateVisibility(expand/collapse) on any View. fun View.animateVisibility(setVisible: Boolean) { if (setVisible) expand(this) else collapse(this) } private fun expand(view: View) { view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val initialHeight = 0 val targetHeight = view.measuredHeight // Older versions of Android (pre API 21) cancel animations for views with a height of 0. //v.getLayoutParams().height = 1; view.layoutParams.height = 0 view.visibility = View.VISIBLE animateView(view, initialHeight, targetHeight) } private fun collapse(view: View) { val initialHeight = view.measuredHeight val targetHeight = 0 animateView(view, initialHeight, targetHeight) } private fun animateView(v: View, initialHeight: Int, targetHeight: Int) { val valueAnimator = ValueAnimator.ofInt(initialHeight, targetHeight) valueAnimator.addUpdateListener { animation -> v.layoutParams.height = animation.animatedValue as Int v.requestLayout() } valueAnimator.addListener(object : Animator.AnimatorListener { override fun onAnimationEnd(animation: Animator) { v.layoutParams.height = targetHeight } override fun onAnimationStart(animation: Animator) {} override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} }) valueAnimator.duration = 300 valueAnimator.interpolator = DecelerateInterpolator() valueAnimator.start() }
Adding to Tom Esterez's excellent answer and Erik B's excellent update to it, I thought I'd post my own take, compacting the expand and contract methods into one. This way, you could for example have an action like this... button.setOnClickListener(v -> expandCollapse(view)); ... which calls the method below and letting it figure out what to do after each onClick()... public static void expandCollapse(View view) { boolean expand = view.getVisibility() == View.GONE; Interpolator easeInOutQuart = PathInterpolatorCompat.create(0.77f, 0f, 0.175f, 1f); view.measure( View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ); int height = view.getMeasuredHeight(); int duration = (int) (height/view.getContext().getResources().getDisplayMetrics().density); Animation animation = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (expand) { view.getLayoutParams().height = 1; view.setVisibility(View.VISIBLE); if (interpolatedTime == 1) { view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; } else { view.getLayoutParams().height = (int) (height * interpolatedTime); } view.requestLayout(); } else { if (interpolatedTime == 1) { view.setVisibility(View.GONE); } else { view.getLayoutParams().height = height - (int) (height * interpolatedTime); view.requestLayout(); } } } #Override public boolean willChangeBounds() { return true; } }; animation.setInterpolator(easeInOutQuart); animation.setDuration(duration); view.startAnimation(animation); }
For Smooth animation please use Handler with run method.....And Enjoy Expand /Collapse animation class AnimUtils{ public void expand(final View v) { int ANIMATION_DURATION=500;//in milisecond v.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); final int targtetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LayoutParams.WRAP_CONTENT : (int)(targtetHeight * interpolatedTime); v.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration(ANIMATION_DURATION); // a.setDuration((int)(targtetHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } public void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if(interpolatedTime == 1){ v.setVisibility(View.GONE); }else{ v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime); v.requestLayout(); } } #Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration(ANIMATION_DURATION); // a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } } And Call using this code: private void setAnimationOnView(final View inactive ) { //I am applying expand and collapse on this TextView ...You can use your view //for expand animation new Handler().postDelayed(new Runnable() { #Override public void run() { new AnimationUtililty().expand(inactive); } }, 1000); //For collapse new Handler().postDelayed(new Runnable() { #Override public void run() { new AnimationUtililty().collapse(inactive); //inactive.setVisibility(View.GONE); } }, 8000); } Other solution is: public void expandOrCollapse(final View v,String exp_or_colpse) { TranslateAnimation anim = null; if(exp_or_colpse.equals("expand")) { anim = new TranslateAnimation(0.0f, 0.0f, -v.getHeight(), 0.0f); v.setVisibility(View.VISIBLE); } else{ anim = new TranslateAnimation(0.0f, 0.0f, 0.0f, -v.getHeight()); AnimationListener collapselistener= new AnimationListener() { #Override public void onAnimationStart(Animation animation) { } #Override public void onAnimationRepeat(Animation animation) { } #Override public void onAnimationEnd(Animation animation) { v.setVisibility(View.GONE); } }; anim.setAnimationListener(collapselistener); } // To Collapse // anim.setDuration(300); anim.setInterpolator(new AccelerateInterpolator(0.5f)); v.startAnimation(anim); }
I would like to add something to the very helpful answer above. If you don't know the height you'll end up with since your views .getHeight() returns 0 you can do the following to get the height: contentView.measure(DUMMY_HIGH_DIMENSION, DUMMY_HIGH_DIMENSION); int finalHeight = view.getMeasuredHeight(); Where DUMMY_HIGH_DIMENSIONS is the width/height (in pixels) your view is constrained to ... having this a huge number is reasonable when the view is encapsulated with a ScrollView.
This is a snippet that I used to resize the width of a view (LinearLayout) with animation. The code is supposed to do expand or shrink according the target size. If you want a fill_parent width, you will have to pass the parent .getMeasuredWidth as target width while setting the flag to true. Hope it helps some of you. public class WidthResizeAnimation extends Animation { int targetWidth; int originaltWidth; View view; boolean expand; int newWidth = 0; boolean fillParent; public WidthResizeAnimation(View view, int targetWidth, boolean fillParent) { this.view = view; this.originaltWidth = this.view.getMeasuredWidth(); this.targetWidth = targetWidth; newWidth = originaltWidth; if (originaltWidth > targetWidth) { expand = false; } else { expand = true; } this.fillParent = fillParent; } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (expand && newWidth < targetWidth) { newWidth = (int) (newWidth + (targetWidth - newWidth) * interpolatedTime); } if (!expand && newWidth > targetWidth) { newWidth = (int) (newWidth - (newWidth - targetWidth) * interpolatedTime); } if (fillParent && interpolatedTime == 1.0) { view.getLayoutParams().width = -1; } else { view.getLayoutParams().width = newWidth; } view.requestLayout(); } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } #Override public boolean willChangeBounds() { return true; } }
Yes, I agreed with the above comments. And indeed, it does seem like the right (or at least the easiest?) thing to do is to specify (in XML) an initial layout height of "0px" -- and then you can pass in another argument for "toHeight" (i.e. the "final height") to the constructor of your custom Animation sub-class, e.g. in the example above, it would look something like so: public DropDownAnim( View v, int toHeight ) { ... } Anyways, hope that helps! :)
combined solutions from #Tom Esterez and #Geraldo Neto public static void expandOrCollapseView(View v,boolean expand){ if(expand){ v.measure(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT); final int targetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); ValueAnimator valueAnimator = ValueAnimator.ofInt(targetHeight); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(500); valueAnimator.start(); } else { final int initialHeight = v.getMeasuredHeight(); ValueAnimator valueAnimator = ValueAnimator.ofInt(initialHeight,0); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator animation) { v.getLayoutParams().height = (int) animation.getAnimatedValue(); v.requestLayout(); if((int)animation.getAnimatedValue() == 0) v.setVisibility(View.GONE); } }); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.setDuration(500); valueAnimator.start(); } } //sample usage expandOrCollapseView((Your ViewGroup),(Your ViewGroup).getVisibility()!=View.VISIBLE);
Here is my solution. I think it is simpler. It only expands the view but can easy be extended. public class WidthExpandAnimation extends Animation { int _targetWidth; View _view; public WidthExpandAnimation(View view) { _view = view; } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime < 1.f) { int newWidth = (int) (_targetWidth * interpolatedTime); _view.layout(_view.getLeft(), _view.getTop(), _view.getLeft() + newWidth, _view.getBottom()); } else _view.requestLayout(); } #Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); _targetWidth = width; } #Override public boolean willChangeBounds() { return true; } }
I think the easiest solution is to set android:animateLayoutChanges="true" to your LinearLayout and then just show/hide view by seting its visibility. Works like a charm, but you have no controll on the animation duration
You can use Transition or Animator that changes visibility of section to be expanded/collapsed, or ConstraintSet with different layouts. Easiest one is to use motionLayout with 2 different layouts and constraintSets to change from one layout to another on button click. You can change between layouts with val constraintSet = ConstraintSet() constraintSet.clone(this, R.layout.layout_collapsed) val transition = ChangeBounds() transition.interpolator = AccelerateInterpolator(1.0f) transition.setDuration(300) TransitionManager.beginDelayedTransition(YOUR_VIEW, transition) constraintSet.applyTo(YOUR_VIEW) With Transition api RotateX.kt I created the one in gif using Transitions api that change rotationX. class RotateX : Transition { #Keep constructor() : super() #Keep constructor(context: Context, attrs: AttributeSet) : super(context, attrs) override fun getTransitionProperties(): Array<String> { return TRANSITION_PROPERTIES } override fun captureStartValues(transitionValues: TransitionValues) { captureValues(transitionValues) } override fun captureEndValues(transitionValues: TransitionValues) { captureValues(transitionValues) } override fun createAnimator( sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { if (startValues == null || endValues == null) return null val startRotation = startValues.values[PROP_ROTATION] as Float val endRotation = endValues.values[PROP_ROTATION] as Float if (startRotation == endRotation) return null val view = endValues.view // ensure the pivot is set view.pivotX = view.width / 2f view.pivotY = view.height / 2f return ObjectAnimator.ofFloat(view, View.ROTATION_X, startRotation, endRotation) } private fun captureValues(transitionValues: TransitionValues) { val view = transitionValues.view if (view == null || view.width <= 0 || view.height <= 0) return transitionValues.values[PROP_ROTATION] = view.rotationX } companion object { private const val PROP_ROTATION = "iosched:rotate:rotation" private val TRANSITION_PROPERTIES = arrayOf(PROP_ROTATION) } } create xml file that targets expand button <?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="#android:interpolator/fast_out_slow_in"> <transition class="com.smarttoolfactory.tutorial3_1transitions.transition.RotateX"> <targets> <target android:targetId="#id/ivExpand" /> </targets> </transition> <autoTransition android:duration="200" /> </transitionSet> My layout to be expanded or collapsed <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <com.google.android.material.card.MaterialCardView android:id="#+id/cardView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="4dp" android:layout_marginVertical="2dp" android:clickable="true" android:focusable="true" android:transitionName="#string/transition_card_view" app:cardCornerRadius="0dp" app:cardElevation="0dp" app:cardPreventCornerOverlap="false"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="16dp" android:paddingBottom="16dp"> <androidx.appcompat.widget.AppCompatImageView android:id="#+id/ivAvatar" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:scaleType="centerCrop" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:src="#drawable/avatar_1_raster" /> <androidx.appcompat.widget.AppCompatImageView android:id="#+id/ivExpand" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:padding="8dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="#drawable/ic_baseline_expand_more_24" /> <TextView android:id="#+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="12dp" android:layout_marginTop="6dp" android:text="Some Title" android:textSize="20sp" android:textStyle="bold" app:layout_constraintStart_toEndOf="#+id/ivAvatar" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="#+id/tvDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textColor="?android:textColorSecondary" android:textSize="12sp" app:layout_constraintStart_toStartOf="#+id/tvTitle" app:layout_constraintTop_toBottomOf="#id/tvTitle" tools:text="Tuesday 7pm" /> <TextView android:id="#+id/tvBody" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" android:ellipsize="end" android:lines="1" android:text="#string/bacon_ipsum_short" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="#+id/ivAvatar" app:layout_constraintTop_toBottomOf="#id/tvDate" /> <androidx.recyclerview.widget.RecyclerView android:id="#+id/recyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:orientation="horizontal" android:overScrollMode="never" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="#id/tvBody" tools:listitem="#layout/item_image_destination" /> </androidx.constraintlayout.widget.ConstraintLayout> </com.google.android.material.card.MaterialCardView> </layout> And set up visibility of items to collapse or expand private fun setUpExpandedStatus() { if (isExpanded) { binding.recyclerView.visibility = View.VISIBLE binding.ivExpand.rotationX = 180f } else { binding.recyclerView.visibility = View.GONE binding.ivExpand.rotationX = 0f } } And start transition with val transition = TransitionInflater.from(itemView.context) .inflateTransition(R.transition.icon_expand_toggle) TransitionManager.beginDelayedTransition(parent, transition) isExpanded = !isExpanded setUpExpandedStatus() I created animation and transitions samples including the one on the gif, you can check them out there.
You are on the right track. Make sure you have v1 set to have a layout height of zero right before the animation starts. You want to initialize your setup to look like the first frame of the animation before starting the animation.
This was my solution, my ImageView grows from 100% to 200% and return to his original size, using two animation files inside res/anim/ folder anim_grow.xml <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="#android:anim/accelerate_interpolator"> <scale android:fromXScale="1.0" android:toXScale="2.0" android:fromYScale="1.0" android:toYScale="2.0" android:duration="3000" android:pivotX="50%" android:pivotY="50%" android:startOffset="2000" /> </set> anim_shrink.xml <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="#android:anim/accelerate_interpolator"> <scale android:fromXScale="2.0" android:toXScale="1.0" android:fromYScale="2.0" android:toYScale="1.0" android:duration="3000" android:pivotX="50%" android:pivotY="50%" android:startOffset="2000" /> </set> Send an ImageView to my method setAnimationGrowShrink() ImageView img1 = (ImageView)findViewById(R.id.image1); setAnimationGrowShrink(img1); setAnimationGrowShrink() method: private void setAnimationGrowShrink(final ImageView imgV){ final Animation animationEnlarge = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.anim_grow); final Animation animationShrink = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.anim_shrink); imgV.startAnimation(animationEnlarge); animationEnlarge.setAnimationListener(new AnimationListener() { #Override public void onAnimationStart(Animation animation) {} #Override public void onAnimationRepeat(Animation animation) {} #Override public void onAnimationEnd(Animation animation) { imgV.startAnimation(animationShrink); } }); animationShrink.setAnimationListener(new AnimationListener() { #Override public void onAnimationStart(Animation animation) {} #Override public void onAnimationRepeat(Animation animation) {} #Override public void onAnimationEnd(Animation animation) { imgV.startAnimation(animationEnlarge); } }); }
This is a proper working solution, I have tested it: Exapnd: private void expand(View v) { v.setVisibility(View.VISIBLE); v.measure(View.MeasureSpec.makeMeasureSpec(PARENT_VIEW.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); final int targetHeight = v.getMeasuredHeight(); mAnimator = slideAnimator(0, targetHeight); mAnimator.setDuration(800); mAnimator.start(); } Collapse: private void collapse(View v) { int finalHeight = v.getHeight(); mAnimator = slideAnimator(finalHeight, 0); mAnimator.addListener(new Animator.AnimatorListener() { #Override public void onAnimationStart(Animator animator) { } #Override public void onAnimationEnd(Animator animator) { //Height=0, but it set visibility to GONE llDescp.setVisibility(View.GONE); } #Override public void onAnimationCancel(Animator animator) { } #Override public void onAnimationRepeat(Animator animator) { } }); mAnimator.start(); } Value Animator: private ValueAnimator slideAnimator(int start, int end) { ValueAnimator mAnimator = ValueAnimator.ofInt(start, end); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //Update Height int value = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = llDescp.getLayoutParams(); layoutParams.height = value; v.setLayoutParams(layoutParams); } }); return mAnimator; } View v is the view to be animated, PARENT_VIEW is the container view containing the view.
Based on solutions by #Tom Esterez and #Seth Nelson (top 2) I simlified them. As well as original solutions it doesn't depend on Developer options (animation settings). private void resizeWithAnimation(final View view, int duration, final int targetHeight) { final int initialHeight = view.getMeasuredHeight(); final int distance = targetHeight - initialHeight; view.setVisibility(View.VISIBLE); Animation a = new Animation() { #Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime == 1 && targetHeight == 0) { view.setVisibility(View.GONE); } view.getLayoutParams().height = (int) (initialHeight + distance * interpolatedTime); view.requestLayout(); } #Override public boolean willChangeBounds() { return true; } }; a.setDuration(duration); view.startAnimation(a); }
This is really simple with droidQuery. For starts, consider this layout: <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" > <LinearLayout android:id="#+id/v1" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="View 1" /> </LinearLayout> <LinearLayout android:id="#+id/v2" android:layout_width="wrap_content" android:layout_height="0dp" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="View 2" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="View 3" /> </LinearLayout> </LinearLayout> We can animate the height to the desired value - say 100dp - using the following code: //convert 100dp to pixel value int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics()); Then use droidQuery to animate. The simplest way is with this: $.animate("{ height: " + height + "}", new AnimationOptions()); To make the animation more appealing, consider adding an easing: $.animate("{ height: " + height + "}", new AnimationOptions().easing($.Easing.BOUNCE)); You can also change the duration on AnimationOptions using the duration() method, or handle what happens when the animation ends. For a complex example, try: $.animate("{ height: " + height + "}", new AnimationOptions().easing($.Easing.BOUNCE) .duration(1000) .complete(new Function() { #Override public void invoke($ d, Object... args) { $.toast(context, "finished", Toast.LENGTH_SHORT); } }));
Best solution for expand/collapse view's: #Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { View view = buttonView.getId() == R.id.tb_search ? fSearch : layoutSettings; transform(view, 200, isChecked ? ViewGroup.LayoutParams.WRAP_CONTENT : 0); } public static void transform(final View v, int duration, int targetHeight) { int prevHeight = v.getHeight(); v.setVisibility(View.VISIBLE); ValueAnimator animator; if (targetHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { v.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); animator = ValueAnimator.ofInt(prevHeight, v.getMeasuredHeight()); } else { animator = ValueAnimator.ofInt(prevHeight, targetHeight); } animator.addUpdateListener(animation -> { v.getLayoutParams().height = (animation.getAnimatedFraction() == 1.0f) ? targetHeight : (int) animation.getAnimatedValue(); v.requestLayout(); }); animator.setInterpolator(new LinearInterpolator()); animator.setDuration(duration); animator.start(); }
You can use a ViewPropertyAnimator with a slight twist. To collapse, scale the view to a height of 1 pixel, then hide it. To expand, show it, then expand it to its height. private void collapse(final View view) { view.setPivotY(0); view.animate().scaleY(1/view.getHeight()).setDuration(1000).withEndAction(new Runnable() { #Override public void run() { view.setVisibility(GONE); } }); } private void expand(View view, int height) { float scaleFactor = height / view.getHeight(); view.setVisibility(VISIBLE); view.setPivotY(0); view.animate().scaleY(scaleFactor).setDuration(1000); } The pivot tells the view where to scale from, default is in the middle. The duration is optional (default = 1000). You can also set the interpolator to use, like .setInterpolator(new AccelerateDecelerateInterpolator())
I created version in which you don't need to specify layout height, hence it's a lot easier and cleaner to use. The solution is to get the height in the first frame of the animation (it's available at that moment, at least during my tests). This way you can provide a View with an arbitrary height and bottom margin. There's also one little hack in the constructor - the bottom margin is set to -10000 so that the view stays hidden before the transformation (prevents flicker). public class ExpandAnimation extends Animation { private View mAnimatedView; private ViewGroup.MarginLayoutParams mViewLayoutParams; private int mMarginStart, mMarginEnd; public ExpandAnimation(View view) { mAnimatedView = view; mViewLayoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); mMarginEnd = mViewLayoutParams.bottomMargin; mMarginStart = -10000; //hide before viewing by settings very high negative bottom margin (hack, but works nicely) mViewLayoutParams.bottomMargin = mMarginStart; mAnimatedView.setLayoutParams(mViewLayoutParams); } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { super.applyTransformation(interpolatedTime, t); //view height is already known when the animation starts if(interpolatedTime==0){ mMarginStart = -mAnimatedView.getHeight(); } mViewLayoutParams.bottomMargin = (int)((mMarginEnd-mMarginStart) * interpolatedTime)+mMarginStart; mAnimatedView.setLayoutParams(mViewLayoutParams); } }
Use ValueAnimator: ValueAnimator expandAnimation = ValueAnimator.ofInt(mainView.getHeight(), 400); expandAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { #Override public void onAnimationUpdate(final ValueAnimator animation) { int height = (Integer) animation.getAnimatedValue(); RelativeLayout.LayoutParams lp = (LayoutParams) mainView.getLayoutParams(); lp.height = height; } }); expandAnimation.setDuration(500); expandAnimation.start();
public static void slide(View v, int speed, int pos) { v.animate().setDuration(speed); v.animate().translationY(pos); v.animate().start(); } // slide down slide(yourView, 250, yourViewHeight); // slide up slide(yourView, 250, 0);