12/08/2018, 16:51

Android Content Placeholder Animation using Shimmer

Thông thường ta hay sử dụng spinner loader khi ứng dụng lấy dữ liệu từ network về máy. Trong bài này sẽ giới thiệu về thư viện Shimmer để tạo animation khi load dữ liệu từ network về: demo content placeholder animation 1. Facebook's Shimmer library Để thêm shimmer effect vào ứng dụng ta sẽ để ...

Thông thường ta hay sử dụng spinner loader khi ứng dụng lấy dữ liệu từ network về máy. Trong bài này sẽ giới thiệu về thư viện Shimmer để tạo animation khi load dữ liệu từ network về: demo content placeholder animation 1. Facebook's Shimmer library

  • Để thêm shimmer effect vào ứng dụng ta sẽ để layout bên trong layout ShimmerFrameLayout. Để start animation ta gọi phương thức startShimmerAnimation().
  • Tìm hiểu thêm về Shimmer library
  • Dưới đây là đoạn code minh họa:
<com.facebook.shimmer.ShimmerFrameLayout
     android:id=“@+id/shimmer_view_container”
     android:layout_awidth=“wrap_content”
     android:layout_height="wrap_content"
     shimmer:duration="1000">
 
     <View
        android:layout_awidth="100dp"
        android:layout_height="8dp"
        android:background="#dddddd" />
 
</com.facebook.shimmer.ShimmerFrameLayout>
  • start animation:
ShimmerFrameLayout shimmerContainer = (ShimmerFrameLayout) findViewById(R.id.shimmer_view_container);
shimmerContainer.startShimmerAnimation();

2. Sample JSON

[{
    "id": 1,
    "name": "Salmon Teriyaki",
    "description": "Put the ginger and garlic into a bowl and mix with the soy sauce, maple syrup, mirin and a drizzle of olive oil",
    "price": 140,
    "chef": "Gordon Ramsay",
    "thumbnail": "https://api.androidhive.info/images/food/1.jpg",
    "timestamp": "2 min ago"
}, {
    "id": 2,
    "name": "Grilled Mushroom",
    "description": "Combine butter, dill and garlic salt, brush over mushrooms.",
    "price": 150,
    "chef": "Ravi Tamada",
    "thumbnail": "https://api.androidhive.info/images/food/2.jpg",
    "timestamp": "5 min ago"
}
]

3. Ví dụ

  • Thêm thư viện Shimmer vào project
build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
 
    // Shimmer
    implementation 'com.facebook.shimmer:shimmer:0.1.0@aar'
}
  • resource:
colors.xml
 
<resources>
    <color name="colorPrimary">#d91248</color>
    <color name="colorPrimaryDark">#d91248</color>
    <color name="colorAccent">#3ad23e</color>
    <color name="placeholder_bg">#dddddd</color>
    <color name="item_name">#0c0c0c</color>
    <color name="description">#1a1a1a</color>
    <color name="chef">#777</color>
    <color name="timestamp">#777</color>
</resources>
dimens.xml
 
<resources>
    <dimen name="activity_padding">16dp</dimen>
    <dimen name="placeholder_image">50dp</dimen>
    <dimen name="placeholder_text_height">8dp</dimen>
    <dimen name="activity_padding_horizontal">16dp</dimen>
    <dimen name="padding_10">10dp</dimen>
    <dimen name="name">15dp</dimen>
    <dimen name="chef">12dp</dimen>
    <dimen name="timestamp">11dp</dimen>
    <dimen name="description">15dp</dimen>
    <dimen name="price">13dp</dimen>
