(译)使用RecyclerView实现gmail的收件箱效果

这篇文章讲解如何使用RecyclerView实现Gmail收件箱界面的效果。

1.概述

Gmail app的收件箱界面并不光是用RecyclerView实现的,还需要结合其它的view。总的来说我们将使用以下的控件来实现界面和功能。

> RecyclerView

RecyclerView是这所需的最基本控件。我们用它来显示头像,三行消息,时间,star图标(将消息标记为重要)。

> SwipeRefreshLayout

SwipeRefreshLayout用来包裹RecyclerView,实现下拉刷新。

> ActionMode

ActionMode 用于在长按item的时候显示上下文菜单(toolbar)。它可以让我们在RecyclerView处于多选模式的时候展示不同图标的toolbar。这里我们提供的是一个删除菜单。

> Object Animators

Object Animators 用于对目标元素做动画。这里我们使用属性动画来执行长按之后缩略图的翻转动画。

> Retrofit

真实的app中,所有的消息都是动态的,比如从一个REST API获取的数据。为此我们使用了一个 JSON url来模拟数据。我们使用 Retrofit 库来获取和解析JSON。

demo-gif-compressed.gif

2. Sample JSON for Inbox Messages

我在后端创建了一个返回JSON格式数据的API。这个JSON包含了头像,来源地,主题,消息,时间戳以及其它的信息。真实场景中这些数据都是使用服务端语言从数据库中取出来的。

http://api.androidhive.info/json/inbox.json

\[
  {
    "id": 1,
    "isImportant": false,
    "picture": "http://api.androidhive.info/json/google.png",
    "from": "Google Alerts",
    "subject": "Google Alert - android",
    "message": "Android N update is released to Nexus Family!",
    "timestamp": "10:30 AM",
    "isRead": false
  },
  .
  .
  .
\]

3.创建一个新的工程

我们从新建一个项目开始,然后做基本的设置。下面是项目的代码结构:

android-gmail-project-structure.png

1.创建的时候,我们选择BasicActivity作为默认的activity,以便获得Toolbar, FAB等元素。

2. 在app module下的 build.gradle中添加RecyclerView, Retrofit 以及 Glide 的依赖,然后Sync项目。

dependencies {
    compile fileTree(dir: 'libs', include: \['*.jar'\])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha7'
    compile 'com.android.support:design:24.2.1'
    testCompile 'junit:junit:4.12'
 
    // RecyclerView
    compile 'com.android.support:recyclerview-v7:24.2.1'
 
    // retrofit, gson
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
 
    // glide
    compile 'com.github.bumptech.glide:glide:3.7.0'
}

3. 下载这个 res 文件夹,并把它里面的内容复制到你项目的res目录中。里面包含了RecyclerView和Toolbar所需的所有资源文件。

4. 在相应的文件中添加下面的color,string和dimen

colors.xml

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#db4437</color>
    <color name="colorPrimaryDark">#b93221</color>
    <color name="colorAccent">#FFFFFF</color>
    <color name="from">#000000</color>
    <color name="subject">#111111</color>
    <color name="timestamp">#4285f4</color>
    <color name="message">#7a7a7a</color>
    <color name="icon_tint_normal">#7a7a7a</color>
    <color name="icon_tint_selected">#fed776</color>
    <color name="row_activated">#e0e0e0</color>
    <color name="bg_action_mode">#757575</color>
    <color name="bg_circle_default">#666666</color>
</resources>

dimens.xml

dimens.xml
<resources>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="padding_list_row">16dp</dimen>
    <dimen name="messages_padding_left">72dp</dimen>
    <dimen name="icon_width_height">40dp</dimen>
    <dimen name="msg_text_primary">16sp</dimen>
    <dimen name="msg_text_secondary">14sp</dimen>
    <dimen name="icon_star">25dp</dimen>
    <dimen name="icon_text">22dp</dimen>
    <dimen name="timestamp">12dp</dimen>
</resources>

strings.xml

strings.xml
<resources>
    <string name="app_name">Gmail</string>
    <string name="action_settings">Settings</string>
    <string name="action_search">Search</string>
    <string name="action_delete">Delete</string>
</resources>

5. 打开 styles.xml 并添加如下的styles。这里的 windowActionModeOverlay是为了让ActionMode叠加在Toolbar上面。 

