RecyclerView
is the “Swiss army knife” of Android selection widgets.
You can use it for a wide range of scenarios, well beyond what
classic AdapterView
widgets — like ListView
or GridView
— could
handle.
In this chapter, we will “go outside the (AdapterView
) box” and
explore some advanced uses of RecyclerView
.
Understanding this chapter requires that you have read
the preceding chapter, on RecyclerView
.
ViewPager
has been used for horizontally-swiped page-at-a-time user
interfaces since its debut in 2011.
However, ViewPager
is not that flexible:
setSwipeDirection()
or similar method to switch to vertical swiping.PagerAdapter
itself can work with views as pages, even the minimum required API is
difficult to understand.
And using fragments as pages means that you may wind up in cases with
nested fragments, which adds to the complexity.PagerAdapter
has enough hooks to allow you to add and remove
pages on the fly, neither FragmentPagerAdapter
nor FragmentStatePagerAdapter
support this use case, requiring you to roll your own PagerAdapter
implementation.And so on.
However, as it turns out, RecyclerView
can be readily adapted to serve
as a ViewPager
replacement. Instead of a PagerAdapter
, you use an
ordinary RecyclerView.Adapter
, where your pages are simple views.
RecyclerView
itself is far more flexible than is ViewPager
, giving you
a stronger foundation for more advanced paging scenarios.
The original solution for using RecyclerView
as a ViewPager
replacement came in the form of a third-party library,
com.github.lsjwzh.RecyclerViewPager
. This library offers a
a RecyclerViewPager
subclass of RecyclerView
that offers the
page-at-a-time swiping metaphor.
The
RecyclerViewPager/PlainRVP
sample project illustrates its use. This is another rendition of the
10-EditText
-widgets pager that was used in the chapter on ViewPager
,
swapping in RecyclerViewPager
for the ViewPager
.
The com.github.lsjwzh.RecyclerViewPager
library is not on JCenter
or Maven Central. Instead, it is on jitpack.io
, an artifact
repository that builds artifacts directly from GitHub source
repositories. So, to use this library, we need to add jitpack.io
as a repository, in addition to adding the RecyclerViewPager
library
itself:
apply plugin: 'com.android.application'
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
compile 'com.android.support:recyclerview-v7:25.1.0'
compile 'com.github.lsjwzh.RecyclerViewPager:lib:v1.1.2'
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
}
}
In the equivalent ViewPager
sample app, the main.xml
layout resource
held the ViewPager
. In this sample, it holds the RecyclerViewPager
(or, more accurately, the com.lsjwzh.widget.recyclerviewpager.RecyclerViewPager
,
since the class name needs to be fully-qualified since it is coming
from a library):
<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.widget.recyclerviewpager.RecyclerViewPager android:id="@+id/pager"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:layout_margin="@dimen/pager_padding"
app:rvp_singlePageFling="true"
app:rvp_triggerOffset="0.1" />
The app:rvp_singlePageFling
indicates that we want to limit the user
to switch one page at a time, rather than a long fling gesture resulting
in moving through many pages at once. The app:rvp_triggerOffset
attribute is undocumented but appears to control how much of a swipe
gesture is necessary to trigger a page change.
With ViewPager
, you supply the pages via a PagerAdapter
, typically
a FragmentPagerAdapter
or a FragmentStatePagerAdapter
. With
RecyclerViewPager
, you supply the pages via a RecyclerView.Adapter
,
just as you would with any other RecyclerView
.
So, in onCreate()
of the MainActivity
, we get the RecyclerViewPager
,
hand it a horizontal LinearLayoutManager
, create a PageAdapter
, and
attach that PageAdapter
to the RecyclerViewPager
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
RecyclerViewPager pager=(RecyclerViewPager)findViewById(R.id.pager);
pager.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL, false));
adapter=new PageAdapter(pager, getLayoutInflater());
pager.setAdapter(adapter);
}
By using a horizontal LinearLayoutManager
, the RecyclerViewPager
will behave akin to a regular ViewPager
, with navigation occurring
via horizontal swipes. Want a vertical ViewPager
? Replace the
horizontal LinearLayoutManager
with a vertical one, and you are set.
Our PageAdapter
is a RecyclerView.Adapter
, for a RecyclerView.ViewHolder
named PageController
. The basic setup for PageAdapter
is not that
different than any other RecyclerView.Adapter
:
PageController
in onCreateViewHolder()
PageController
with model data in onBindViewHolder()
getItemCount()
class PageAdapter extends RecyclerView.Adapter<PageController> {
private static final String STATE_BUFFERS="buffers";
private static final int PAGE_COUNT=10;
private final RecyclerViewPager pager;
private final LayoutInflater inflater;
private ArrayList<String> buffers=new ArrayList<>();
PageAdapter(RecyclerViewPager pager, LayoutInflater inflater) {
this.pager=pager;
this.inflater=inflater;
for (int i=0;i<10;i++) {
buffers.add("");
}
}
@Override
public PageController onCreateViewHolder(ViewGroup parent, int viewType) {
return(new PageController(inflater.inflate(R.layout.editor, parent, false)));
}
@Override
public void onBindViewHolder(PageController holder, int position) {
holder.setText(buffers.get(position));
}
@Override
public int getItemCount() {
return(PAGE_COUNT);
}
In this case, our model data is an ArrayList
of String
objects,
representing the text that the user enters into each page’s EditText
.
PAGE_COUNT
caps the number of editors (and pages) at 10, and so we
initialize 10 buffers in the PageAdapter
constructor.
The layout used for the pages — inflated by onCreateViewHolder()
–
is just a full-page multi-line EditText
widget:
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/editor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textMultiLine"
android:gravity="left|top"
/>
PageController
is a fairly basic RecyclerView.ViewHolder
, wrapping
our EditText
and offering a getter and setter for manipulating the
text in the editor:
package com.commonsware.android.rvp;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.EditText;
class PageController extends RecyclerView.ViewHolder {
private final EditText editor;
PageController(View itemView) {
super(itemView);
editor=(EditText)itemView.findViewById(R.id.editor);
}
void setText(String text) {
editor.setText(text);
editor.setHint(editor.getContext().getString(R.string.hint,
getAdapterPosition()+1));
}
String getText() {
return(editor.getText().toString());
}
}
In case the buffer is empty (as it is at the outset), we also set the
hint of the EditText
to be the current page’s index, adding one to
adjust the range to start at 1 rather than 0. The hint text itself
is in a string resource, with a %d
placeholder for the page number:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">RVP Demo</string>
<string name="hint">Editor #%d</string>
</resources>
We use the N-parameter getString()
method to not only retrieve the
hint
string resource but run it through String.format()
to populate
the placeholders, in this case using getAdapterPosition()
to determine
our page number.
RecyclerView
wants to recycle its items. That is in contrast to how
the stock PagerAdapter
implementation work:
FragmentPagerAdapter
holds onto every fragment created due to the
user’s navigationFragmentStatePagerAdapter
holds onto some fragments, but lets
others get garbage-collected, to minimize memory consumptionIf our pages were read-only, we would not have to worry about recycling.
This is how many RecyclerView
implementations work — they just focus
on binding the right data into the right RecyclerView.ViewHolder
at the right time,
based on calls to onBindViewHolder
in the RecyclerView.Adapter
.
However, when the RecyclerView
items are interactive, we need to make
sure that we hold onto the changed data, rather than having it be
overwritten when we bind fresh data into the recycled item’s views.
In PageAdapter
, we handle this by overriding onViewDetachedFromWindow()
,
which is called when the views of a PageController
are no longer
part of our activity’s window.
Typically, this will occur as part
of scrolling. In our case,
we use this opportunity to grab the current contents of that
EditText
widget and update our buffers
data model to match:
@Override
public void onViewDetachedFromWindow(PageController holder) {
super.onViewDetachedFromWindow(holder);
buffers.set(holder.getAdapterPosition(), holder.getText());
}
Alternatively, you could aim to deal with this more in “real time”,
such as by using a TextWatcher
to update the model as the user types.
That adds a fair bit of overhead, though.
We need to make sure that we do not lose what the user types into the
pages when we undergo a configuration change. Since our model is a simple
ArrayList
of String
objects, we can use the saved instance state
Bundle
to hold onto the in-flight information.
A RecyclerView.Adapter
does not have its own onSaveInstanceState()
method, but we can add one, then call it from MainActivity
:
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
Bundle adapterState=new Bundle();
adapter.onSaveInstanceState(adapterState);
state.putBundle(STATE_ADAPTER, adapterState);
}
Here, MainActivity
provides a fresh Bundle
to the adapter. This way,
values that the adapter wishes to save in the instance state will not
collide with anything else the activity would want to save in the instance
state, due to accidental key collisions. In this case, this may well be
superfluous, but it is a worthwhile practice.
The challenge in our PageAdapter
is that buffers
only has text
from those PageController
objects that have been recycled. That will
not include the currently-visible page or possibly some adjacent pages.
So, we iterate over all pages and call findViewHolderForAdapterPosition()
on the RecyclerView
itself. This will return null
for any positions
for which no PageController
is presently allocated, or the PageController
for the position for those positions that are actively being used. For
those latter ones, we update the buffers
to reflect whatever is in
the EditText
widgets, saving that into the instance state
Bundle
:
void onSaveInstanceState(Bundle state) {
for (int i=0;i<PAGE_COUNT;i++) {
PageController holder=
(PageController)pager.findViewHolderForAdapterPosition(i);
if (holder!=null) {
buffers.set(i, holder.getText());
}
}
state.putStringArrayList(STATE_BUFFERS, buffers);
}
MainActivity
has a corresponding onRestoreInstanceState()
method:
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
adapter.onRestoreInstanceState(state.getBundle(STATE_ADAPTER));
}
That delegates the work to the onRestoreInstanceState()
method on the
PageAdapter
:
void onRestoreInstanceState(Bundle state) {
buffers=state.getStringArrayList(STATE_BUFFERS);
}
This sets up our buffers
for use in populating pages again.
RecyclerViewPager
was first released in 2014. Since then, RecyclerView
and its supporting classes have evolved. Now, you can get much of the
functionality of RecyclerViewPager
with an ordinary RecyclerView
,
with the assistance of SnapHelper
. As Lisa Wray profiled in
a droidcon NYC 2016 presentation,
SnapHelper
is a utility class that forces swipe gestures to “snap”
to certain locations or boundaries. And, there is a PagerSnapHelper
that, in conjunction with properly-configured RecyclerView
and
items, gives you ViewPager
-like behavior.
The
RecyclerViewPager/PlainSnap
sample project is a clone of the PlainRVP
sample, except that the
RecyclerViewPager
is replaced by a RecyclerView
and a PagerSnapHelper
.
There are three requirements of PagerSnapHelper
. Two are tied to the
layouts: both the RecyclerView
and its items need to have match_parent
for android:layout_width
and android:layout_height
. That was how
PlainRVP
was set up already, though PlainSnap
swaps in a RecyclerView
for the RecyclerViewPager
in main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView android:id="@+id/pager"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/pager_padding"
android:clipToPadding="false" />
The other requirement is that we create an instance of PagerSnapHelper
and call attachToRecyclerView()
on it, supplying our RecyclerView
.
This is handled in an updated MainActivity
:
package com.commonsware.android.rvp;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.PagerSnapHelper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SnapHelper;
public class MainActivity extends Activity {
private static final String STATE_ADAPTER="adapter";
private final SnapHelper snapperCarr=new PagerSnapHelper();
private PageAdapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
RecyclerView pager=(RecyclerView)findViewById(R.id.pager);
pager.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL, false));
snapperCarr.attachToRecyclerView(pager);
adapter=new PageAdapter(pager, getLayoutInflater());
pager.setAdapter(adapter);
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
Bundle adapterState=new Bundle();
adapter.onSaveInstanceState(adapterState);
state.putBundle(STATE_ADAPTER, adapterState);
}
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
adapter.onRestoreInstanceState(state.getBundle(STATE_ADAPTER));
}
}
We hold onto the PagerSnapHelper
in a field, to ensure that it will
not be garbage-collected unexpectedly. Probably the PagerSnapHelper
has sufficient connections to the RecyclerView
to ensure that it will
stay around as long as its associated RecyclerView
does, but that is
not apparent from the API or the documentation.
Beyond that, we configure the RecyclerView
much as we had configured
the RecyclerViewPager
, and our PageAdapter
and PageController
are largely unaffected by the UI switch. In the end, we wind up once again
with page-at-a-time horizontal swiping, though this time we can skip
the third-party library.
Many times, with a pager-style interface, we want an indicator to help the user understand where they are within the range of pages offered by the pager. One of the more popular indicator styles is tabs, as those also provide an alternative navigation option, with the user tapping on tabs to switch to particular pages.
For adding tabs to a RecyclerView
-powered pager, you need a tab
implementation that is not tied inextricably to ViewPager
, the way
PagerTabStrip
is. At the same time, you need one that is not
tied inextricably to some other particular UI setup, the way that
FragmentTabHost
is. Instead, you need tabs that “stick to their
knitting” and focus solely on handling the tab UI, giving you the
hooks necessary to update your UI based on tab changes, and to update
the tabs based on other UI changes.
TabLayout
, from the Design Support library,
is one such tab implementation. While it offers hooks into ViewPager
,
those are optional. You have two main options for using TabLayout
:
appcompat-v7
. This works back to API Level 7, as does
RecyclerView
itself.TabLayout
from the CWAC-CrossPort library,
which is the official TabLayout
code, with all references to appcompat-v7
replaced by references to Theme.Material
and related native items. However,
this limits this cross-ported TabLayout
to API Level 21 and higher
(Android 5.0).The
RecyclerViewPager/TabSnap
sample project is a clone of the PlainSnap
sample, with tabs added,
via the TabLayout
from CWAC-CrossPort. As a result, we need the
CWAC-CrossPort dependency and need to raise our minSdkVersion
to
21:
apply plugin: 'com.android.application'
repositories {
maven {
url "https://s3.amazonaws.com/repo.commonsware.com"
}
}
dependencies {
compile 'com.android.support:recyclerview-v7:25.1.0'
compile 'com.commonsware.cwac:crossport:0.0.2'
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
minSdkVersion 21
targetSdkVersion 25
applicationId 'com.commonsware.cwac.rvp.tabsnap'
}
}
The tabs themselves can then go above the RecyclerView
, in a vertical
LinearLayout
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.commonsware.cwac.crossport.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/pager_padding"
android:clipToPadding="false" />
</LinearLayout>
Next, we need to set up the tab contents in onCreate()
of MainActivity
.
To do that, we get our hands
on the TabLayout
using findViewById()
, then iterate through the
items in the PageAdapter
to set up tabs for each:
final TabLayout tabs=(TabLayout)findViewById(R.id.tabs);
for (int i=0;i<adapter.getItemCount();i++) {
tabs.addTab(tabs.newTab().setText(adapter.getTabText(this, i)));
}
We ask the PageAdapter
for the text to show in the tab, via a getTabText()
method:
String getTabText(Context ctxt, int position) {
return(PageController.getTitle(ctxt, position));
}
That, in turn, delegates to a static
version of the getTitle()
method
on PageController
, to fill in the string resource template with the
proper page number:
static String getTitle(Context ctxt, int position) {
return(ctxt.getString(R.string.hint, position+1));
}
We now need to add code in onCreate()
of MainActivity
to tie the
navigation together:
This is handled by event listeners:
tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
pager.smoothScrollToPosition(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
// unused
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// unused
}
});
pager.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int tab=layoutManager.findFirstCompletelyVisibleItemPosition();
if (tab>=0 && tab<tabs.getTabCount()) {
tabs.getTabAt(tab).select();
}
}
});
When a tab is selected, our anonymous TabLayout.OnTabSelectedListener
implementation will get control in onTabSelected()
. There, we tell
the RecyclerView
to scroll to show a particular position, tied to the
position of the selected tab.
Similarly, when the user scrolls the pager, we need to update the tabs
to show the new selection. To do that, we take advantage of the
findFirstCompletelyVisibleItemPosition()
method on LinearLayoutManager
.
As the (lengthy) method name suggests, this returns the position of the
first item that is completely visible within the pager. This might return
–1, if we are in the middle of a swipe, as no item may be completely
visible at that point. But, once we get a plausible value, we tell
the TabLayout
to select that tab.
RecyclerViewPager
has a more sophisticated algorithm for integrating
with the official Design Support library implementation of TabLayout
.
RecyclerViewPager
calculates the position of the tab highlight, based
upon the current swipe position, and updates that. This provides
visual feedback within the tabs while the swipe is going on. The approach
shown in the sample app has the effect of only updating the tabs once
the swipe is completed.
A major problem with ViewPager
— or, more accurately, the stock PagerAdapter
implementations — was when we wanted to add, move, or remove pages
on the fly, such as by the user tapping some sort of “add tab” action
bar item. To pull this off required a custom PagerAdapter
, as neither
FragmentPagerAdapter
nor FragmentStatePagerAdapter
were well-suited
for this scenario.
RecyclerView
can readily handle such changes in its roster of items,
and so we can add, move, and remove pages easily enough. However, it does
require a somewhat more elaborate example, not so much due to RecyclerView
,
but for the rest of our business logic. For example, all the prior samples
hard-coded getItemCount()
to return 10 (by way of a PAGE_COUNT
value),
and that will no longer be the case as we add and remove pages.
The
RecyclerViewPager/DynSnap
sample project is a clone of the TabSnap
sample, with four action
bar items, to add, “split”, swap, and remove pages.
In our previous samples, the tab title was tied to the tab position. That worked well, since our tabs all had fixed positions. Now, though, we want to add, move, and remove tabs, which means the positions of the existing tabs will change. We do not want to change the tab titles just because the positions change, as that will be very confusing for the user. So, our data model is more than just the text that the user types in — now it needs to include the title associated with that text.
To that end, DynSnap
sets up an actual model object, called EditBuffer
,
to track both of those values:
package com.commonsware.android.rvp;
import android.os.Parcel;
import android.os.Parcelable;
class EditBuffer implements Parcelable {
private String prose;
final private String title;
EditBuffer(String title) {
this(title, "");
}
EditBuffer(String title, String prose) {
this.prose=prose;
this.title=title;
}
protected EditBuffer(Parcel in) {
prose=in.readString();
title=in.readString();
}
@Override
public String toString() {
return(title);
}
String getProse() {
return(prose);
}
void setProse(String prose) {
this.prose=prose;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(prose);
dest.writeString(title);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<EditBuffer> CREATOR=
new Parcelable.Creator<EditBuffer>() {
@Override
public EditBuffer createFromParcel(Parcel in) {
return(new EditBuffer(in));
}
@Override
public EditBuffer[] newArray(int size) {
return(new EditBuffer[size]);
}
};
}
EditBuffer
is Parcelable
, so we will be able to hold onto this information
in the saved instance state Bundle
, for dealing with configuration
changes.
The buffers
field in PageAdapter
now is an ArrayList
of EditBuffer
objects, and our original constructor and other methods are updated
to match that:
PageAdapter(RecyclerView pager, LayoutInflater inflater) {
this.pager=pager;
this.inflater=inflater;
for (int i=0;i<10;i++) {
buffers.add(new EditBuffer(getNextTitle()));
}
}
@Override
public PageController onCreateViewHolder(ViewGroup parent, int viewType) {
return(new PageController(inflater.inflate(R.layout.editor, parent, false)));
}
@Override
public void onBindViewHolder(PageController holder, int position) {
holder.setText(buffers.get(position).getProse());
holder.setTitle(buffers.get(position).toString());
}
@Override
public int getItemCount() {
return(buffers.size());
}
String getTabText(int position) {
return(buffers.get(position).toString());
}
@Override
public void onViewDetachedFromWindow(PageController holder) {
super.onViewDetachedFromWindow(holder);
if (holder.getAdapterPosition()>=0) {
buffers.get(holder.getAdapterPosition()).setProse(holder.getText());
}
}
private String getNextTitle() {
return(pager.getContext().getString(R.string.hint, nextTabNumber++));
}
void onSaveInstanceState(Bundle state) {
for (int i=0;i<getItemCount();i++) {
updateProse(i);
}
state.putParcelableArrayList(STATE_BUFFERS, buffers);
}
void onRestoreInstanceState(Bundle state) {
buffers=state.getParcelableArrayList(STATE_BUFFERS);
}
We pull out the “update-the-EditBuffer
-based-on-the-holder-contents”
logic into a separate updateProse()
method, that we will reuse elsewhere:
private void updateProse(int position) {
PageController holder=
(PageController)pager.findViewHolderForAdapterPosition(position);
if (holder!=null) {
buffers.get(position).setProse(holder.getText());
}
}
Also note that when we set up the buffers in the constructor, we call
a getNextTitle()
method. This tracks the number of titles that we
have generated, via a nextTabNumber
field, and creates a new title
with a new number on each call. This further decouples our tab titles
from any concept of tab positions.
We have four vector assets in res/drawable/
for our four action bar items:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add"
android:icon="@drawable/ic_add_white_24dp"
android:showAsAction="ifRoom"
android:title="@string/menu_add" />
<item
android:id="@+id/split"
android:icon="@drawable/ic_split_white_24dp"
android:showAsAction="ifRoom"
android:title="@string/menu_split" />
<item
android:id="@+id/swap"
android:icon="@drawable/ic_swap_white_24dp"
android:showAsAction="ifRoom"
android:title="@string/menu_swap" />
<item
android:id="@+id/remove"
android:icon="@drawable/ic_remove_white_24dp"
android:showAsAction="ifRoom"
android:title="@string/menu_remove" />
</menu>
Those items get inflated and applied in MainActivity
:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.actions, menu);
remove=menu.findItem(R.id.remove);
return(super.onCreateOptionsMenu(menu));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.add:
add();
return(true);
case R.id.split:
split();
return(true);
case R.id.swap:
swap();
return(true);
case R.id.remove:
remove();
return(true);
}
return(super.onOptionsItemSelected(item));
}
Each of the items gets tied to its own dedicated method (e.g., R.id.add
triggers a call to add()
).
Note that we find and hold onto the remove
MenuItem
, caching its
value in a field. We are going to need to disable this MenuItem
when we can no longer allow the user to remove pages. In the case of
this app, we prevent the user from removing all the pages, so once we are
down to our last page, we need to disable the MenuItem
, re-enabling
it when the user adds new pages.
The “add” and “split” operations both do the same thing: add a page. One difference is where they add it. “Add” adds a page before the current one. “Split” adds a page after the current one.
The add()
and split()
methods therefore do very similar things:
remove
MenuItem
, in case it had been disabled and
should now be enabled
private void add() {
int current=getCurrentPosition();
adapter.insert(current);
tabs.addTab(tabs.newTab().setText(adapter.getTabText(current)), current);
updateRemoveMenuItem();
}
private void split() {
int newPosition=getCurrentPosition()+1;
adapter.clone(newPosition-1);
tabs.addTab(tabs.newTab().setText(adapter.getTabText(newPosition)), newPosition);
updateRemoveMenuItem();
}
Here, updateRemoveMenuItem()
simply sets the enabled state of the
remove
item based on our page count:
private void updateRemoveMenuItem() {
remove.setEnabled(adapter.getItemCount()>1);
}
Another difference between “add” and “split” is what the new page has for content. “Add” adds an empty page. “Split” clones the prose from the selected page and puts that prose in the new page as well.
That is why add()
on MainActivity
calls an insert()
method
on PageAdapter
and split()
calls clone()
, because the PageAdapter
has different behavior for each:
void insert(int position) {
buffers.add(position, new EditBuffer(getNextTitle()));
notifyItemInserted(position);
}
void clone(int position) {
updateProse(position);
EditBuffer newBuffer=new EditBuffer(getNextTitle(),
buffers.get(position).getProse());
buffers.add(position+1, newBuffer);
notifyItemInserted(position+1);
}
In both cases, a new EditBuffer
gets added to the buffers
, and we
call notifyItemInserted()
to let the RecyclerView
know of the data
model change. In the case of clone()
, we also update the EditBuffer
for the page to be cloned, then use that data to populate the new
EditBuffer
.
The swap()
method on MainActivity
flips the positions of two pages. Normally, this will be
the active tab and the next tab, but if the active tab is the last
tab, the pair will be the active tab and the previous tab.
Hence, swap()
identifies the right pair of positions, tells the
PageAdapter
to swap the contents, then swaps the titles of the two
affected tabs:
private void swap() {
int first=getCurrentPosition();
int second;
if (first>=adapter.getItemCount()-1) {
second=first;
first--;
}
else {
second=first+1;
}
adapter.swap(first, second);
TabLayout.Tab firstTab=tabs.getTabAt(first);
TabLayout.Tab secondTab=tabs.getTabAt(second);
CharSequence firstText=firstTab.getText();
firstTab.setText(secondTab.getText());
secondTab.setText(firstText);
}
The swap()
method on PageAdapter
needs to do three things:
buffers
to reflect the swapped pagesPageController
objects for those pages, if they
existRecyclerView
that the contents of these two positions
changed, to ensure that they get redrawn
void swap(int first, int second) {
EditBuffer firstBuffer=buffers.get(first);
EditBuffer secondBuffer=buffers.get(second);
buffers.set(first, secondBuffer);
buffers.set(second, firstBuffer);
PageController holder=
(PageController)pager.findViewHolderForAdapterPosition(first);
if (holder!=null) {
holder.setText(secondBuffer.getProse());
}
holder=(PageController)pager.findViewHolderForAdapterPosition(second);
if (holder!=null) {
holder.setText(firstBuffer.getProse());
}
notifyItemChanged(first);
notifyItemChanged(second);
}
Removing a page involves:
EditBuffer
from the data modelRecyclerView
remove
MenuItem
, if we are down to our last pageThe latter two tasks are handled by remove()
on MainActivity
:
private void remove() {
final int current=getCurrentPosition();
tabs.removeTabAt(current);
adapter.remove(current);
if (current<adapter.getItemCount()) {
pager.scrollToPosition(current);
}
updateRemoveMenuItem();
}
The former two tasks are handled by remove()
on PageAdapter
:
void remove(int position) {
buffers.remove(position);
notifyItemRemoved(position);
}