</resources>
  • Tạo layout recipe_placeholder_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_awidth="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/activity_padding">
 
    <View
        android:id="@+id/thumbnail"
        android:layout_awidth="@dimen/placeholder_image"
        android:layout_height="@dimen/placeholder_image"
        android:layout_marginRight="@dimen/activity_padding"
        android:background="@color/placeholder_bg" />
 
    <View
        android:id="@+id/name"
        android:layout_awidth="150dp"
        android:layout_height="10dp"
        android:layout_marginBottom="10dp"
        android:layout_toRightOf="@id/thumbnail"
        android:background="@color/placeholder_bg" />
 
    <View
        android:layout_awidth="100dp"
        android:layout_height="@dimen/placeholder_text_height"
        android:layout_below="@id/name"
        android:layout_toRightOf="@id/thumbnail"
        android:background="@color/placeholder_bg" />
 
    <LinearLayout
        android:layout_awidth="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/thumbnail"
        android:layout_marginBottom="40dp"
        android:layout_marginTop="20dp"
        android:orientation="vertical">
 
        <View
            android:layout_awidth="match_parent"
            android:layout_height="@dimen/placeholder_text_height"
            android:layout_marginRight="100dp"
            android:background="@color/placeholder_bg" />
 
        <View
            android:layout_awidth="match_parent"
            android:layout_height="@dimen/placeholder_text_height"
            android:layout_marginRight="50dp"
            android:layout_marginTop="10dp"
            android:background="@color/placeholder_bg" />
 
        <View
            android:layout_awidth="match_parent"
            android:layout_height="@dimen/placeholder_text_height"
            android:layout_marginRight="160dp"
            android:layout_marginTop="10dp"
            android:background="@color/placeholder_bg" />
 
    </LinearLayout>
 
</RelativeLayout>
  • Tiếp theo trong activity_main.xml ta thêm placeholder layout ta vừa tạo ở trên
activity_main.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"
    xmlns:shimmer="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_awidth="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">
 
    <com.facebook.shimmer.ShimmerFrameLayout
        android:id="@+id/shimmer_view_container"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        shimmer:duration="800">
 
         
        <LinearLayout
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
 
            <include layout="@layout/recipe_placeholder_item" />
 
            <include layout="@layout/recipe_placeholder_item" />
 
            <include layout="@layout/recipe_placeholder_item" />
 
        </LinearLayout>
 
    </com.facebook.shimmer.ShimmerFrameLayout>
 
</android.support.constraint.ConstraintLayout>
  • Tiếp theo trong MainActivity.java ta sẽ start animation ở onResume và pause ở method onPause.
MainActivity.java
import com.facebook.shimmer.ShimmerFrameLayout;
 
public class MainActivity extends AppCompatActivity {
 
    private ShimmerFrameLayout mShimmerViewContainer;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        mShimmerViewContainer = findViewById(R.id.shimmer_view_container);
    }
 
    @Override
    public void onResume() {
        super.onResume();
        mShimmerViewContainer.startShimmerAnimation();
    }
 
    @Override
    public void onPause() {
        mShimmerViewContainer.stopShimmerAnimation();
        super.onPause();
    }
}

4. Loading dữ liệu từ JSON và ẩn shimmer effect

  • Thêm một số thư viện trong file build.gradle
build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    // ...
 
    // Shimmer
    implementation 'com.facebook.shimmer:shimmer:0.1.0@aar'
 
    // RecyclerView
    implementation 'com.android.support:recyclerview-v7:26.1.0'
 
    // glide image library
    implementation 'com.github.bumptech.glide:glide:3.7.0'
 
    // volley http library
    implementation 'com.android.volley:volley:1.0.0'
    implementation 'com.google.code.gson:gson:2.6.2'
}
  • Tạo file custom Application để khởi tạo Volley
MyApplication.java
 
import android.app.Application;
import android.text.TextUtils;
 
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
 
public class MyApplication extends Application {
 
    public static final String TAG = MyApplication.class
            .getSimpleName();
 
    private RequestQueue mRequestQueue;
 
    private static MyApplication mInstance;
 
    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
    }
 
    public static synchronized MyApplication getInstance() {
        return mInstance;
    }
 
    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        }
 
        return mRequestQueue;
    }
 
    public <T> void addToRequestQueue(Request<T> req, String tag) {
        // set the default tag if tag is empty
        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
        getRequestQueue().add(req);
    }
 
    public <T> void addToRequestQueue(Request<T> req) {
        req.setTag(TAG);
        getRequestQueue().add(req);
    }
 
    public void cancelPendingRequests(Object tag) {
        if (mRequestQueue != null) {
            mRequestQueue.cancelAll(tag);
        }
    }
}
  • Thêm permission và Application vừa tạo ở trên vào file AndroidManifest.xml
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="example.android.shimmer">
 
    <uses-permission android:name="android.permission.INTERNET"/>
 
     
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:name=".MyApplication"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>
  • Tạo layout cho MainActivity
