A useful stack on android #2, user interface

This is the second part of the series: ‘A useful stack on android’, in the first part, I reviewed the general architecture of the proyect, this article focuses on the user interface and the global design of the application.

I do not like to talk about how to materialize an android application with Material Design you can find here a nice presentation by David Gonzalez.

Looking at the structure of the design, there are only two activities: MoviesActivity with a RecyclerView containing all the films, and the MovieDetailActivity, that shows some details of a selected film.

This project is available on GitHub


Libraries


app/build.gradle

    // Google libraries
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.android.support:recyclerview-v7:21.0.3'
    compile 'com.android.support:palette-v7:21.0.0'

    // Square libraries
    compile 'com.squareup.picasso:picasso:2.4.0'
    compile 'com.jakewharton:butterknife:6.0.0'


AppCompat

What to say about the new AppCompat library from Google, in this library a new element called Toolbar is included.

The Toolbar element is a generalization of the old: Action Bar.

This new widget is a ViewGroup, so we can group views within it, in my case I have included a custom TextView with a particular font.

Taking advantage of being able to have this component in my layout, when the user scrolls down the Toolbar is hidden, when the user scrolls up the `Toolbar is displayed.


activity_main.xml

<android.widget.Toolbar
    android:id="@+id/activity_main_toolbar"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="?attr/actionBarSize"
    android:background="@color/theme_primary"
    android:elevation="10dp"
    >

    <com.hackvg.android.views.custom_views.LobsterTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textSize="22sp"
        android:textColor="#FFF"
        />

</android.widget.Toolbar>


MoviesActivity.java

private RecyclerView.OnScrollListener recyclerScrollListener = 
    new RecyclerView.OnScrollListener() {

    public boolean flag;

    @Override
    public void onScrolled(RecyclerView recyclerView, 
        int dx, int dy) {

        super.onScrolled(recyclerView, dx, dy);

        // Is scrolling up
        if (dy > 10) {

            if (!flag) {

                showToolbar();
                flag = true;
            }

        // Is scrolling down
        } else if (dy < -10) {

            if (flag) {

                hideToolbar();
                flag = false;
            }
        }
    }
};

private void showToolbar() {

    toolbar.startAnimation(AnimationUtils.loadAnimation(this,
        R.anim.translate_up_off));
}

private void hideToolbar() {

    toolbar.startAnimation(AnimationUtils.loadAnimation(this,
        R.anim.translate_up_on));
}


translate_up_off.xml

<?xml version="1.0" encoding="utf-8"?>

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/fast_out_linear_in"
    android:fillAfter="true">

    <translate
        android:duration="@integer/anim_trans_duration_millis"
        android:startOffset="0"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="-100%"
        />
</set>


ButterKnife

ButterKnife by Jake Wharton, is a library to perform view injections.

This avoids having to write repetitive sentences like findViewById or setOnClickListener(new OnClick...).

Using ButterKnife, the code is more readable in adition to write much less.


MovieDetailActivity.java

    @InjectViews({
        R.id.activity_detail_title,
        R.id.activity_detail_content,
        R.id.activity_detail_homepage,
        R.id.activity_detail_company,
        R.id.activity_detail_tagline,
        R.id.activity_detail_confirmation_text,
    }) List<TextView> movieInfoTextViews;

    @InjectViews({
        R.id.activity_detail_header_tagline,
        R.id.activity_detail_header_description
    }) List<TextView> headers;

    @InjectView(R.id.activity_detail_book_info)              
    View overviewContainer;
    @InjectView(R.id.activity_detail_fab)                    
    ImageView fabButton;
    @InjectView(R.id.activity_detail_cover)                  
    ImageView coverImageView;
    @InjectView(R.id.activity_detail_confirmation_image)     
    ImageView confirmationView;
    @InjectView(R.id.activity_detail_confirmation_container) 
    FrameLayout confirmationContainer;

An interesting fact of this library, is the annotation @InjectViews, which allows to inject multiple views in a list, so you can use interfaces as Setters or Actions to apply a property to all views inside the list at once.


GUIUtils.java

public static final ButterKnife.Setter<TextView, Integer> setter = new ButterKnife.Setter<TextView, Integer>() {

    @Override
    public void set(TextView view, Integer value, int index) {
        view.setTextColor(value);
    }
};

In my case all TextViews that show information about the film are set with a certain text color.

MoviesActivity.java

ButterKnife.apply(movieInfoTextViews, GUIUtils.setter, 
    lightSwatch.getTitleTextColor());

With ButterKnife also you can handle some view events:

@OnClick(R.id.activity_movie_detail_fab)
public void onClick() {
    showConfirmationView();
}


Palette

With Android L Google has introduced a new library called Palette, it promises to extract the predominant colors in an image.


MovieDetailActivity
These colors are grouped in a container called Swatch, which contains among other properties a background color and a color for the text readable in conjunction with the background color.

With Palette you can obtain the following sets of colors:

For this application I have used: VibrantSwatch, DarkVibrantSwatch and LightVibrantSwatch.


Note that you can not always extract certain colors in an image, so it is recommended to check that Palette does not return null sets.

Another aspect to consider is that the task of determining colors is a complex task, so Palette provides an asynchronous way to generate the colors.


MoviesActivity.java

Palette.generateAsync(bookCoverBitmap, this);
public class MovieDetailActivity extends Activity implements 
    MVPDetailView, Palette.PaletteAsyncListener {
    ...

        @Override
    public void onGenerated(Palette palette) {

        if (palette != null) {

            Palette.Swatch vibrantSwatch = palette
                .getVibrantSwatch();

            Palette.Swatch darkVibrantSwatch = palette
                .getDarkVibrantSwatch();

            Palette.Swatch lightSwatch = palette
                .getLightVibrantSwatch();

            if (lightSwatch != null) {

                // awesome palette code
            }
        }
    }
}

I found some interesting concepts in the Dialer app of Lollipop. In the contact detail view, the icons are tinted depending the tint color that is applied to the contact picture:

This effect can be achieved dynamically by applying a ColorFilter to the CompoundDrawable of the TextView


GUIUtils.java

  public static void tintAndSetCompoundDrawable (Context context, 
    @DrawableRes int drawableRes, int color, TextView textview) {

        Resources res = context.getResources();
        int padding = (int) res.getDimension(
            R.dimen.activity_horizontal_margin);

        Drawable drawable = res.getDrawable(drawableRes);
        drawable.setColorFilter(color, PorterDuff.Mode.MULTIPLY);

        textview.setCompoundDrawablesRelativeWithIntrinsicBounds(
            drawable, null, null, null);

        textview.setCompoundDrawablePadding(padding);
    }

Result:


Transitions

The transition between the activity: MoviesActivity, and the MovieDetailActivity, makes use of a shared element which is the cover image of the selected film.

In RecyclerView adapter is specified the transitionName that will be necessary to perform the transition.

    @Override
    public void onBindViewHolder(MovieViewHolder holder, 
        int position) {

        TvMovie selectedMovie = movieList.get(position);

        holder.titleTextView.setText(selectedMovie.getTitle());
        holder.coverImageView.setTransitionName("cover" + position);

        String posterURL = Constants.POSTER_PREFIX 
            + selectedMovie.getPoster_path();

        Picasso.with(context)
            .load(posterURL)
            .into(holder.coverImageView);
    }

Before change to the detail activity, is specified in the intent the element that will be shared with the ActivityOptions.

    @Override
    public void onClick(View v, int position) {

        Intent i = new Intent (MoviesActivity.this, 
            MovieDetailActivity.class);

        String movieID = moviesAdapter.getMovieList()
            .get(position).getId();

        i.putExtra("movie_id", movieID);
        i.putExtra("movie_position", position);

        ImageView coverImage = (ImageView) v.findViewById(
            R.id.item_movie_cover);

        photoCache.put(0, coverImage
            .getDrawingCache());

        // Setup the transition to the detail activity
        ActivityOptions options = ActivityOptions
            .makeSceneTransitionAnimation(this, 
            new Pair<View, String>(v, "cover" + position));

        startActivity(i, options.toBundle());
    }

Finally in the detail activity is specified which view is shared collecting the metadata from the incoming intent.

    @Override
    public void onCreate(Bundle savedInstanceState) {

        ...

        int moviePosition = getIntent()
            .getIntExtra("movie_position", 0);
        
        coverImageView.setTransitionName(
            "cover" + moviePosition);

        ...

These could be the requirements of any application with a list and a detail view, but what if there is an intermediate state between the detail, and with the returning to the list?

When a user presses the Floating Action Button to set a movie as favorite, a view is briefly presented to inform the user that the save has been successful.

After that I am not interested in making use of the transition: sharedElementReturnTransition for return to the main activity, what interests me is to show an animation that changes the user experience.

Is not the same to mark a film as favorite that return without do anything with the film, so the design has to behave differently.


When the confirmation view is shown, the return transition is overwritten, the shared element effect (the film cover) will not be animated and the return effect will be to pull down the activity: getWindow().setReturnTransition(new Slide());

VectorDrawable

A very interesting feature introduced with lollipop are the VectorDrawables.

With the new drawables a new world opens to the vector graphics, image scalling etc…

Lollipop also includes powerful tools to deal with these new graphics.

The VectorDrawable makes use of part of the SVG specification, for example, this is a star in SVG format:



<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"  xmlns="http://www.w3.org/2000/svg" 
    width="300px" 
    height="300px" >

    <g id="star_group">
        <path fill="#000000" d="M 200.30535,69.729172
        C 205.21044,69.729172 236.50709,141.52218 240.4754,144.40532
        C 244.4437,147.28846 322.39411,154.86809 323.90987,159.53312
        C 325.42562,164.19814 266.81761,216.14828 265.30186,220.81331
        C 263.7861,225.47833 280.66544,301.9558 276.69714,304.83894
        C 272.72883,307.72209 205.21044,268.03603 200.30534,268.03603
        C 195.40025,268.03603 127.88185,307.72208 123.91355,304.83894
        C 119.94524,301.9558 136.82459,225.47832 135.30883,220.8133
        C 133.79307,216.14828 75.185066,164.19813 76.700824,159.53311
        C 78.216581,154.86809 156.16699,147.28846 160.13529,144.40532
        C 164.1036,141.52218 195.40025,69.729172 200.30535,69.729172 z"/>
    </g>
</svg>

An this, its implementation with a VectorDrawable

vd_star.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportWidth="400"
    android:viewportHeight="400"
    android:width="300px"
    android:height="300px">

    <group android:name="star_group"
        android:pivotX="200"
        android:pivotY="200"
        android:scaleX="0.0"
        android:scaleY="0.0">

        <path
            android:name="star"
            android:fillColor="#FFFFFF"
            android:pathData="@string/star_data"/>
    </group>
</vector>

strings.xml

<string name="star_data">
    M 200.30535,69.729172
    C 205.21044,69.729172 236.50709,141.52218 240.4754,144.40532
    C 244.4437,147.28846 322.39411,154.86809 323.90987,159.53312
    C 325.42562,164.19814 266.81761,216.14828 265.30186,220.81331
    C 263.7861,225.47833 280.66544,301.9558 276.69714,304.83894
    C 272.72883,307.72209 205.21044,268.03603 200.30534,268.03603
    C 195.40025,268.03603 127.88185,307.72208 123.91355,304.83894
    C 119.94524,301.9558 136.82459,225.47832 135.30883,220.8133
    C 133.79307,216.14828 75.185066,164.19813 76.700824,159.53311
    C 78.216581,154.86809 156.16699,147.28846 160.13529,144.40532
    C 164.1036,141.52218 195.40025,69.729172 200.30535,69.729172 z
</string>

There is little difference, there are groups, paths etc…, android:viewport{Width|Height} specifies the size of the canvas, while android:width & android:height specifies the image size.

The <animated-vector> allows to animate groups of <paths>, translations, rotations and among other animations, morphing.

In this case for example, a star is shown with a scaling animation and when it ends, an animation of rotation is shown. At the same time the shape of the star is changing to the stick candy shape, and then changes again to its original shape, a star.

It is noteworthy that to make it possible to show a morphing animation, the data has to contain the same SVG commands, if not, an exception will be triggered.

avd_star.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vd_star">

    <target
        android:name="star_group"
        android:animation="@anim/appear_rotate" />

    <target
        android:name="star"
        android:animation="@anim/star_morph" />

</animated-vector>

The <animated-vector> is associated with the drawable vd_star.xml, the targets, are the elements that will be animated:

appear_rotate.xml

<set
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:interpolator="@android:anim/decelerate_interpolator"
    >

    <set
        android:ordering="together"
        >

        <objectAnimator
            android:duration="300"
            android:propertyName="scaleX"
            android:valueFrom="0.0"
            android:valueTo="1.0"/>

        <objectAnimator
            android:duration="300"
            android:propertyName="scaleY"
            android:valueFrom="0.0"
            android:valueTo="1.0"/>
    </set>

    <objectAnimator
        android:propertyName="rotation"
        android:duration="500"
        android:valueFrom="0"
        android:valueTo="360"
        android:valueType="floatType"/>
</set>

I would like emphasize that the mutation will be succesfull if the SVG commands are the same in the new SVG data but with different values.

In this <set> is changed the star shape to a stick candy, after the shape returns to be a star.

star_morph.xml

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:fillAfter="true">

    <objectAnimator
        android:duration="500"
        android:propertyName="pathData"
        android:valueFrom="@string/star_data"
        android:valueTo="@string/star_lollipop"
        android:valueType="pathType"
        android:interpolator="@android:anim/accelerate_interpolator"/>

    <objectAnimator
        android:duration="500"
        android:propertyName="pathData"
        android:valueFrom="@string/star_lollipop"
        android:valueTo="@string/star_data"
        android:valueType="pathType"
        android:interpolator="@android:anim/accelerate_interpolator"/>
</set>

activity_detail.xml

    <ImageView
        android:id="@+id/activity_movie_detail_confirmation_image"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:src="@drawable/avd_star"
        />

MovieDetailActivity.java

    @Override
    public void animateConfirmationView() {

        Drawable drawable = confirmationView.getDrawable();

        if (drawable instanceof Animatable)
            ((Animatable) drawable).start();
    }

Result:



Sticky headers

Another aspect that caught my attention in the implementation of Google Dialer is that when you scroll in a contact information, the image becomes smaller until a point. In that point the image becomes fixed.



Trying to replicate this effect I have found a post by Roman Nurik, where the property: View.setTranslationY(float translationY) is used for this purpose in a ScrollView listener.

To achieve this effect, the translationY conjugated with the displaced value in the ScrollView allows to set the title fixed.

MovieDetailActivity.xml


    @Override
    public void onScrollChanged(ScrollView scrollView, 
        int x, int y, int oldx, int oldy) {

        if (y > coverImageView.getHeight()) {

            movieInfoTextViews.get(TITLE).setTranslationY(
                y - coverImageView.getHeight());

            if (!isTranslucent) {
                GUIUtils.setTheStatusbarNotTranslucent(this);
                getWindow().setStatusBarColor(mBrightSwatch.getRgb());
                isTranslucent = true;
            }
        }

        if (y < coverImageView.getHeight() && isTranslucent) {

            GUIUtils.makeTheStatusbarTranslucent(this);
            isTranslucent = false;
        }
    }

Result:



** There are still a few bugs in this section, for example, if you make a quickly scroll a gap is created between the cover image and the title, Pull request welcome!

Resources: