RecyclerView的介绍是android世界发生的最好的事情。您可以使用RecyclerView创建令人惊叹的美丽列表和网格。当UI非常简单时,很多人都非常熟悉渲染基本列表。但它涉及更复杂的列表,其中包含多个UI元素以及动画,并非每个人都可以实现他们正在寻找的最终输出。
本文旨在通过一个包含带有交互式动画的复杂列表设计的Gmail样式收件箱示例,即兴发挥您对RecyclerView的了解。
下载源代码:https://pan.baidu.com/s/1uGlBdRC4kGwQwPAYOOJJwQ
1.概述
仅使用RecyclerView无法实现Gmail应用程序等所需的输出。它需要其他几个android概念的组合。总的来说,我们将使用下面提到的组件来获得最好的外观和功能。
> RecyclerView
此应用程序所需的基本组件是RecyclerView,因为我们的主要任务是以列表方式显示数据。列表的外观是自定义的,就像Gmail应用程序显示缩略图图标,三行消息,时间戳和星形图标一样,将消息标记为重要。
> SwipeRefreshLayout
为了刷新收件箱,SwipeRefreshLayout包裹在RecyclerView中。本文不解释数据的持久性。因此,在刷新时,收件箱将重置为初始状态。
> ActionMode
ActionMode用于在列表中长按行时显示上下文工具栏。这使我们能够在回收器视图处于多选模式时提供一组备用工具栏图标。这里我们提供删除选项来删除所选邮件。
> Object Animators
Object Animators允许我们为目标元素设置动画。在这里,我们使用对象动画师在长按行时执行列表缩略图图标的翻转动画。
> Retrofit
在生产应用程序中,所有收件箱消息都是动态的,即它们是从RESTAPI 获取的。为了证明这一点,我使用了一个JSON url来列出消息。我们使用Retrofit库来获取和反序列化JSON。
2.收件箱邮件的示例JSON
我创建了一个端点,以JSON格式提供收件箱消息。JSON包含诸如个人资料图片,来自主题,消息,时间戳以及呈现列表所需的其他详细信息之类的信息。实时地,应该使用任何服务器端语言从数据库生成此json。
https://api.androidhive.info/json/inbox.json
[ { "id": 1,
"isImportant": false,
"picture": "https://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 Studio中创建新项目并进行所需的基本设置。以下是我为本文计划的最终项目结构。这篇文章似乎很冗长,但相信我这会增强你的知识,当你达到最低点时,你会看到令人惊讶的结果。
1。从File⇒NewProject在Android Studio中创建一个新项目,并填写项目详细信息。在创建项目时,我选择了基本活动作为默认活动来获取工具栏,FAB和其他元素。
2。打开位于app模块下的build.gradle,添加RecyclerView,Retrofit和Glide依赖项并同步项目。
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和工具栏所需的所有必要图标。
4。将以下颜色,字符串和尺寸添加到相应的文件中。
colors.xml
colors.xml
#db4437
#b93221
#FFFFFF
#000000
#111111
#4285f4
#7a7a7a
#7a7a7a
#fed776
#e0e0e0
#757575
#666666
dimens.xml
dimens.xml
16dp
16dp
72dp
40dp
16sp
14sp
25dp
22dp
12dp
strings.xml中
strings.xml中
Gmail
Settings
Search
Delete
5。打开styles.xml并添加以下样式。这里添加windowActionModeOverlay以将ActionMode重叠到工具栏上。
styles.xml
@color/colorPrimary
@color/colorPrimaryDark
@color/colorAccent
false
true
true
@color/bg_action_mode
6。由于我们要进行网络调用,因此我们需要清单文件中的INTERNET权限。打开AndroidManifest.xml并添加权限。
AndroidManifest.xml中
http://schemas.android.com/apk/res/android"
package="info.androidhive.gmail">
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:name=".activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
7。创建五个名为activity,adapter,helper,model和network的包。我们使用这些包来保持项目的有序性。创建包后,将MainActivity移动到活动包。
4.添加改造 - 获取JSON
现在我们的项目已准备好基本资源。让我们使用Retrofit库添加网络层。如果您不熟悉Retrofit,我强烈建议您阅读我之前关于Retrofit的文章。
8。在模型包下,创建一个名为Message.java的类。此POJO类用于在解析时反序列化json。
Message.java
packageinfo.androidhive.gmail.model;
publicclassMessage {
privateintid;
privateString from;
privateString subject;
privateString message;
privateString timestamp;
privateString picture;
privatebooleanisImportant;
privatebooleanisRead;
privateintcolor = -1;
publicMessage() {
}
publicintgetId() {
returnid;
}
publicvoidsetId(intid) {
this.id = id;
}
publicString getFrom() {
returnfrom;
}
publicvoidsetFrom(String from) {
this.from = from; }
publicString getSubject() {
returnsubject;
}
publicvoidsetSubject(String subject) {
this.subject = subject;
}
publicString getMessage() {
returnmessage;
}
publicvoidsetMessage(String message) {
this.message = message;
}
publicString getTimestamp() {
returntimestamp;
}
publicvoidsetTimestamp(String timestamp) {
this.timestamp = timestamp;
}
publicbooleanisImportant() {
returnisImportant;
}
publicvoidsetImportant(booleanimportant) {
isImportant = important;
}
publicString getPicture() {
returnpicture;
}
publicvoidsetPicture(String picture) {
this.picture = picture;
}
publicbooleanisRead() {
returnisRead;
}
publicvoidsetRead(booleanread) {
isRead = read;
}
publicintgetColor() {
returncolor;
}
publicvoidsetColor(intcolor) {
this.color = color;
}
}
9。在网络包下,创建一个名为ApiClient.java的新类。此类创建静态改造实例。
ApiClient.java
packageinfo.androidhive.gmail.network;
importretrofit2.Retrofit;
importretrofit2.converter.gson.GsonConverterFactory;
publicclassApiClient {
publicstaticfinalString BASE_URL = "https://api.androidhive.info/json/";
privatestaticRetrofit retrofit = null;
publicstaticRetrofit getClient() {
if(retrofit == null) {
retrofit = newRetrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
returnretrofit;
}
}
10。在网络包下,创建一个名为ApiInterface.java的新类。此类包含其余的api端点以及它所期望的响应类型。在我们的例子中,我们只有一个端点,即inbox.json
ApiInterface.java
packageinfo.androidhive.gmail.network;
importjava.util.List;
importinfo.androidhive.gmail.model.Message;
importretrofit2.Call;
importretrofit2.http.GET;
publicinterfaceApiInterface {
@GET("inbox.json")
Call> getInbox();
}
这样就完成了改造整合。现在让我们添加一些助手类来帮助渲染列表。
11。在帮助程序包下,创建一个名为CircleTransform.java的类。此类用于使用Glide库以圆形形状显示缩略图图像。
CircleTransform.java
packageinfo.androidhive.gmail.helper;
importandroid.content.Context;
importandroid.graphics.Bitmap;
importandroid.graphics.BitmapShader;
importandroid.graphics.Canvas;
importandroid.graphics.Paint;
importcom.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
importcom.bumptech.glide.load.resource.bitmap.BitmapTransformation;
publicclassCircleTransform extendsBitmapTransformation {
publicCircleTransform(Context context) {
super(context);
}
@OverrideprotectedBitmap transform(BitmapPool pool, Bitmap toTransform, intoutWidth, intoutHeight) {
returncircleCrop(pool, toTransform);
}
privatestaticBitmap circleCrop(BitmapPool pool, Bitmap source) {
if(source == null) returnnull;
intsize = Math.min(source.getWidth(), source.getHeight());
intx = (source.getWidth() - size) / 2;
inty = (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 = newCanvas(result);
Paint paint = newPaint();
paint.setShader(newBitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
floatr = size / 2f;
canvas.drawCircle(r, r, r, paint);
returnresult;
}
@OverridepublicString getId() {
returngetClass().getName();
}
}
12。在帮助程序包下,创建另一个名为DividerItemDecoration.java的类。这有助于在回收器视图中添加分隔线。
DividerItemDecoration
packageinfo.androidhive.gmail.helper;
importandroid.content.Context;
importandroid.content.res.TypedArray;
importandroid.graphics.Canvas;
importandroid.graphics.Rect;
importandroid.graphics.drawable.Drawable;
importandroid.support.v7.widget.LinearLayoutManager;
importandroid.support.v7.widget.RecyclerView;
importandroid.view.View;
/**
* Created by Ravi Tamada on 21/02/17.
* www.androidhive.info
*/
publicclassDividerItemDecoration extendsRecyclerView.ItemDecoration {
privatestaticfinalint[] ATTRS = newint[]{
android.R.attr.listDivider
};
publicstaticfinalintHORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
publicstaticfinalintVERTICAL_LIST = LinearLayoutManager.VERTICAL;
privateDrawable mDivider;
privateintmOrientation;
publicDividerItemDecoration(Context context, intorientation) {
finalTypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
publicvoidsetOrientation(intorientation) {
if(orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
thrownewIllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
publicvoidonDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if(mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else{
drawHorizontal(c, parent);
}
}
publicvoiddrawVertical(Canvas c, RecyclerView parent) {
finalintleft = parent.getPaddingLeft();
finalintright = parent.getWidth() - parent.getPaddingRight();
finalintchildCount = parent.getChildCount();
for(inti = 0; i < childCount; i++) {
finalView child = parent.getChildAt(i);
finalRecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
finalinttop = child.getBottom() + params.bottomMargin;
finalintbottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
publicvoiddrawHorizontal(Canvas c, RecyclerView parent) {
finalinttop = parent.getPaddingTop();
finalintbottom = parent.getHeight() - parent.getPaddingBottom();
finalintchildCount = parent.getChildCount();
for(inti = 0; i < childCount; i++) {
finalView child = parent.getChildAt(i);
finalRecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
finalintleft = child.getRight() + params.rightMargin;
finalintright = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
publicvoidgetItemOffsets(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.生成随机材料颜色
您将遇到的另一个令人兴奋的事情是为每个行图标指定一个随机背景颜色。为实现这一目标,我们需要在数组中预定义一组材质颜色,并在准备RecyclerView时选择随机颜色。感谢daniellevass提供这样一个有用的颜色代码。
13。在res⇒ 值下创建一个名为array.xml的xml 。这个xml包含很少的材料颜色,可以在列表中随机加载。
array.xml
#e84e40
#ec407a
#ab47bc
#7e57c2
#5c6bc0
#738ffe
#29b6f6
#26c6da
#26a69a
#2baf2b
#9ccc65
#d4e157
#ffee58
#ffa726
#ff7043
#8d6e63
#bdbdbd
#78909c
#e51c23
#e91e63
#9c27b0
#673ab7
#3f51b5
#5677fc
#03a9f4
#00bcd4
#009688
#259b24
#8bc34a
#cddc39
#ffeb3b
#ff9800
#ff5722
#795548
#9e9e9e
#607d8b
要随机加载这些颜色,可以使用以下功能。您将很快看到如何使用此功能。
privateintgetRandomMaterialColor(String typeColor) {
intreturnColor = Color.GRAY;
intarrayId = getResources().getIdentifier("mdcolor_"+ typeColor, "array", getPackageName());
if(arrayId != 0) {
TypedArray colors = getResources().obtainTypedArray(arrayId);
intindex = (int) (Math.random() * colors.length());
returnColor = colors.getColor(index, Color.GRAY);
colors.recycle();
}
returnreturnColor;
}
6.使用Object Animators翻转动画
如果你观察gmail应用程序,当你长按并选择一行时,缩略图图标将以翻转动画显示动画,显示图标的另一面。我们可以使用ObjectAnimator概念来做同样的事情。在项目中仔细创建下面提到的文件。
14。在res⇒值下,创建一个名为integer.xml的xml 。这里我们定义动画持续时间。
integer.xml
500
200
15。在res文件夹下创建名为animator的文件夹。在此目录中,我们放置与动画相关的所有xml资源。
16。在animator下,创建四个名为card_flip_left_in.xml,card_flip_left_out.xml,card_flip_right_in.xml和card_flip_right_out.xml的xml文件。
card_flip_left_in.xml
card_flip_left_in.xml
http://schemas.android.com/apk/res/android">
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0"/>
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full"/>
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1"/>
card_flip_left_out.xml
card_flip_left_out.xml
http://schemas.android.com/apk/res/android">
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full"/>
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1"/>
card_flip_right_in.xml
card_flip_right_in.xml
http://schemas.android.com/apk/res/android">
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0"/>
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full"/>
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1"/>
card_flip_right_out.xml
card_flip_right_out.xml
http://schemas.android.com/apk/res/android">
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full"/>
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1"/>
17。在帮助程序包下,创建一个名为FlipAnimator.java的类。这个类有一个静态方法flipView(),它执行翻转动画。
FlipAnimator.java
packageinfo.androidhive.gmail.helper;
importandroid.animation.AnimatorInflater;
importandroid.animation.AnimatorSet;
importandroid.content.Context;
importandroid.view.View;
importinfo.androidhive.gmail.R;
publicclassFlipAnimator {
privatestaticString TAG = FlipAnimator.class.getSimpleName();
privatestaticAnimatorSet leftIn, rightOut, leftOut, rightIn;
/**
* Performs flip animation on two views
*/
publicstaticvoidflipView(Context context, finalView back, finalView front, booleanshowFront) {
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);
finalAnimatorSet showFrontAnim = newAnimatorSet();
finalAnimatorSet showBackAnim = newAnimatorSet();
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所需的少数剩余文件。我们需要的只是主要活动,列表行,背景drawable和适配器类的xml布局。
18。在res⇒ 可绘制下,创建两个名为bg_circle.xml和bg_list_row.xml的可绘制资源。
bg_circle.xml(这为缩略图图标提供了实心圆形背景颜色)
bg_circle.xml
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
android:color="@color/bg_circle_default"/>
android:width="120dp"
android:height="120dp"/>
bg_list_row.xml(提供背景颜色以列出具有正常和活动状态的项目)
bg_list_row.xml
http://schemas.android.com/apk/res/android">
19。打开主活动的布局文件(content_main.xml)并添加RecyclerView元素。
activity_main.xml中
activity_main.xml中
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
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"/>
content_main.xml
content_main.xml
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:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
20。在res⇒布局下,使用以下代码创建名为message_list_row.xml的xml布局。此布局将在回收器视图中呈现每一行。回收器视图行的实际自定义发生在此处。
message_list_row.xml
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">
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">
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"/>
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"/>
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"/>
android:id="@+id/icon_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:id="@+id/icon_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="@dimen/icon_width_height"
android:layout_height="@dimen/icon_width_height"
android:src="@drawable/bg_circle"/>
android:layout_width="25dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/ic_done_white_24dp"/>
android:id="@+id/icon_front"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:id="@+id/icon_profile"
android:layout_width="@dimen/icon_width_height"
android:layout_height="@dimen/icon_width_height"/>
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"/>
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"/>
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"/>
我们还需要两个菜单文件来渲染工具栏图标。一种是在工具栏处于正常模式时显示图标。另一种是在启用ActionMode时显示图标。
21。在水库⇒菜单文件夹中创建一个名为两个菜单文件menu_main.xml和menu_action_mode.xml。
menu_main.xml
menu_main.xml
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">
android:id="@+id/action_search"
android:icon="@drawable/ic_search_white_24dp"
android:orderInCategory="100"
android:title="@string/action_search"
app:showAsAction="always"/>
menu_action_mode.xml
menu_action_mode.xml
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">
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:orderInCategory="100"
android:title="@string/action_delete"
app:showAsAction="always"/>
您需要注意的另一个重要的类是适配器类。RecyclerView的功能完全取决于您管理适配器类的效率。
22。在适配器包下,创建一个名为MessagesAdapter.java的类并粘贴以下代码。这个课非常重要,花时间去探索和理解代码。所有的魔法都发生在onBindViewHolder()方法中。
> applyReadStatus()方法根据读取状态使文本变为粗体。如果邮件未读,则将以粗体显示。
> applyImportant()- 如果消息标记为重要,则以黄色显示星形图标。
> applyIconAnimation()- Method在缩略图图标上执行翻转动画。
> applyProfilePicture()- 以循环方式显示缩略图图标配置文件图片/背景。
MessagesAdapter.java
packageinfo.androidhive.gmail.adapter;
importandroid.content.Context;
importandroid.graphics.Typeface;
importandroid.support.v4.content.ContextCompat;
importandroid.support.v7.widget.RecyclerView;
importandroid.text.TextUtils;
importandroid.util.SparseBooleanArray;
importandroid.view.HapticFeedbackConstants;
importandroid.view.LayoutInflater;
importandroid.view.View;
importandroid.view.ViewGroup;
importandroid.widget.ImageView;
importandroid.widget.LinearLayout;
importandroid.widget.RelativeLayout;
importandroid.widget.TextView;
importcom.bumptech.glide.Glide;
importcom.bumptech.glide.load.engine.DiskCacheStrategy;
importjava.util.ArrayList;
importjava.util.List;
importinfo.androidhive.gmail.R;
importinfo.androidhive.gmail.helper.CircleTransform;
importinfo.androidhive.gmail.helper.FlipAnimator;
importinfo.androidhive.gmail.model.Message;
publicclassMessagesAdapter extendsRecyclerView.Adapter {
privateContext mContext;
privateList messages;
privateMessageAdapterListener listener;
privateSparseBooleanArray selectedItems;
// array used to perform multiple animation at once
privateSparseBooleanArray animationItemsIndex;
privatebooleanreverseAllAnimations = false;
// index is used to animate only the selected row
// dirty fix, find a better solution
privatestaticintcurrentSelectedIndex = -1;
publicclassMyViewHolder extendsRecyclerView.ViewHolder implementsView.OnLongClickListener {
publicTextView from, subject, message, iconText, timestamp;
publicImageView iconImp, imgProfile;
publicLinearLayout messageContainer;
publicRelativeLayout iconContainer, iconBack, iconFront;
publicMyViewHolder(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
publicbooleanonLongClick(View view) {
listener.onRowLongClicked(getAdapterPosition());
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
returntrue;
}
}
publicMessagesAdapter(Context mContext, List messages, MessageAdapterListener listener) {
this.mContext = mContext;
this.messages = messages;
this.listener = listener;
selectedItems = newSparseBooleanArray();
animationItemsIndex = newSparseBooleanArray();
}
@Override
publicMyViewHolder onCreateViewHolder(ViewGroup parent, intviewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.message_list_row, parent, false);
returnnewMyViewHolder(itemView);
}
@Override
publicvoidonBindViewHolder(finalMyViewHolder holder, finalintposition) {
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);
}
privatevoidapplyClickEvents(MyViewHolder holder, finalintposition) {
holder.iconContainer.setOnClickListener(newView.OnClickListener() {
@Override
publicvoidonClick(View view) {
listener.onIconClicked(position);
}
});
holder.iconImp.setOnClickListener(newView.OnClickListener() {
@Override
publicvoidonClick(View view) {
listener.onIconImportantClicked(position);
}
});
holder.messageContainer.setOnClickListener(newView.OnClickListener() {
@Override
publicvoidonClick(View view) {
listener.onMessageRowClicked(position);
}
});
holder.messageContainer.setOnLongClickListener(newView.OnLongClickListener() {
@Override
publicbooleanonLongClick(View view) {
listener.onRowLongClicked(position);
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
returntrue;
}
});
}
privatevoidapplyProfilePicture(MyViewHolder holder, Message message) {
if(!TextUtils.isEmpty(message.getPicture())) {
Glide.with(mContext).load(message.getPicture())
.thumbnail(0.5f)
.crossFade()
.transform(newCircleTransform(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);
}
}
privatevoidapplyIconAnimation(MyViewHolder holder, intposition) {
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
privatevoidresetIconYAxis(View view) {
if(view.getRotationY() != 0) {
view.setRotationY(0);
}
}
publicvoidresetAnimationIndex() {
reverseAllAnimations = false;
animationItemsIndex.clear();
}
@Override
publiclonggetItemId(intposition) {
returnmessages.get(position).getId();
}
privatevoidapplyImportant(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));
}
}
privatevoidapplyReadStatus(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
publicintgetItemCount() {
returnmessages.size();
}
publicvoidtoggleSelection(intpos) {
currentSelectedIndex = pos;
if(selectedItems.get(pos, false)) {
selectedItems.delete(pos);
animationItemsIndex.delete(pos);
} else{
selectedItems.put(pos, true);
animationItemsIndex.put(pos, true);
}
notifyItemChanged(pos);
}
publicvoidclearSelections() {
reverseAllAnimations = true;
selectedItems.clear();
notifyDataSetChanged();
}
publicintgetSelectedItemCount() {
returnselectedItems.size();
}
publicList getSelectedItems() {
List items =
newArrayList<>(selectedItems.size());
for(inti = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
returnitems;
}
publicvoidremoveData(intposition) {
messages.remove(position);
resetCurrentIndex();
}
privatevoidresetCurrentIndex() {
currentSelectedIndex = -1;
}
publicinterfaceMessageAdapterListener {
voidonIconClicked(intposition);
voidonIconImportantClicked(intposition);
voidonMessageRowClicked(intposition);
voidonRowLongClicked(intposition);
}
}
23。最后打开您的主要活动(MainActivity.java)并修改代码,如下所示。
>添加SwipeRefreshLayout以在刷新时获取json消息。
> getInbox()方法获取并解析附加到数组列表的JSON。
>创建适配器实例并将其附加到RecyclerView。
>长按列表项时启用ActionMode。
MainActivity.java
packageinfo.androidhive.gmail.activity;
importandroid.content.res.TypedArray;
importandroid.graphics.Color;
importandroid.os.Bundle;
importandroid.support.design.widget.FloatingActionButton;
importandroid.support.design.widget.Snackbar;
importandroid.support.v4.widget.SwipeRefreshLayout;
importandroid.support.v7.app.AppCompatActivity;
importandroid.support.v7.view.ActionMode;
importandroid.support.v7.widget.DefaultItemAnimator;
importandroid.support.v7.widget.LinearLayoutManager;
importandroid.support.v7.widget.RecyclerView;
importandroid.support.v7.widget.Toolbar;
importandroid.view.Menu;
importandroid.view.MenuItem;
importandroid.view.View;
importandroid.widget.Toast;
importjava.util.ArrayList;
importjava.util.List;
importinfo.androidhive.gmail.R;
importinfo.androidhive.gmail.adapter.MessagesAdapter;
importinfo.androidhive.gmail.helper.DividerItemDecoration;
importinfo.androidhive.gmail.model.Message;
importinfo.androidhive.gmail.network.ApiClient;
importinfo.androidhive.gmail.network.ApiInterface;
importretrofit2.Call;
importretrofit2.Callback;
importretrofit2.Response;
publicclassMainActivity extendsAppCompatActivity implementsSwipeRefreshLayout.OnRefreshListener, MessagesAdapter.MessageAdapterListener {
privateList messages = newArrayList<>();
privateRecyclerView recyclerView;
privateMessagesAdapter mAdapter;
privateSwipeRefreshLayout swipeRefreshLayout;
privateActionModeCallback actionModeCallback;
privateActionMode actionMode;
@Override
protectedvoidonCreate(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(newView.OnClickListener() {
@Override
publicvoidonClick(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 = newMessagesAdapter(this, messages, this);
RecyclerView.LayoutManager mLayoutManager = newLinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setItemAnimator(newDefaultItemAnimator());
recyclerView.addItemDecoration(newDividerItemDecoration(this, LinearLayoutManager.VERTICAL));
recyclerView.setAdapter(mAdapter);
actionModeCallback = newActionModeCallback();
// show loader and fetch messages
swipeRefreshLayout.post(
newRunnable() {
@Override
publicvoidrun() {
getInbox();
}
}
);
}
/**
* Fetches mail messages by making HTTP request
* url:https://api.androidhive.info/json/inbox.json
*/
privatevoidgetInbox() {
swipeRefreshLayout.setRefreshing(true);
ApiInterface apiService =
ApiClient.getClient().create(ApiInterface.class);
Call> call = apiService.getInbox();
call.enqueue(newCallback>() {
@Override
publicvoidonResponse(Call> call, Response> 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
publicvoidonFailure(Call> 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
*/
privateintgetRandomMaterialColor(String typeColor) {
intreturnColor = Color.GRAY;
intarrayId = getResources().getIdentifier("mdcolor_"+ typeColor, "array", getPackageName());
if(arrayId != 0) {
TypedArray colors = getResources().obtainTypedArray(arrayId);
intindex = (int) (Math.random() * colors.length());
returnColor = colors.getColor(index, Color.GRAY);
colors.recycle();
}
returnreturnColor;
}
@Override
publicbooleanonCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
returntrue;
}
@Override
publicbooleanonOptionsItemSelected(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.
intid = item.getItemId();
//noinspection SimplifiableIfStatement
if(id == R.id.action_search) {
Toast.makeText(getApplicationContext(), "Search...", Toast.LENGTH_SHORT).show();
returntrue;
}
returnsuper.onOptionsItemSelected(item);
}
@Override
publicvoidonRefresh() {
// swipe refresh is performed, fetch the messages again
getInbox();
}
@Override
publicvoidonIconClicked(intposition) {
if(actionMode == null) {
actionMode = startSupportActionMode(actionModeCallback);
}
toggleSelection(position);
}
@Override
publicvoidonIconImportantClicked(intposition) {
// 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
publicvoidonMessageRowClicked(intposition) {
// 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
publicvoidonRowLongClicked(intposition) {
// long press is performed, enable action mode
enableActionMode(position);
}
privatevoidenableActionMode(intposition) {
if(actionMode == null) {
actionMode = startSupportActionMode(actionModeCallback);
}
toggleSelection(position);
}
privatevoidtoggleSelection(intposition) {
mAdapter.toggleSelection(position);
intcount = mAdapter.getSelectedItemCount();
if(count == 0) {
actionMode.finish();
} else{
actionMode.setTitle(String.valueOf(count));
actionMode.invalidate();
}
}
privateclassActionModeCallback implementsActionMode.Callback {
@Override
publicbooleanonCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_action_mode, menu);
// disable swipe refresh if action mode is enabled
swipeRefreshLayout.setEnabled(false);
returntrue;
}
@Override
publicbooleanonPrepareActionMode(ActionMode mode, Menu menu) {
returnfalse;
}
@Override
publicbooleanonActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId()) {
caseR.id.action_delete:
// delete all the selected messages
deleteMessages();
mode.finish();
returntrue;
default:
returnfalse;
}
}
@Override
publicvoidonDestroyActionMode(ActionMode mode) {
mAdapter.clearSelections();
swipeRefreshLayout.setEnabled(true);
actionMode = null;
recyclerView.post(newRunnable() {
@Override
publicvoidrun() {
mAdapter.resetAnimationIndex();
// mAdapter.notifyDataSetChanged();
}
});
}
}
// deleting the messages from recycler view
privatevoiddeleteMessages() {
mAdapter.resetAnimationIndex();
List selectedItemPositions =
mAdapter.getSelectedItems();
for(inti = selectedItemPositions.size() - 1; i >= 0; i--) {
mAdapter.removeData(selectedItemPositions.get(i));
}
mAdapter.notifyDataSetChanged();
}
}
运行项目并查看输出的实际效果。确保您的设备具有良好的互联网连接。如果您有任何疑问,请随时在下面的评论部分询问。
快乐的编码
下一步是什么?
本文涵盖了所有内容,但缺少一件事,即添加Swipe以删除和撤消功能。但不要担心,下一篇文章解释了向RecyclerView 添加Swipe Delete和Undo。
你好!我是androidhive和编程爱好者的创始人。我的技能包括Android,iOS,PHP,Ruby on Rails等等。如果您有任何想法,我希望我发展?我们来聊聊吧:ravi@androidhive.info
网友评论