styles.xml
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="windowActionModeOverlay">true</item>
        <item name="android:actionModeBackground">@color/bg_action_mode</item>
    </style>
 
    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
 
    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

6. 因为要使用网络,因此需要在manifest中申请权限。打开AndroidManifest.xml添加权限。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.gmail">
 
    <uses-permission android:name="android.permission.INTERNET" />
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".activity.MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

7. 创建5个包,分别命名 activity, adapter, helper, model and network。创建好了之后,把MainActivity移动到activity包之下。

4. 添加 Retrofit – 获取 JSON

现在项目的基本资源就准备好了。我们使用Retrofit来处理网络层。如果你对Retrofit不熟悉,强烈推荐看一遍我关于Retrofit的前一篇文章。

8. 在model在model包中,创建名为 Message.java的类。这个类解析时反序列化json。

Message.java
package info.androidhive.gmail.model;
 
public class Message {
    private int id;
    private String from;
    private String subject;
    private String message;
    private String timestamp;
    private String picture;
    private boolean isImportant;
    private boolean isRead;
    private int color = -1;
 
    public Message() {
    }
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getFrom() {
        return from;
    }
 
    public void setFrom(String from) {
        this.from = from;
    }
 
    public String getSubject() {
        return subject;
    }
 
    public void setSubject(String subject) {
        this.subject = subject;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public String getTimestamp() {
        return timestamp;
    }
 
    public void setTimestamp(String timestamp) {
        this.timestamp = timestamp;
    }
 
    public boolean isImportant() {
        return isImportant;
    }
 
    public void setImportant(boolean important) {
        isImportant = important;
    }
 
    public String getPicture() {
        return picture;
    }
 
    public void setPicture(String picture) {
        this.picture = picture;
    }
 
    public boolean isRead() {
        return isRead;
    }
 
    public void setRead(boolean read) {
        isRead = read;
    }
 
    public int getColor() {
        return color;
    }
 
    public void setColor(int color) {
        this.color = color;
    }
}

9. 在network包下面,创建一个名为 ApiClient.java的类。这个类用于创建静态的retrofit实例。

ApiClient.java
package info.androidhive.gmail.network;
 
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
 
 
public class ApiClient {
    public static final String BASE_URL = "http://api.androidhive.info/json/";
    private static Retrofit retrofit = null;
     
    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

10. 在network包下,创建ApiInterface.java类。这个类包含了请求的接口,这里我们只有一个inbox.json接口。

ApiInterface.java
package info.androidhive.gmail.network;
 
import java.util.List;
 
import info.androidhive.gmail.model.Message;
import retrofit2.Call;
import retrofit2.http.GET;
 
public interface ApiInterface {
 
    @GET("inbox.json")
    Call<List<Message>> getInbox();
 
}

这就完成了retrofit的集成。现在我们添加一些helper类来帮助渲染list。

11. 在helper包下,创建一个名为CircleTransform.java的类。这个类用Glide显示圆形的缩略图。

CircleTransform.java
package info.androidhive.gmail.helper;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
 
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
 
public class CircleTransform  extends BitmapTransformation {
    public CircleTransform(Context context) {
        super(context);
    }
 
    @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
        return circleCrop(pool, toTransform);
    }
 
    private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
        if (source == null) return null;
 
        int size = Math.min(source.getWidth(), source.getHeight());
        int x = (source.getWidth() - size) / 2;
        int y = (source.getHeight() - size) / 2;
 
        // TODO this could be acquired from the pool too
        Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
 
        Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
        if (result == null) {
            result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        }
 
        Canvas canvas = new Canvas(result);
        Paint paint = new Paint();
        paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
        paint.setAntiAlias(true);
        float r = size / 2f;
        canvas.drawCircle(r, r, r, paint);
        return result;
    }
 
    @Override public String getId() {
        return getClass().getName();
    }
}

12.包之下,创建另一个名为DividerItemDecoration.java的类。为recycler view添加分割线。

DividerItemDecoration
package info.androidhive.gmail.helper;
 
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
 
/**
 * Created by Ravi Tamada on 21/02/17.
 * www.androidhive.info
 */
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
 
    private static final int\[\] ATTRS = new int\[\]{
            android.R.attr.listDivider
    };
 
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    private Drawable mDivider;
    private int mOrientation;
 
    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }
 
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }
 
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
 
    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
 
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
 
    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
 
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
 
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

5. 随机生成Material Color

这里的另一个有趣的事情是,为每行的图标设置一个随机的背景色。为此我们需要预先定义一套material color的数组,然后然后在RecyclerView准备好的时候随机的选择一个颜色。感谢daniellevass提供这些颜色代码。

13. 在res ⇒ values下创建array.xml 。这个xml包含了将要在list中随机加载的 material color。

array.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="mdcolor_400">
        <item name="red_400" type="color">#e84e40</item>
        <item name="pink_400" type="color">#ec407a</item>
        <item name="purple_400" type="color">#ab47bc</item>
        <item name="deep_purple_400" type="color">#7e57c2</item>
        <item name="indigo_400" type="color">#5c6bc0</item>
        <item name="blue_400" type="color">#738ffe</item>
        <item name="light_blue_400" type="color">#29b6f6</item>
        <item name="cyan_400" type="color">#26c6da</item>
        <item name="teal_400" type="color">#26a69a</item>
        <item name="green_400" type="color">#2baf2b</item>
        <item name="light_green_400" type="color">#9ccc65</item>
        <item name="lime_400" type="color">#d4e157</item>
        <item name="yellow_400" type="color">#ffee58</item>
        <item name="orange_400" type="color">#ffa726</item>
        <item name="deep_orange_400" type="color">#ff7043</item>
        <item name="brown_400" type="color">#8d6e63</item>
        <item name="grey_400" type="color">#bdbdbd</item>
        <item name="blue_grey_400" type="color">#78909c</item>
    </array>
    <array name="mdcolor_500">
        <item name="red_500" type="color">#e51c23</item>
        <item name="pink_500" type="color">#e91e63</item>
        <item name="purple_500" type="color">#9c27b0</item>
        <item name="deep_purple_500" type="color">#673ab7</item>
        <item name="indigo_500" type="color">#3f51b5</item>
        <item name="blue_500" type="color">#5677fc</item>
        <item name="light_blue_500" type="color">#03a9f4</item>
        <item name="cyan_500" type="color">#00bcd4</item>
        <item name="teal_500" type="color">#009688</item>
        <item name="green_500" type="color">#259b24</item>
        <item name="light_green_500" type="color">#8bc34a</item>
        <item name="lime_500" type="color">#cddc39</item>
        <item name="yellow_500" type="color">#ffeb3b</item>
        <item name="orange_500" type="color">#ff9800</item>
        <item name="deep_orange_500" type="color">#ff5722</item>
        <item name="brown_500" type="color">#795548</item>
        <item name="grey_500" type="color">#9e9e9e</item>
        <item name="blue_grey_500" type="color">#607d8b</item>
    </array>
</resources>

要随机加载这些颜色,可以使用下面的函数。马上你就可以看到如何使用这个函数。

private int getRandomMaterialColor(String typeColor) {
    int returnColor = Color.GRAY;
    int arrayId = getResources().getIdentifier("mdcolor_" + typeColor, "array", getPackageName());
 
    if (arrayId != 0) {
        TypedArray colors = getResources().obtainTypedArray(arrayId);
        int index = (int) (Math.random() * colors.length());
        returnColor = colors.getColor(index, Color.GRAY);
        colors.recycle();
    }
    return returnColor;
}

6. 使用属性动画实现翻转动画

如果你观察gmail应用,当你长按一行的时候,缩略图图标会显示一个翻转动画,显示图标的另一面。我们可以使用ObjectAnimator做同样的事情。在你的项目中仔细创建下面提到的文件。

14.在res ⇒ values下,创建一个 integer.xml。我们在这里定义动画的持续时间。

integer.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="card_flip_time_full">500</integer>
    <integer name="card_flip_time_half">200</integer>
</resources>

15.在res目录下创建一个名为animator的目录。在这个目录中我们存放与动画相关的所有xml资源。

16.  在animator目录下,我们创建card_flip_left_in.xml, card_flip_left_out.xml, card_flip_right_in.xml and card_flip_right_out.xml。card_flip_left_in.xml

card_flip_left_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />
 
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="-180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />
 
    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_left_out.xml

card_flip_left_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />
 
    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_in.xml

card_flip_right_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />
 
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />
 
    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_out.xml

card_flip_right_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />
 
    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

17. 在helper包下,创建一个名为FlipAnimator.java的类。这个类有一个执行翻转动画的静态方法 flipView() 。

FlipAnimator.java
package info.androidhive.gmail.helper;
 
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.content.Context;
import android.view.View;
 
import info.androidhive.gmail.R;
 
public class FlipAnimator {
    private static String TAG = FlipAnimator.class.getSimpleName();
    private static AnimatorSet leftIn, rightOut, leftOut, rightIn;
 
    /**
     * Performs flip animation on two views
     */
    public static void flipView(Context context, final View back, final View front, boolean showFront) {
        leftIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_left_in);
        rightOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_right_out);
        leftOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_left_out);
        rightIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, R.animator.card_flip_right_in);
 
        final AnimatorSet showFrontAnim = new AnimatorSet();
        final AnimatorSet showBackAnim = new AnimatorSet();
 
        leftIn.setTarget(back);
        rightOut.setTarget(front);
        showFrontAnim.playTogether(leftIn, rightOut);
 
        leftOut.setTarget(back);
        rightIn.setTarget(front);
        showBackAnim.playTogether(rightIn, leftOut);
 
        if (showFront) {
            showFrontAnim.start();
        } else {
            showBackAnim.start();
        }
    }
}

7. 渲染RecyclerView中的信箱

终于到了本文的关键部分-渲染列表。

现在让我们来创建几个RecyclerView所需要的文件。我们所需要的所有文件:main activity的布局文件,列表的item,背景drawable以及一个adapter类。

18. 在 res ⇒ drawable下,创建两个drawable资源,分别是bg_circle.xml 和 bg_list_row.xml。

bg_circle.xml (为缩略图提供背景色)

bg_circle.xml
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
 
    <solid
        android:color="@color/bg_circle_default"/>
 
    <size
        android:width="120dp"
        android:height="120dp"/>
</shape>

bg_list_row.xml (为列表item的普通状态和按下状态提供背景色)

bg_list_row.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/row_activated" android:state_activated="true" />
    <item android:drawable="@android:color/transparent" />
</selector>

19. 打开main activity的布局文件 (content_main.xml),并添加RecyclerView。

activity_main.xml

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:mContext="info.androidhive.gmail.activity.MainActivity">
 
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">
 
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />
 
    </android.support.design.widget.AppBarLayout>
 
    <include layout="@layout/content_main" />
 
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="@color/colorPrimary"
        app:srcCompat="@drawable/ic_edit_white_24dp" />
 
</android.support.design.widget.CoordinatorLayout>

content_main.xml

content_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:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:mContext="info.androidhive.gmail.activity.MainActivity"
    tools:showIn="@layout/activity_main">
 
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
 
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical" />
 
    </android.support.v4.widget.SwipeRefreshLayout>
 
</android.support.constraint.ConstraintLayout>

20. 在res ⇒ layout下,用下面的代码创建一个message_list_row.xml。这个布局用于显示列表的行。

message_list_row.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_list_row"
    android:clickable="true"
    android:focusable="true"
    android:orientation="vertical"
    android:paddingBottom="@dimen/padding_list_row"
    android:paddingLeft="?listPreferredItemPaddingLeft"
    android:paddingRight="?listPreferredItemPaddingRight"
    android:paddingTop="@dimen/padding_list_row">
 
    <LinearLayout
        android:id="@+id/message_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:orientation="vertical"
        android:paddingLeft="72dp"
        android:paddingRight="@dimen/padding_list_row">
 
        <TextView
            android:id="@+id/from"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/from"
            android:textSize="@dimen/msg_text_primary"
            android:textStyle="bold" />
 
        <TextView
            android:id="@+id/txt_primary"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/subject"
            android:textSize="@dimen/msg_text_secondary"
            android:textStyle="bold" />
 
        <TextView
            android:id="@+id/txt_secondary"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:lines="1"
            android:textColor="@color/message"
            android:textSize="@dimen/msg_text_secondary" />
 
    </LinearLayout>
 
    <RelativeLayout
        android:id="@+id/icon_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
 
        <RelativeLayout
            android:id="@+id/icon_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
            <ImageView
                android:layout_width="@dimen/icon_width_height"
                android:layout_height="@dimen/icon_width_height"
                android:src="@drawable/bg_circle" />
 
            <ImageView
                android:layout_width="25dp"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:src="@drawable/ic_done_white_24dp" />
        </RelativeLayout>
 
        <RelativeLayout
            android:id="@+id/icon_front"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
            <ImageView
                android:id="@+id/icon_profile"
                android:layout_width="@dimen/icon_width_height"
                android:layout_height="@dimen/icon_width_height" />
 
            <TextView
                android:id="@+id/icon_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@android:color/white"
                android:textSize="@dimen/icon_text" />
        </RelativeLayout>
 
    </RelativeLayout>
 
    <TextView
        android:id="@+id/timestamp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:textColor="@color/timestamp"
        android:textSize="@dimen/timestamp"
        android:textStyle="bold" />
 
    <ImageView
        android:id="@+id/icon_star"
        android:layout_width="@dimen/icon_star"
        android:layout_height="@dimen/icon_star"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:tint="@color/icon_tint_normal" />
 
</RelativeLayout>

我们还有两个渲染toolbar图标的menu文件。一个用于显示正常状态下的Toolbar 图标。另一个用于显示ActionMode启用时的图标。

21. 在res ⇒ menu目录下,创建两个menu文件,分别为menu_main.xml 和 menu_action_mode.xml。

menu_main.xml

menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:mContext="info.androidhive.gmail.activity.MainActivity">
    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search_white_24dp"
        android:orderInCategory="100"
        android:title="@string/action_search"
        app:showAsAction="always" />
</menu>

menu_action_mode.xml

menu_action_mode.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:mContext="info.androidhive.gmail.activity.MainActivity">
    <item
        android:id="@+id/action_delete"
        android:icon="@drawable/ic_delete_white_24dp"
        android:orderInCategory="100"
        android:title="@string/action_delete"
        app:showAsAction="always" />
</menu>

还有一个需要处理的类是adapter类,RecyclerView的功能完全取决于你如何高效的管理adapter类。

22.在adapter包下,创建 MessagesAdapter.java 然后拷贝下面的代码。这个类非常重要,花点时间来研究这段代码,左右的奇迹都发生在 onBindViewHolder() 方法中。

> applyReadStatus() 根据阅读状态决定是否设置粗体文字,未读状态粗体。

> applyImportant() – 如果消息标记为重要,star图标显示为黄色。

> applyIconAnimation() – 执行thumbnail图标的翻转动画。 

> applyProfilePicture() – 显示头像图片/或者圆形的背景 

MessagesAdapter.java
package info.androidhive.gmail.adapter;
 
import android.content.Context;
import android.graphics.Typeface;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
 
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
 
import java.util.ArrayList;
import java.util.List;
 
import info.androidhive.gmail.R;
import info.androidhive.gmail.helper.CircleTransform;
import info.androidhive.gmail.helper.FlipAnimator;
import info.androidhive.gmail.model.Message;
 
public class MessagesAdapter extends RecyclerView.Adapter<MessagesAdapter.MyViewHolder> {
    private Context mContext;
    private List<Message> messages;
    private MessageAdapterListener listener;
    private SparseBooleanArray selectedItems;
 
    // array used to perform multiple animation at once
    private SparseBooleanArray animationItemsIndex;
    private boolean reverseAllAnimations = false;
 
    // index is used to animate only the selected row
    // dirty fix, find a better solution
    private static int currentSelectedIndex = -1;
 
    public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
        public TextView from, subject, message, iconText, timestamp;
        public ImageView iconImp, imgProfile;
        public LinearLayout messageContainer;
        public RelativeLayout iconContainer, iconBack, iconFront;
 
        public MyViewHolder(View view) {
            super(view);
            from = (TextView) view.findViewById(R.id.from);
            subject = (TextView) view.findViewById(R.id.txt_primary);
            message = (TextView) view.findViewById(R.id.txt_secondary);
            iconText = (TextView) view.findViewById(R.id.icon_text);
            timestamp = (TextView) view.findViewById(R.id.timestamp);
            iconBack = (RelativeLayout) view.findViewById(R.id.icon_back);
            iconFront = (RelativeLayout) view.findViewById(R.id.icon_front);
            iconImp = (ImageView) view.findViewById(R.id.icon_star);
            imgProfile = (ImageView) view.findViewById(R.id.icon_profile);
            messageContainer = (LinearLayout) view.findViewById(R.id.message_container);
            iconContainer = (RelativeLayout) view.findViewById(R.id.icon_container);
            view.setOnLongClickListener(this);
        }
 
        @Override
        public boolean onLongClick(View view) {
            listener.onRowLongClicked(getAdapterPosition());
            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            return true;
        }
    }
 
 
    public MessagesAdapter(Context mContext, List<Message> messages, MessageAdapterListener listener) {
        this.mContext = mContext;
        this.messages = messages;
        this.listener = listener;
        selectedItems = new SparseBooleanArray();
        animationItemsIndex = new SparseBooleanArray();
    }
 
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.message_list_row, parent, false);
 
        return new MyViewHolder(itemView);
    }
 
    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        Message message = messages.get(position);
 
        // displaying text view data
        holder.from.setText(message.getFrom());
        holder.subject.setText(message.getSubject());
        holder.message.setText(message.getMessage());
        holder.timestamp.setText(message.getTimestamp());
 
        // displaying the first letter of From in icon text
        holder.iconText.setText(message.getFrom().substring(0, 1));
 
        // change the row state to activated
        holder.itemView.setActivated(selectedItems.get(position, false));
 
        // change the font style depending on message read status
        applyReadStatus(holder, message);
 
        // handle message star
        applyImportant(holder, message);
 
        // handle icon animation
        applyIconAnimation(holder, position);
 
        // display profile image
        applyProfilePicture(holder, message);
 
        // apply click events
        applyClickEvents(holder, position);
    }
 
    private void applyClickEvents(MyViewHolder holder, final int position) {
        holder.iconContainer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onIconClicked(position);
            }
        });
 
        holder.iconImp.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onIconImportantClicked(position);
            }
        });
 
        holder.messageContainer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onMessageRowClicked(position);
            }
        });
 
        holder.messageContainer.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                listener.onRowLongClicked(position);
                view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
                return true;
            }
        });
    }
 
    private void applyProfilePicture(MyViewHolder holder, Message message) {
        if (!TextUtils.isEmpty(message.getPicture())) {
            Glide.with(mContext).load(message.getPicture())
                    .thumbnail(0.5f)
                    .crossFade()
                    .transform(new CircleTransform(mContext))
                    .diskCacheStrategy(DiskCacheStrategy.ALL)
                    .into(holder.imgProfile);
            holder.imgProfile.setColorFilter(null);
            holder.iconText.setVisibility(View.GONE);
        } else {
            holder.imgProfile.setImageResource(R.drawable.bg_circle);
            holder.imgProfile.setColorFilter(message.getColor());
            holder.iconText.setVisibility(View.VISIBLE);
        }
    }
 
    private void applyIconAnimation(MyViewHolder holder, int position) {
        if (selectedItems.get(position, false)) {
            holder.iconFront.setVisibility(View.GONE);
            resetIconYAxis(holder.iconBack);
            holder.iconBack.setVisibility(View.VISIBLE);
            holder.iconBack.setAlpha(1);
            if (currentSelectedIndex == position) {
                FlipAnimator.flipView(mContext, holder.iconBack, holder.iconFront, true);
                resetCurrentIndex();
            }
        } else {
            holder.iconBack.setVisibility(View.GONE);
            resetIconYAxis(holder.iconFront);
            holder.iconFront.setVisibility(View.VISIBLE);
            holder.iconFront.setAlpha(1);
            if ((reverseAllAnimations && animationItemsIndex.get(position, false)) || currentSelectedIndex == position) {
                FlipAnimator.flipView(mContext, holder.iconBack, holder.iconFront, false);
                resetCurrentIndex();
            }
        }
    }
 
 
    // As the views will be reused, sometimes the icon appears as
    // flipped because older view is reused. Reset the Y-axis to 0
    private void resetIconYAxis(View view) {
        if (view.getRotationY() != 0) {
            view.setRotationY(0);
        }
    }
 
    public void resetAnimationIndex() {
        reverseAllAnimations = false;
        animationItemsIndex.clear();
    }
 
    @Override
    public long getItemId(int position) {
        return messages.get(position).getId();
    }
 
    private void applyImportant(MyViewHolder holder, Message message) {
        if (message.isImportant()) {
            holder.iconImp.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_star_black_24dp));
            holder.iconImp.setColorFilter(ContextCompat.getColor(mContext, R.color.icon_tint_selected));
        } else {
            holder.iconImp.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_star_border_black_24dp));
            holder.iconImp.setColorFilter(ContextCompat.getColor(mContext, R.color.icon_tint_normal));
        }
    }
 
    private void applyReadStatus(MyViewHolder holder, Message message) {
        if (message.isRead()) {
            holder.from.setTypeface(null, Typeface.NORMAL);
            holder.subject.setTypeface(null, Typeface.NORMAL);
            holder.from.setTextColor(ContextCompat.getColor(mContext, R.color.subject));
            holder.subject.setTextColor(ContextCompat.getColor(mContext, R.color.message));
        } else {
            holder.from.setTypeface(null, Typeface.BOLD);
            holder.subject.setTypeface(null, Typeface.BOLD);
            holder.from.setTextColor(ContextCompat.getColor(mContext, R.color.from));
            holder.subject.setTextColor(ContextCompat.getColor(mContext, R.color.subject));
        }
    }
 
    @Override
    public int getItemCount() {
        return messages.size();
    }
 
    public void toggleSelection(int pos) {
        currentSelectedIndex = pos;
        if (selectedItems.get(pos, false)) {
            selectedItems.delete(pos);
            animationItemsIndex.delete(pos);
        } else {
            selectedItems.put(pos, true);
            animationItemsIndex.put(pos, true);
        }
        notifyItemChanged(pos);
    }
 
    public void clearSelections() {
        reverseAllAnimations = true;
        selectedItems.clear();
        notifyDataSetChanged();
    }
 
    public int getSelectedItemCount() {
        return selectedItems.size();
    }
 
    public List<Integer> getSelectedItems() {
        List<Integer> items =
                new ArrayList<>(selectedItems.size());
        for (int i = 0; i < selectedItems.size(); i++) {
            items.add(selectedItems.keyAt(i));
        }
        return items;
    }
 
    public void removeData(int position) {
        messages.remove(position);
        resetCurrentIndex();
    }
 
    private void resetCurrentIndex() {
        currentSelectedIndex = -1;
    }
 
    public interface MessageAdapterListener {
        void onIconClicked(int position);
 
        void onIconImportantClicked(int position);
 
        void onMessageRowClicked(int position);
 
        void onRowLongClicked(int position);
    }
}

