Fragments translate animation
During one of my recent assignments I had to implement a Fragment which slides up from the bottom when you open it and slides back down when you close it. Something like this:
Here list view is a separate
ListFragment
and when I press "list" action item - I add this fragment to the main activity.
Lets find out how to do that and what problems you might be facing here.
setCustomAnimations
The very first thing I found is FragmentTransaction#setCustomAnimations() method which lets you specify custom enter/exit animations for certain fragment transaction.
Neat! Looks like exactly what I need. The only thing I need to do - is to create custom Animator via XML and pass it into my fragment transaction:
Neat! Looks like exactly what I need. The only thing I need to do - is to create custom Animator via XML and pass it into my fragment transaction:
slide_up.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="translationY"
android:valueType="floatType"
android:valueFrom="1280"
android:valueTo="0"
android:duration="@android:integer/config_mediumAnimTime"/>
slide_down.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="translationY"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1280"
android:duration="@android:integer/config_mediumAnimTime"/>
fragment toggle routine:
private void toggleList() {
Fragment f = getFragmentManager()
.findFragmentByTag(LIST_FRAGMENT_TAG);
if (f != null) {
getFragmentManager().popBackStack();
} else {
getFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.slide_up,
R.animator.slide_down,
R.animator.slide_up,
R.animator.slide_down)
.add(R.id.list_fragment_container, Fragment
.instantiate(this, SlidingListFragment.class.getName()),
LIST_FRAGMENT_TAG
).addToBackStack(null).commit();
}
}
Problem #1: Order matters
Please note that order in which you call methods of your fragment transaction matters! In my case I callsetCustomAnimations
beforeadd
. If you swap these calls - animation will not be specified.Problem #2: Hardcoded values are bad
Unfortunately, it is not possible to specify relative translation inObjectAnimator
(like 100%, -50%, etc.), so we had to hardcode target translation. Don't want to even explain why this is bad.
setYFraction()
Definitely hardcoded translation values is not acceptable approach for me, so let's see what we can do here.
Even if
Even if
ObjectAnimator
doesn't have relative translation attribute, it doesn't mean we cannot create our own one. Essentially we can animate any built-in object property (accessible via setter method) or even create our own one.
Having said that, let's create custom
RelativeLayout
and add yFraction
attribute which we will be animating via ObjectAnimator
:SlidingRelativeLayout.java
:package com.trickyandroid.fragmenttranslate.app.view;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
public class SlidingRelativeLayout extends RelativeLayout {
public SlidingRelativeLayout(Context context) {
super(context);
}
public SlidingRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setYFraction(final float fraction) {
float translationY = getHeight() * fraction;
setTranslationY(translationY);
}
public float getYFraction() {
if (getHeight() == 0) {
return 0;
}
return getTranslationY() / getHeight();
}
}
Here I created custom attribute
yFraction
which represents current Y translation relative to view's height (fraction 0.5 = 50% of height). I.e. now if we animate our fraction value from 0 to 1 - we will animate translationY from 0% to 100%
Now let's set this custom layout as a root layout for my fragment:
sliding_fragment_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<com.trickyandroid.fragmenttranslate.app.view.SlidingRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#7c7c7c">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.trickyandroid.fragmenttranslate.app.view.SlidingRelativeLayout>
And let's update my animation resources:
slide_up.xml
:<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="yFraction"
android:valueType="floatType"
android:valueFrom="1.0"
android:valueTo="0"
android:duration="@android:integer/config_mediumAnimTime"/>
slide_down.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="yFraction"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1.0"
android:duration="@android:integer/config_mediumAnimTime"/>
Let's see what we've got:
WTF? It seems like it slides correctly, but the very first frame in slide_up animation is not translated :(
The problem is that custom fragment animation starts before our layout is measured. It means that our
getHeight()
is '0' thus translationY
is '0' as well when it should be 100%. To confirm this let's add onPreDrawListener()
to our custom layout and dump current yFraction
, translationY
and getHeight()
values:
Every
And when the first frame is about to render - translation is set incorrectly.
setYFraction
call represents a call from our animator. Here you can see that first 2 calls were made when layout height was '0', but yFraction
is 1 which means it should be translated by entire height of the view.And when the first frame is about to render - translation is set incorrectly.
setYFraction(). Revised
The problem is clear - we need to remember
yFraction
value and update our translationY once view is measured but just before it is rendered. onPreDraw()
suits the best here:SlidingRelativeLayout.java
package com.trickyandroid.fragmenttranslate.app.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.widget.RelativeLayout;
public class SlidingRelativeLayout extends RelativeLayout {
private float yFraction = 0;
public SlidingRelativeLayout(Context context) {
super(context);
}
public SlidingRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
private ViewTreeObserver.OnPreDrawListener preDrawListener = null;
public void setYFraction(float fraction) {
this.yFraction = fraction;
if (getHeight() == 0) {
if (preDrawListener == null) {
preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
setYFraction(yFraction);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
return;
}
float translationY = getHeight() * fraction;
setTranslationY(translationY);
}
public float getYFraction() {
return this.yFraction;
}
}
Let's see what we got:
Gif puts a lot of distortion, but on a device this animation looks really smooth and silky.
Bonus
One neat trick would also help with our "missing" first frame. Along with translate animation you can also animate alpha (from 0 to 1). This will cause first frame to be fully transparent, so you don't see it :) No need to mention that it is not a solution, but rather a workaround..:
slide_up.xml
:<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="yFraction"
android:valueType="floatType"
android:valueFrom="1.0"
android:valueTo="0"
android:duration="@android:integer/config_mediumAnimTime"/>
<objectAnimator
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="alpha"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="1.0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
That's kinda it. With these not very complex manipulations we got pretty nice looking fragment animation.
No comments:
Post a Comment