Upgrade progress loading with a skeleton and shimmer effect in Android

Skeletons are everywhere now, in every app and every site. Sounds like a horror movie, but it is actually not so bad and quite easy to implement.

Skeleton loading is everywhere!

Here is a simple tutorial for changing your progress loading UI look, ditch the spinning circle and upgrade to the skeleton loader which gives the user a feel the data is only micro seconds away from them.

The skeleton should look like the data row that will replace it, but while the data is loading we will show a grey background in place of the views in the data row.

The tutorial consists of 4 Parts:

  1. Create the skeleton row layout
  2. Dynamic allocation of rows
  3. Add a shimmer effect
  4. Implement the skeleton progress load

1. Create the Skeleton Row Layout

So the design of your skeleton row should be based on the data you will show when the loading is done, but here is a snippet of a pretty basic look, which you can play with and make it match your own needs:

First, we create a drawable for the background of each view, in this case I added a radius to round the corners a bit.

drawable/skeleton.xml:

<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<solid android:color="@color/skeleton"/>
<corners android:radius="4dp"/>
</shape>

Second we create a layout for one skeleton row which we will multiply later.

layout/skeleton_row_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/row_layout_height">

<View
android:id="@+id/icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_margin="15dp"
android:layout_gravity="center_vertical"
android:background="@drawable/skeleton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

<View
android:id="@+id/topText"
android:layout_width="200dp"
app:layout_constraintTop_toTopOf="@id/icon"
app:layout_constraintStart_toEndOf="@id/icon"
android:layout_height="15dp"
android:layout_marginLeft="16dp"
android:background="@drawable/skeleton"/>

<View
android:id="@+id/bottomText"
android:layout_width="250dp"
android:layout_height="15dp"
app:layout_constraintTop_toBottomOf="@id/topText"
android:layout_marginTop="10dp"
app:layout_constraintStart_toEndOf="@id/icon"
android:layout_marginLeft="16dp"
android:background="@drawable/skeleton"/>

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/skeleton"
app:layout_constraintTop_toBottomOf="@id/icon"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

</android.support.constraint.ConstraintLayout>

This is what we achieved so far:

Skeleton row layout

Great work! Now how do we fill the screen with more skeleton rows?

2. Dynamic allocation of rows

In my example we know the height of row in advance, it is similar to that of the data row that will appear after loading, so we use the same height for our skeleton row and we can measure how many rows to show per screen.

public int getSkeletonRowCount(Context context) {
int pxHeight = getDeviceHeight(context);
int skeletonRowHeight = (int) getResources()
.getDimension(R.dimen.row_layout_height); //converts to pixel
return (int) Math.ceil(pxHeight / skeletonRowHeight);
}
public int getDeviceHeight(Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return metrics.heightPixels;
}

We want to round up the number of rows, so the bottom one will be cut off rather than partially blank, therefore I used Math.ceil().

Next we will put the skeleton layout in a linear layout which we will add skeleton rows to dynamically depending on the number of rows we get from getSkeletonRowCount().

Eventually our loading screen will look like this:

Linear layout of skeleton rows

3. Add a shimmer effect

There are several libraries for adding the shimmer effect, I used the following, it is very straight forward and lets you play with the attributes:

To add the ShimmerLayout to your app copy the following line to your app gradle dependencies model:

implementation 'io.supercharge:shimmerlayout:2.1.0'

We wrap the skeleton linear layout with a shimmer layout and fill in the attributes as we please (or as the UI guy instructs us…).

layout/skeleton_shimmer_layout.xml

<?xml version="1.0" encoding="utf-8"?>  <io.supercharge.shimmerlayout.ShimmerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/shimmerSkeleton"
android:background="@color/White"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:shimmer_angle="10"
android:visibility="gone"
app:shimmer_animation_duration="2500"
app:shimmer_color="@color/shimmer"
app:shimmer_mask_width="0.2">
<LinearLayout
android:id="@+id/skeletonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</io.supercharge.shimmerlayout.ShimmerLayout>

I embed the skeleton_shimmer_layout.xml file in every xml file in my app that requires the skeleton progress using ‘include’. I put it in a FrameLayout above the RecyclerView I am replacing:

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/skeleton_shimmer_layout"/> <android.support.v7.widget.RecyclerView
android:id="@+id/myRecyclerView"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>

This is a very minimal piece of code and is very dependent on your app, but I hope it gives you the right picture and will help you in your app.

4. Implement the skeleton progress load

So now comes the “difficult” part, making it actually happen. I use two methods here, one for showing the skeleton and the other for animating the skeleton fade out while the RecyclerView with the loaded data fades in.

I declare three global variables:

public LinearLayout skeletonLayout;
public ShimmerLayout shimmer;
public LayoutInflater inflater;

Which I initiate in onCreate():

skeletonLayout = rootView.findViewById(R.id.skeletonLayout);
shimmer = rootView.findViewById(R.id.shimmerSkeleton);
this.inflater = (LayoutInflater) getActivity()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

And wherever I load data I call the following method:

public void showSkeleton(boolean show) {

if (show) {

skeletonLayout.removeAllViews();

int skeletonRows = getSkeletonRowCount();
for (int i = 0; i <= skeletonRows; i++) {
ViewGroup rowLayout = (ViewGroup) inflater
.inflate(R.layout.skeleton_row_layout, null);
skeletonLayout.addView(rowLayout);
}
shimmer.setVisibility(View.VISIBLE);
shimmer.startShimmerAnimation();
skeletonLayout.setVisibility(View.VISIBLE);
skeletonLayout.bringToFront();
} else {
shimmer.stopShimmerAnimation();
shimmer.setVisibility(View.GONE);
}
}

Tada! We have a shimmery shiny skeleton loader.

Shimmer effect on the skeleton layout

Now, for the final touch, when data successfully loads I call the animate method, the fading occurs at the same time and looks good since I put both views in a FrameLayout (as shown in section 3):

public void animateReplaceSkeleton(View listView) {

listView.setVisibility(View.VISIBLE);
listView.setAlpha(0f);
listView.animate().alpha(1f).setDuration(1000).start();

skeletonLayout.animate().alpha(0f).setDuration(1000).withEndAction(new Runnable() {
@Override
public void run() {
showSkeleton(false, listView);
}
}).start();

}

If data fails to load you can call showSkeleton(false) and show your error view.

I hope this tutorial was useful and you enjoyed reading it. Feel free to ask questions and comment.