activity_main.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"
    xmlns:shimmer="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_awidth="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">
 
    <com.facebook.shimmer.ShimmerFrameLayout
        android:id="@+id/shimmer_view_container"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        shimmer:duration="800">
 
         
        <LinearLayout
            android:layout_awidth="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
 
            <include layout="@layout/layout_placeholder_row" />
 
            <include layout="@layout/layout_placeholder_row" />
 
            <include layout="@layout/layout_placeholder_row" />
 
        </LinearLayout>
 
    </com.facebook.shimmer.ShimmerFrameLayout>
 
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_awidth="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="vertical" />
 
</android.support.constraint.ConstraintLayout>

Ở đây layout trong ShimmerFrameLayout sẽ thực hiện shimmer effect, còn RecyclerView là layout chính của ứng dụng sẽ hiển thị list dữ liệu

  • Tiếp theo ta cần tạo POJO class để load dữ liệu từ network
Recipe.java
 
public class Recipe {
    int id;
    String name;
    String description;
    double price;
    String thumbnail;
    String chef;
    String timestamp;
 
    public Recipe() {
    }
 
    public int getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public String getDescription() {
        return description;
    }
 
    public double getPrice() {
        return price;
    }
 
    public String getThumbnail() {
        return thumbnail;
    }
 
    public String getChef() {
        return chef;
    }
 
    public String getTimestamp() {
        return timestamp;
    }
}
  • Tiếp theo ta tạo layout cho mỗi item của RecyclerView
recipe_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_awidth="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:clickable="true"
    android:foreground="?attr/selectableItemBackground"
    android:padding="@dimen/activity_padding_horizontal">
 
    <ImageView
        android:id="@+id/thumbnail"
        android:layout_awidth="@dimen/placeholder_image"
        android:layout_height="@dimen/placeholder_image"
        android:layout_marginRight="@dimen/padding_10"
        android:scaleType="centerCrop" />
 
    <TextView
        android:id="@+id/name"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/thumbnail"
        android:ellipsize="end"
        android:fontFamily="sans-serif-medium"
        android:maxLines="1"
        android:textColor="@color/item_name"
        android:textSize="@dimen/name" />
 
    <TextView
        android:id="@+id/chef"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_toRightOf="@id/thumbnail"
        android:maxLines="1"
        android:textColor="@color/chef"
        android:textSize="@dimen/chef" />
 
    <TextView
        android:id="@+id/timestamp"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/chef"
        android:layout_toRightOf="@id/thumbnail"
        android:maxLines="1"
        android:text="2 min ago"
        android:textColor="@color/timestamp"
        android:textSize="@dimen/timestamp" />
 
    <TextView
        android:id="@+id/description"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/thumbnail"
        android:layout_marginTop="@dimen/activity_padding_horizontal"
        android:ellipsize="end"
        android:maxLines="3"
        android:textColor="@color/description"
        android:textSize="@dimen/description" />
 
    <TextView
        android:id="@+id/price"
        android:layout_awidth="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/description"
        android:layout_marginTop="@dimen/padding_10"
        android:textColor="@color/colorPrimary"
        android:textSize="@dimen/price"
        android:textStyle="bold" />
 
</RelativeLayout>
  • Tiếp theo ta tạo Adapter cho RecyclerView
RecipeListAdapter.java
 
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
 
import com.bumptech.glide.Glide;
 
import java.util.List;
 
public class RecipeListAdapter extends RecyclerView.Adapter<RecipeListAdapter.MyViewHolder> {
    private Context context;
    private List<Recipe> cartList;
 
    public class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView name, description, price, chef, timestamp;
        public ImageView thumbnail;
 
