Wednesday, June 10, 2015

MyNotes: Color Filter Toolbar Pt. 2

Code Changes

The biggest change that you will notice is the removal of the toolbar fragment.  I decided that with the changes, the Fragment was overkill and fit really well in the MainActivity. 

The Color Filter Toolbar still gets to stay a RecyclerView.  Since I now have two RecyclerViews in the MainActivity, I broke up their initializations into separate methods:


@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);

// Set a toolbar to replace the action bar.
Toolbar toolbar = (Toolbar) findViewById(R.id.activity_main_toolbar);
setSupportActionBar(toolbar);
initializeNoteCards();
initializeColorFilterToolbar();

getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
}


/**
* Initialize the color filter toolbar
*/
private void initializeColorFilterToolbar() {

int dividerHorizontal =
(int) getResources().getDimension(R.dimen.color_filter_item_divider_horizontal);
int dividerVertical =
(int) getResources().getDimension(R.dimen.color_filter_item_divider_vertical);

mFilterRecyclerView
.addItemDecoration(new DividerItemDecoration(dividerVertical, dividerHorizontal));

boolean vertical = getResources().getBoolean(R.bool.color_filter_toolbar_vertical);
int orientation = vertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL;

mFilterRecyclerView.setLayoutManager(new ColorFilterLayoutManager(this, orientation));
mFilterRecyclerView.setAdapter(new FilterColorAdapter(this));
}


/**
* Initialize the note cards UI.
*/
private void initializeNoteCards() {

mCardAdapter = new CardAdapter();

int columnCount = getResources().getInteger(R.integer.card_column_count);
float cardDividerDimen = getResources().getDimension(R.dimen.note_divider_size);

mCardsRecyclerView.addItemDecoration(new DividerItemDecoration((int) cardDividerDimen));
mCardsRecyclerView.setLayoutManager(new GridLayoutManager(this, columnCount));
mCardsRecyclerView.setAdapter(mCardAdapter);
}

Custom Layout Manager


I want the Color Filter Toolbar to fit across the longest side of the device without scrolling.  I also want all of the filter items to be the same size:


$RE77AWB


There isn’t a great way to do this without creating a custom Layout Manager for the recycler view.  Since I still want a single column I extended the LinearLayoutManager and modified the measure routines.



/**
* The height/width for each child.
*/
private int mChildDimension;


/**
* Constructor.
*
* @param context
* The context to use for accessing application resources.
*/
public ColorFilterLayoutManager(final Context context) {
super(context);
}


/**
* Constructor.
*
* @param context
* The context to use for accessing application resources.
* @param orientation
* The orientation of the Views. The only valid values are {@link
* LinearLayoutManager#VERTICAL} or {@link LinearLayoutManager#HORIZONTAL}.
*/
public ColorFilterLayoutManager(final Context context, int orientation) {
super(context, orientation, false);
}


@Override
public void onMeasure(final RecyclerView.Recycler recycler, final RecyclerView.State state,
final int widthSpec, final int heightSpec) {
super.onMeasure(recycler, state, widthSpec, heightSpec);

int itemCount = getItemCount();
if ((itemCount == 0) && (getChildCount() == 0)) {
return;
}

//Determine the expected child dimension for the correct axis.
switch (getOrientation()) {
case VERTICAL:
mChildDimension = (getHeight() - getPaddingBottom() + getPaddingTop()) / itemCount;
break;
case HORIZONTAL:
mChildDimension = (getWidth() - getPaddingLeft() - getPaddingRight()) / itemCount;
break;
}
}


@Override
public void measureChildWithMargins(final View child, final int widthUsed,
final int heightUsed) {
super.measureChildWithMargins(child, widthUsed, heightUsed);

//Determine the child height/width.
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int width = lp.width;
int height = lp.height;
switch (getOrientation()) {
case VERTICAL:
height = mChildDimension - getBottomDecorationHeight(child) -
getTopDecorationHeight(child);
break;
case HORIZONTAL:
width = mChildDimension - getLeftDecorationWidth(child) -
getRightDecorationWidth(child);
break;
}

//Determine the height and width specs and re-measure the child.
final int widthSpec = getChildMeasureSpec(getWidth(),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin + widthUsed,
width,
canScrollHorizontally());
final int heightSpec =
getChildMeasureSpec(getHeight(),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin + heightUsed,
height,
canScrollVertically());
child.measure(widthSpec, heightSpec);
}
}

The key methods here are the onMeasure and measureChildWithMargins methods.  The onMeasure method calculates a size in one dimension that each child will use.  That size is along the X-axis when using the horizontal orientation and along the Y-axis for vertical orientations. 


The measureChildWithMargins method then uses forces each child use to match that size.  It factors in each child’s padding, margins and decorations to ensure it doesn’t exceed the expected dimension for that child.


Adapter Post Issues


I am the first to admit that I am still learning the nuances of the RecyclerView.  I happened to learn a fun one while testing the latest round of changes.  This one involves an IllegalStateException that occuring if you call any of the Adapters notifyBlah methods while the RecyclerView is laying out or scrolling. 


This is really annoying because things scroll and lay out a lot.   This can be fixed by checking the RecyclerView state before calling any of the notify methods or you can post the notify methods on the UI thread handler and that seems to fix the issue. 


I opted for method two because I am lazy and I don’t like cluttered if branches.  To make the code reusable I extended the RecyclerView.Adapter and created a new version of each notify method.  The new method posts the actual notify call on a supplied handler.  This should eliminate the exception issue.  Emphasis on the should in the last sentence.  The new class is RecyclerViewAdapter in the common library.


RecyclerViewAdapter


public abstract class RecyclerViewAdapter<VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {


/**
* Asynchronously post {@link #notifyDataSetChanged()} if the handler exists.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public void postNotifyDataSetChanged(Handler handler) {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
}


/**
* Asynchronously post {@link #notifyItemChanged(int)} if the handler exists.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemChanged(Handler handler, final int position) {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemChanged(position);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemRangeChanged(int, int)}.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemRangeChanged(Handler handler, final int positionStart,
final int itemCount) {

if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemRangeChanged(positionStart, itemCount);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemInserted(int)}.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemInserted(Handler handler, final int position) {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemInserted(position);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemMoved(int, int)}.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemMoved(Handler handler, final int fromPosition,
final int toPosition) {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemMoved(fromPosition, toPosition);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemRangeInserted(int, int)}.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemRangeInserted(Handler handler, final int positionStart,
final int itemCount) {

if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemRangeInserted(positionStart, itemCount);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemRemoved(int)}
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemRemoved(Handler handler, final int position) {

if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemRemoved(position);
}
});
}
}


/**
* Asynchronously post {@link #notifyItemRangeRemoved(int, int)}.
*
* @param handler
* the handler to use for posting the requested notify method.
*/
public final void postNotifyItemRangeRemoved(Handler handler, final int positionStart,
final int itemCount) {
if (handler != null) {
handler.post(new Runnable() {
@Override
public void run() {
notifyItemRangeRemoved(positionStart, itemCount);
}
});
}
}
}

Commit


The commit for these changes can be found at https://github.com/fsk-software/mynotes/commit/e24614e2557ab6791e2f00617895c0fd4e726526.

No comments :

Post a Comment