Cdacians

Cdacians
Cdacians

Thursday, 7 September 2017

Fragments translate animation

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:
My gif
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:
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 call setCustomAnimations before add. 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 in ObjectAnimator (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 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:
yFraction
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 yFractiontranslationY and getHeight() values:
logcat
Every 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:
fixed animation
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.

Android Test tricks - Sharing code between UI & unit tests

Android Test tricks - Sharing code between UI & unit tests

In this series of posts I would like to share some useful tricks I learned over the last few years related to testing on Android.
The first trick will be really short

Sharing code between unit tests and integration tests

At the moment the most common Android testing layout includes 2 different test suites - unit tests (pure Java tests which run on JVM and don't require Android device) and integration (aka UI tests, aka Android tests, you name it) test suite which runs on Android device (or emulator).
Normally this looks something like this:
The problem is that sometimes you want to have code which is shared between those 2 source sets. Let's say some TestUtils.java which has some common functionality for both test suites.
Unfortunately, UI test suite and unit test suite do not share code, i.e. any code placed in test folder will not be visible by UI test suite (and vice versa).
But luckily there is an easy solution! Gradle magic to the rescue!

Creating a shared test folder

What we need to do - is create a new folder (let's call it testShared) inside app/srcfolder:
Now create our beloved TestUtils.java class:
And now goes the magic part. In your app/build.gradle file add the following (anywhere outside android closure):
android.sourceSets {  
    test {
        java.srcDirs += "$projectDir/src/testShared"
    }

    androidTest {
        java.srcDirs += "$projectDir/src/testShared"
    }
}
This way we just told both test suites to also include our testShared folder in their source sets.
Now after you sync your project with Gradle changes, you should be able to reference your TestUtils class from both UI and unit test suites:
Have fun!

Android resources and attributes cheatsheet

Android resources and attributes cheatsheet

Few days ago I stumbled upon one of my early-Android-dev-days cheatsheet I created for understanding different syntax when dealing with Android resources and theme attributes.
Surprisingly, I found it quite useful now, so I decided to make it more blogpost-ready and share with others.
Just so you understand what I will be talking about today, consider following ways to set view's background color via xml layout:
android:background="@color/colorPrimary"  
android:background="@com.myapp:color/colorPrimary"  
android:background="?colorPrimary"  
android:background="?attr/colorPrimary"  
android:background="?com.myapp:attr/colorPrimary"  
android:background="?com.myapp:colorPrimary"  
android:background="?android:colorPrimary"  
android:background="?android:attr/colorPrimary"  
Exciting, isn't it? Well, I hope I'll try to break it down, so it doesn't look this scary anymore.

Referencing resources vs style attributes

This is a small detour into Android basics since it is really important to understand the difference between @ and ? before we move any further.
When we use @ - we reference actual resource value (color, string, dimension, etc). I.e. this resource should have an actual value. In this case we know exactly what value we are dealing with.
I.e.
app/src/main/res/values/color.xml
<?xml version="1.0" encoding="utf-8"?>  
<resources>  
    <color name="colorPrimary">#3F51B5</color>
</resources>  
So when we try to reference this value in xml (android:background="@color/colorPrimary"), background will be set to color #3F51B5no matter what theme is currently set for this activity.
On the other side, when you see ? notation - it means that we are trying to reference a style attribute - a value which may vary depending on current theme. I.e. in each specific theme I can override this attribute, so I don't need to change my XML layout - I just need to apply proper theme:
<resources>  
    <style name="AppTheme" parent="Theme.AppCompat.Light">
        <item name="colorPrimary">#F00</item>
    </style>
</resources>  
<TextView  
    android:id="@+id/my_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="?colorPrimary"/>
In this case, we ask Android: "Hey, give me a value of colorPrimary attribute which is specified in current theme". In this case it is a bit harder to tell exactly what color our background is going to be since it really depends on theme applied to the activity this layout belongs to.

Syntax

Now let's see what is the actual syntax for referencing different resources.

Referencing resources (@)

@[package_name:]resource_type/resource_name
  • package_name - optional name of the package this resource belongs to (by default - your app package). Reserved package - android. Used for resources shipping with platform
  • resource_type - the R subclass for the resource type (attrcolorstringdimen, etc)
  • resource_name - an actual name of the resource we are trying to reference.
Let's actually take my first 2 examples and try to break them down:
android:background="@color/colorPrimary"  
android:background="@com.myapp:color/colorPrimary"  
As you can see - both of them are equivalent since by default, package name is set to our app's package name, so it is not necessary to mention it:
  • package(optional) = com.myapp
  • resource_type = color
  • resource_name = colorPrimary
As you might think, Android ships with some predefined resources for entire OS. F.i. I could reference some built-in color this way:
android:background="@android:color/holo_orange_dark"  
Here is what we got in this case:
  • package = android - referencing built-in resources
  • resource_type = color
  • resource_name = holo_orange_dark
PLEASE NOTE
Nowadays, lots of people use AppCompat (and if you don't - you probably should), and AppCompat often defines its own resources. Even though AppCompat is a first-party lib shipped by Google, it is not really a part of operating system. Instead, those resources get merged into your app, so you don't need to use android keyword to reference those.
Example:
android:background="?selectableItemBackground"  
Here, even though we don't have custom style attribute name selectableItemBackground in our app (notice that we didn't use android: prefix), we can still reference it because it was "added" to our app by AppCompat.

Referencing style attributes (?)

Guess what. The syntax is pretty similar to resources:
?[package_name:][resource_type/]resource_name
There one small difference though.
The only allowed resource_type when referencing style attributes is attr. So given that, Android packaging tool actually allows us to omit resource_type, so it is effectively optional.
So following expressions mean exactly the same thing from Android perspective:
android:background="?com.myapp:attr/colorPrimary" //verbose format  
android:background="?com.myapp:colorPrimary" //attr is skipped since its optional  
android:background="?attr/colorPrimary" //package is skipped since its optional  
android:background="?colorPrimary"  // package & attr is skipped  
As you can see, syntax is super simple after all. Never get confused again!