        public MyViewHolder(View view) {
            super(view);
            name = view.findViewById(R.id.name);
            chef = view.findViewById(R.id.chef);
            description = view.findViewById(R.id.description);
            price = view.findViewById(R.id.price);
            thumbnail = view.findViewById(R.id.thumbnail);
            timestamp = view.findViewById(R.id.timestamp);
        }
    }
 
 
    public RecipeListAdapter(Context context, List<Recipe> cartList) {
        this.context = context;
        this.cartList = cartList;
    }
 
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.recipe_list_item, parent, false);
 
        return new MyViewHolder(itemView);
    }
 
    @Override
    public void onBindViewHolder(MyViewHolder holder, final int position) {
        final Recipe recipe = cartList.get(position);
        holder.name.setText(recipe.getName());
        holder.chef.setText("By " + recipe.getChef());
        holder.description.setText(recipe.getDescription());
        holder.price.setText("Price: ₹" + recipe.getPrice());
        holder.timestamp.setText(recipe.getTimestamp());
 
        Glide.with(context)
                .load(recipe.getThumbnail())
                .into(holder.thumbnail);
    }
    // recipe
    @Override
    public int getItemCount() {
        return cartList.size();
    }
}
  • Cuối cùng, ta hiện thị Shimmer effect khi thực hiện network call, và ẩn Shimmer effect khi finish network call trong MainActivity.java
MainActivity.java
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
 
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.facebook.shimmer.ShimmerFrameLayout;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
 
import org.json.JSONArray;
 
import java.util.ArrayList;
import java.util.List;
 
public class MainActivity extends AppCompatActivity {
 
    private static final String TAG = MainActivity.class.getSimpleName();
    private RecyclerView recyclerView;
    private List<Recipe> cartList;
    private RecipeListAdapter mAdapter;
 
    private ShimmerFrameLayout mShimmerViewContainer;
 
    // URL to fetch menu json
    // this endpoint takes 2 sec before giving the response to add
    // some delay to test the Shimmer effect
    private static final String URL = "https://api.androidhive.info/json/shimmer/menu.php";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        mShimmerViewContainer = findViewById(R.id.shimmer_view_container);
 
        recyclerView = findViewById(R.id.recycler_view);
        cartList = new ArrayList<>();
        mAdapter = new RecipeListAdapter(this, cartList);
 
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.addItemDecoration(new MyDividerItemDecoration(this, LinearLayoutManager.VERTICAL, 16));
        recyclerView.setAdapter(mAdapter);
 
        // making http call and fetching menu json
        fetchRecipes();
    }
 
    /**
     * method make volley network call and parses json
     */
    private void fetchRecipes() {
        JsonArrayRequest request = new JsonArrayRequest(URL,
                new Response.Listener<JSONArray>() {
                    @Override
                    public void onResponse(JSONArray response) {
                        if (response == null) {
                            Toast.makeText(getApplicationContext(), "Couldn't fetch the menu! Pleas try again.", Toast.LENGTH_LONG).show();
                            return;
                        }
 
                        List<Recipe> recipes = new Gson().fromJson(response.toString(), new TypeToken<List<Recipe>>() {
                        }.getType());
 
                        // adding recipes to cart list
                        cartList.clear();
                        cartList.addAll(recipes);
 
                        // refreshing recycler view
                        mAdapter.notifyDataSetChanged();
 
                        // stop animating Shimmer and hide the layout
                        mShimmerViewContainer.stopShimmerAnimation();
                        mShimmerViewContainer.setVisibility(View.GONE);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // error in getting json
                Log.e(TAG, "Error: " + error.getMessage());
                Toast.makeText(getApplicationContext(), "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
 
        MyApplication.getInstance().addToRequestQueue(request);
    }
 
    @Override
    public void onResume() {
        super.onResume();
        mShimmerViewContainer.startShimmerAnimation();
    }
 
    @Override
    public void onPause() {
        mShimmerViewContainer.stopShimmerAnimation();
        super.onPause();
    }
}

source:

0