Android Creating Gmail Like Inbox using RecyclerView
source link: https://www.androidhive.info/2017/02/android-creating-gmail-like-inbox-using-recyclerview/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Introduction of RecyclerView is the best thing ever happen to android world. You can create stunningly beautiful lists and grids using the RecyclerView. Lot of us are very familiar in rendering the basic lists when the UI is very simple. But it comes to more complex lists that contains multiple UI elements along with animations, not everybody can achieve the final output they are looking for.
This article aims to improvise your knowledge on RecyclerView by taking an example of Gmail style inbox that contains the complex list design with interactive animations.
1. Overview
The desired output like Gmail app can’t be achieved just with the RecyclerView alone. It needs combination of other few android concepts. Overall we gonna use the below mentioned components to get the finest appearance and functionality.
> RecyclerView
The basic component required for this app is RecyclerView as our primary task is to display the data in list fashion. The appearance of the list is customized just like Gmail app displaying a thumbnail icon, three line message, timestamp and a star icon to mark the message as important.
> SwipeRefreshLayout
In order to refresh the inbox, SwipeRefreshLayout is wrapped around the RecyclerView. This article doesn’t explains the persistence of the data. So the inbox will be reset to initial state up on refresh.
> ActionMode
ActionMode is used to display the contextual toolbar when a row is long pressed in the list. This enables us to provide set of alternative toolbar icons when the recycler view is in multiple choice mode. Here we provide delete option to delete the selected messages.
> Object Animators
Object Animators allows us to animate a target element. In this we use the object animators to perform the Flip Animation of list thumbnail icon when a row is long pressed.
> Retrofit
In a production app, all the inbox messages are dynamic i.e they are fetched from a REST API. To demonstrate that, I have used a JSON url to list the messages. We use Retrofit library to fetch and deserialize the JSON.
2. Sample JSON for Inbox Messages
I have created an endpoint which serves the inbox messages in a JSON format. The JSON contains the information like profile picture, from, subject, message, timestamp and other details necessary to render the list. In realtime this json should be generated from a database using any server side language.
https://api.androidhive.info/json/inbox.json
[
{
"id"
: 1,
"isImportant"
: false,
"from"
:
"Google Alerts"
,
"subject"
:
"Google Alert - android"
,
"message"
:
"Android N update is released to Nexus Family!"
,
"timestamp"
:
"10:30 AM"
,
"isRead"
: false
},
.
.
.
]
3. Creating New Project
We’ll start by creating new project in Android Studio and do the basic setup required. Below is the final project structure I have planned for this article. This post seems to be lengthy but trust me this will enhance your knowledge and you will see surprising results when you reach to the bottom.
1. Create a new project in Android Studio from File ⇒ New Project and fill the project details. While creating the project, I have selected the Basic Activity as default activity to get the Toolbar, FAB and other elements.
2. Open build.gradle located under app module and add RecyclerView, Retrofit and Glide dependencies and Sync the project.
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. Download this res folder and paste the content in your project’s res folder. This folder contains all the necessary icons required for the RecyclerView and Toolbar.
4. Add the below colors, strings and dimens to respective files.
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
<
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
<
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. Open styles.xml and add the below styles. Here windowActionModeOverlay is added to overlap the ActionMode onto Toolbar.
<
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. As we are going to make network calls we need INTERNET permission in the manifest file. Open AndroidManifest.xml and add the permission.
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
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. Create five packages named activity, adapter, helper, model and network. We use these packages to keep the project organized. Once the packages are created, move your MainActivity to activity package.
4. Adding Retrofit – Fetching JSON
Now our project have the basic resources ready. Let’s add the network layer by using the Retrofit library. If you are new to Retrofit, I strongly suggest you go through my previous article about Retrofit.
8. Under model package, create a class named Message.java. This POJO class is used to deserialize the json while parsing.
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. Under network package, create a new class named ApiClient.java. This class creates the static retrofit instance.
package
info.androidhive.gmail.network;
import
retrofit2.Retrofit;
import
retrofit2.converter.gson.GsonConverterFactory;
public
class
ApiClient {
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. Under network package, create a new class named ApiInterface.java. This class contains the rest api endpoints and the type of response it is expecting. In our case we have only one endpoint i.e inbox.json
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();
}
This completes the retrofit integration. Now let’s add few helper classes those helps in rendering the list.
11. Under helper package, create a class named CircleTransform.java. This class is used to display the thumbnail image in circular shape using the Glide library.
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. Under helper package, create another class named DividerItemDecoration.java. This helps in adding divider lines in recycler view.
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. Generating Random Material Color
Another exciting thing you would come across here is assigning a random background color to each row icon. To achieve this we need to predefine set of material colors in an array and choose a random color while RecyclerView is prepared. Thanks to daniellevass for providing such an useful color codes.
13. Create an xml named array.xml under res ⇒ values. This xml contains few material colors thouse would be loaded randomly in list.
<?
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
>
To load these colors randomly, the following function can be used. You will see how to use this function shortly.
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. Flip Animation using Object Animators
If you observe the gmail app, when you long press and selects a row, the thumbnail icon will be animated in a flip motion showing other side of the icon. We can do the same using the ObjectAnimator concepts. Carefully create the below mentioned files in your project.
14. Under res ⇒ values, create an xml named integer.xml. Here we define the animation durations.
<?
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. Create a folder named animator under res folder. In this directory we place all the xml resources related to animations.
16. Under animator, create four xml files named 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
<!-- 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
<!-- 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
<!-- 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
<!-- 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. Under helper package, create a class named FlipAnimator.java. This class has a static method flipView() which performs the flip animation.
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. Finally Rendering the Inbox in RecyclerView
Oooh, finally we have reached to core part of the article i.e rendering the actual list data. Have some more patience and follow along with me.
Now let’s create the few remaining files required for the RecyclerView. All we need is xml layouts for main activity, list row, background drawables and an adapter class.
18. Under res ⇒ drawable, create two drawable resources named bg_circle.xml and bg_list_row.xml.
bg_circle.xml (This provides solid circular background color to thumbnail icon)
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
shape
android:shape
=
"oval"
>
<
solid
android:color
=
"@color/bg_circle_default"
/>
<
size
android:width
=
"120dp"
android:height
=
"120dp"
/>
</
shape
>
bg_list_row.xml (Provides background color to list item with normal and active states)
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
item
android:drawable
=
"@color/row_activated"
android:state_activated
=
"true"
/>
<
item
android:drawable
=
"@android:color/transparent"
/>
</
selector
>
19. Open the layout file of your main activity (content_main.xml) and add the RecyclerView element.
activity_main.xml
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
android.support.design.widget.CoordinatorLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
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
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
android.support.constraint.ConstraintLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
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. Under res ⇒ layout, create an xml layout named message_list_row.xml with below code. This layout will renders each row in recycler view. The actual customization of recycler view row happens here.
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
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
>
We also need two menu files to render the toolbar icons. One is to display the icons when Toolbar is in normal mode. The other is to display the icons when ActionMode is enabled.
21. Under res ⇒ menu folder, create two menu files named menu_main.xml and menu_action_mode.xml.
menu_main.xml
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
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
>
One more important class you will have to take care is the adapter class. The functionality of the RecyclerView completely depends on how efficiently you are managing the adapter class.
22. Under adapter package, create a class named MessagesAdapter.java and paste the below code. This class is very important, take your own time to explore and understand the code. All the magic happens in onBindViewHolder() method.
> applyReadStatus() method make the text bold depending on read status. If the message is unread, it will be kept in bold.
> applyImportant() – Displays the star icon in yellow color if the message is marked as important.
> applyIconAnimation() – Method performs the flip animation on thumbnail icon.
> applyProfilePicture() – Displays the thumbnail icon profile picture / background in circular fashion.
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. Finally open your main activity (MainActivity.java) and modify the code as below.
> SwipeRefreshLayout is added to fetch the json messages on refresh.
> getInbox() Method fetches and parses the JSON appending it to array list.
> Adapter instance is created and attached to RecyclerView.
> ActionMode is enabled when the list item is long pressed.
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
*/
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();
}
}
Run the project and see the output in action. Make sure that your device is having good internet connection. If you have any queries, feel free to ask them in the comment section below.
Happy Coding
Whats Next?
This article covers everything but one thing is missing i.e adding Swipe to delete and undo functionalities. But don’t worry, the next article explains adding Swipe Delete and Undo to RecyclerView.
Hi there! I am Founder at androidhive and programming enthusiast. My skills includes Android, iOS, PHP, Ruby on Rails and lot more. If you have any idea that you would want me to develop? Let’s talk: [email protected]
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK