(译)使用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。
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.创建一个新的工程
我们从新建一个项目开始,然后做基本的设置。下面是项目的代码结构:
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();
}
}
运行项目就可以看到实际的效果了,确保设备的网络状况良好。