23. 最后打开MainActivity.java,并修如下修改代码。

> 添加SwipeRefreshLayout实现刷新获取数据 

> getInbox() Method获取并解析JSON,然后追加到array list中。  

> 创建Adapter 并设置给RecyclerView。

> 长按item时启用ActionMode

MainActivity.java
package info.androidhive.gmail.activity;
 
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
 
import java.util.ArrayList;
import java.util.List;
 
import info.androidhive.gmail.R;
import info.androidhive.gmail.adapter.MessagesAdapter;
import info.androidhive.gmail.helper.DividerItemDecoration;
import info.androidhive.gmail.model.Message;
import info.androidhive.gmail.network.ApiClient;
import info.androidhive.gmail.network.ApiInterface;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
 
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, MessagesAdapter.MessageAdapterListener {
    private List<Message> messages = new ArrayList<>();
    private RecyclerView recyclerView;
    private MessagesAdapter mAdapter;
    private SwipeRefreshLayout swipeRefreshLayout;
    private ActionModeCallback actionModeCallback;
    private ActionMode actionMode;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
 
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
 
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
        swipeRefreshLayout.setOnRefreshListener(this);
 
        mAdapter = new MessagesAdapter(this, messages, this);
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
        recyclerView.setAdapter(mAdapter);
 
        actionModeCallback = new ActionModeCallback();
 
        // show loader and fetch messages
        swipeRefreshLayout.post(
                new Runnable() {
                    @Override
                    public void run() {
                        getInbox();
                    }
                }
        );
    }
 
    /**
     * Fetches mail messages by making HTTP request
     * url: http://api.androidhive.info/json/inbox.json
     */
    private void getInbox() {
        swipeRefreshLayout.setRefreshing(true);
 
        ApiInterface apiService =
                ApiClient.getClient().create(ApiInterface.class);
 
        Call<List<Message>> call = apiService.getInbox();
        call.enqueue(new Callback<List<Message>>() {
            @Override
            public void onResponse(Call<List<Message>> call, Response<List<Message>> response) {
                // clear the inbox
                messages.clear();
 
                // add all the messages
                // messages.addAll(response.body());
 
                // TODO - avoid looping
                // the loop was performed to add colors to each message
                for (Message message : response.body()) {
                    // generate a random color
                    message.setColor(getRandomMaterialColor("400"));
                    messages.add(message);
                }
 
                mAdapter.notifyDataSetChanged();
                swipeRefreshLayout.setRefreshing(false);
            }
 
            @Override
            public void onFailure(Call<List<Message>> call, Throwable t) {
                Toast.makeText(getApplicationContext(), "Unable to fetch json: " + t.getMessage(), Toast.LENGTH_LONG).show();
                swipeRefreshLayout.setRefreshing(false);
            }
        });
    }
 
    /**
     * chooses a random color from array.xml
     */
    private int getRandomMaterialColor(String typeColor) {
        int returnColor = Color.GRAY;
        int arrayId = getResources().getIdentifier("mdcolor_" + typeColor, "array", getPackageName());
 
        if (arrayId != 0) {
            TypedArray colors = getResources().obtainTypedArray(arrayId);
            int index = (int) (Math.random() * colors.length());
            returnColor = colors.getColor(index, Color.GRAY);
            colors.recycle();
        }
        return returnColor;
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
 
        //noinspection SimplifiableIfStatement
        if (id == R.id.action_search) {
            Toast.makeText(getApplicationContext(), "Search...", Toast.LENGTH_SHORT).show();
            return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
 
    @Override
    public void onRefresh() {
        // swipe refresh is performed, fetch the messages again
        getInbox();
    }
 
    @Override
    public void onIconClicked(int position) {
        if (actionMode == null) {
            actionMode = startSupportActionMode(actionModeCallback);
        }
 
        toggleSelection(position);
    }
 
    @Override
    public void onIconImportantClicked(int position) {
        // Star icon is clicked,
        // mark the message as important
        Message message = messages.get(position);
        message.setImportant(!message.isImportant());
        messages.set(position, message);
        mAdapter.notifyDataSetChanged();
    }
 
    @Override
    public void onMessageRowClicked(int position) {
        // verify whether action mode is enabled or not
        // if enabled, change the row state to activated
        if (mAdapter.getSelectedItemCount() > 0) {
            enableActionMode(position);
        } else {
            // read the message which removes bold from the row
            Message message = messages.get(position);
            message.setRead(true);
            messages.set(position, message);
            mAdapter.notifyDataSetChanged();
 
            Toast.makeText(getApplicationContext(), "Read: " + message.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }
 
    @Override
    public void onRowLongClicked(int position) {
        // long press is performed, enable action mode
        enableActionMode(position);
    }
 
    private void enableActionMode(int position) {
        if (actionMode == null) {
            actionMode = startSupportActionMode(actionModeCallback);
        }
        toggleSelection(position);
    }
 
    private void toggleSelection(int position) {
        mAdapter.toggleSelection(position);
        int count = mAdapter.getSelectedItemCount();
 
        if (count == 0) {
            actionMode.finish();
        } else {
            actionMode.setTitle(String.valueOf(count));
            actionMode.invalidate();
        }
    }
 
 
    private class ActionModeCallback implements ActionMode.Callback {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            mode.getMenuInflater().inflate(R.menu.menu_action_mode, menu);
 
            // disable swipe refresh if action mode is enabled
            swipeRefreshLayout.setEnabled(false);
            return true;
        }
 
        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }
 
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                case R.id.action_delete:
                    // delete all the selected messages
                    deleteMessages();
                    mode.finish();
                    return true;
 
                default:
                    return false;
            }
        }
 
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mAdapter.clearSelections();
            swipeRefreshLayout.setEnabled(true);
            actionMode = null;
            recyclerView.post(new Runnable() {
                @Override
                public void run() {
                    mAdapter.resetAnimationIndex();
                    // mAdapter.notifyDataSetChanged();
                }
            });
        }
    }
 
    // deleting the messages from recycler view
    private void deleteMessages() {
        mAdapter.resetAnimationIndex();
        List<Integer> selectedItemPositions =
                mAdapter.getSelectedItems();
        for (int i = selectedItemPositions.size() - 1; i >= 0; i--) {
            mAdapter.removeData(selectedItemPositions.get(i));
        }
        mAdapter.notifyDataSetChanged();
    }
}

运行项目就可以看到实际的效果了,确保设备的网络状况良好。

android-gmail-like-inbox-recycler-view-tutorial.png

下载代码

下载 .APK

原文:Android Creating Gmail Like Inbox using RecyclerView

来自:UI实验室