Wednesday, June 17, 2015

MyNotes: We all Make Mistakes

 

In one of my last posts I moved all of the logic into the MainActivity.  I changed my mind about that.  I liked the having the fragments more so I reintroduced two fragments: NoteCardsFragment and the ColorFilterFragment.

I then moved the most of the logic out of the MainActivity and into those fragments.

NoteCardsFragment

The same says it all.  This is a fragment that displays all the notes discovered by the loader.  This fragment really just consists of a Loader (moved from the MainActivity), a RecyclerView, and the supporting code to create the RecyclerView. 


/**
* The identifier of the main loader. This loads the color filtered note list.
*/
static final int MAIN_LOADER_ID = 0;


/**
* UI element to display the note data.
*/
@InjectView(R.id.fragment_note_cards_recycler_view)
RecyclerView mCardsRecyclerView;


/**
* The adapter to manage the display of cards in {@link #mCardsRecyclerView}.
*/
CardAdapter mCardAdapter;


/**
* The listener to a note being clicked.
*/
final CardAdapter.OnItemClickListener mNoteClickListener =
new CardAdapter.OnItemClickListener() {

@Override
public void onItemClick(final View view, final Note note) {
editNote(view, note);
}
};


@Nullable
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_note_cards, container, false);
ButterKnife.inject(this, rootView);

Context context = inflater.getContext();
mCardAdapter = new CardAdapter();
mCardAdapter.setOnItemClickListener(mNoteClickListener);

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

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

return rootView;
}


@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
}


@Override
public Loader<List<Note>> onCreateLoader(final int id, final Bundle args) {
return new FilteredNoteLoader(getActivity());
}


@Override
public void onLoadFinished(final Loader<List<Note>> loader, final List<Note> data) {
mCardAdapter.setNotes(data);
mCardAdapter.postNotifyDataSetChanged(mCardsRecyclerView.getHandler());
}


@Override
public void onLoaderReset(final Loader<List<Note>> loader) {
//Space for rent
}


At this point, the biggest lesson to take away is that I don’t create the loader until onActivityCreated.  This is very important because it is the earliest point in the lifecycle that I can guarantee that both the Fragments main view (getView()) and the Activity are ready.




@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(MAIN_LOADER_ID, null, this);
}


One other minor thing to note is that I normally do a public static method that creates my Fragments.  These are normally named something like newInstance.



/**
* Create a new instance of the {@link NoteCardsFragment}
*
* @return a new instance of the {@link NoteCardsFragment}
*/
public static NoteCardsFragment newInstance() {
return new NoteCardsFragment();
}



Normally, Fragment creation goes something like this:


Fragment fragment = new MyFragment();
Bundle arguments = new Bundle();
arguments.putInt(KEY, VALUE);
fragment.setArguments(arguments);

It may not seem like it matters but the newInstance code (along with make all the argument keys private) converts a runtime bugs into a compile time bugs.  To me that is a huge deal, especially on larger projects.


ColorFilterFragment


This fragment is responsible for filtering notes by color.  It is just the RecyclerView and its supporting data.  This was directly ported from the MainActivity.


public class ColorFilterFragment extends Fragment {

/**
* Create a new instance of the {@link ColorFilterFragment}
*
* @return a new instance of the {@link ColorFilterFragment}
*/
public static ColorFilterFragment newInstance() {
return new ColorFilterFragment();
}


/**
* UI element that allows the user to filter notes by color.
*/
@InjectView(R.id.fragment_color_filter_recycler)
RecyclerView mFilterRecyclerView;


/**
* The Cache for the selected colors.
*/
NoteFilterCache mNoteFilterCache;


/**
* The listener to react to changes to the color filter selection.
*/
final FilterColorFilterAdapter.OnColorChangeListener mOnColorSelectChangeListener =
new FilterColorFilterAdapter.OnColorChangeListener() {
@Override
public void onColorSelected(final NoteColor color, final boolean enabled) {
mNoteFilterCache.enableColor(color, enabled);
}
};


@Nullable
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {

View rootView = inflater.inflate(R.layout.fragment_color_selector, container, false);
ButterKnife.inject(this, rootView);

Context context = inflater.getContext();
mNoteFilterCache = new NoteFilterCache(context);

Resources resources = context.getResources();
mFilterRecyclerView.setAdapter(createAdapter(context));
mFilterRecyclerView.addItemDecoration(createDividerItemDecoration(resources));
mFilterRecyclerView.setLayoutManager(createLayoutManager(context, resources));

return rootView;
}


/**
* Create the adapter.
*
* @param context
* The context to use for creating the adapter.
*
* @return The adapter for managing {@link #mFilterRecyclerView}.
*/
private FilterColorFilterAdapter createAdapter(@NonNull Context context) {
FilterColorFilterAdapter adapter = new FilterColorFilterAdapter(context);
adapter.setOnColorChangeListener(mOnColorSelectChangeListener);
adapter.setSelectedColors(mNoteFilterCache.getEnabledColors());

return adapter;
}


/**
* Create a {@link DividerItemDecoration} for the RecyclerView.
*
* @param resources
* The resources to read for divider and orientation data.
*
* @return a new {@link DividerItemDecoration}.
*/
private DividerItemDecoration createDividerItemDecoration(Resources resources) {
int dividerHorizontal =
(int) resources.getDimension(R.dimen.color_filter_item_divider_horizontal);
int dividerVertical =
(int) resources.getDimension(R.dimen.color_filter_item_divider_vertical);

return new DividerItemDecoration(dividerVertical, dividerHorizontal);
}


/**
* Create a {@link ColorFilterLayoutManager} for the RecyclerView.
*
* @param context
* The context to assign to the layout manager.
* @param resources
* The resources to read for divider and orientation data.
*
* @return a new {@link ColorFilterLayoutManager}.
*/
private RecyclerView.LayoutManager createLayoutManager(Context context, Resources resources) {
boolean vertical = resources.getBoolean(R.bool.color_filter_toolbar_vertical);
int orientation = vertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL;
return new ColorFilterLayoutManager(context, orientation);
}
}

MainActivity


Last, but not least, is the MainActivity.  This class was gutted of most of the business logic.  It is now responsible for adding notes and adding the above fragments. 


/**
* The primary activity. It provides a list of notes and the tools to manage them.
*/
public class MainActivity extends AppCompatActivity {

/**
* The fragment tags.
*/
static class FragmentTags {
/**
* The tag for the note cards fragment.
*/
public static final String NOTE_CARDS_FRAGMENT_TAG = "NOTE_CARDS_FRAGMENT_TAG";

/**
* The tag for the color filter fragment.
*/
public static final String COLOR_FILTER_FRAGMENT_TAG = "COLOR_FILTER_FRAGMENT_TAG";
}

/**
* UI element that displays a toolbar
*/
@InjectView(R.id.activity_main_toolbar)
Toolbar mToolbar;


/**
* UI element that allows the user to add a note.
* @param view
*/
@OnClick(R.id.activity_main_add_view)
public void addViewClick(View view) {
createNewNote();
}


@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);

Fragment colorFilterSelectorFragment = ColorFilterFragment.newInstance();
Fragment noteCardsFragment = NoteCardsFragment.newInstance();

FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.activity_main_notes_container,
noteCardsFragment,
FragmentTags.NOTE_CARDS_FRAGMENT_TAG);

transaction.replace(R.id.activity_main_color_filter_container,
colorFilterSelectorFragment,
FragmentTags.COLOR_FILTER_FRAGMENT_TAG);
transaction.commit();
}


/**
* Launch a UI that allows the user to create a new note.
*/
private void createNewNote() {
startActivity(EditNoteActivity.createIntent(this, null));
}
}



Commits


These changes can be found at commits https://github.com/fsk-software/mynotes/commit/a0632503c5fabf74a31cca091d7975419d002328 and https://github.com/fsk-software/mynotes/commit/ec427c6a5684220836f823824d58a61cd2aea47b.

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.

Wednesday, June 3, 2015

MyNotes: ColorFilterToolbar Pt. 1

XML Resource Updates

This entry is going to focus on the XML resource changes for the update.

New UI

A lot of the heavy lifting for the new UI is actually done in the XML resources which makes the code a lot simpler compared to old design.   This includes a nice alpha effect that occurs on all supported versions of android.  It also includes an elevation transition when a filter is enabled/disabled and a ripple animation on press.  Unfortunately, these only occur on Lollipop.

Enter video caption here

Drawable Selector: Alpha Change

To get the alpha effect I updated the xml drawable selectors to include a new translucent color as the default selector.

drawable/blue_button_selector.xml:

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android"> 
  3.  
  4.     <item android:state_checked="true" 
  5.           android:drawable="@color/note_blue" /> 
  6.  
  7.     <item android:drawable="@color/note_blue_translucent" /> 
  8. </selector> 

values/colors_note.xml:






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <resources> 
  3.     <item name="note_blue" type="color">#99CCFF</item> 
  4.     <item name="note_blue_translucent" type="color">#5599CCFF</item> 
  5.  
  6.     <item name="note_gray" type="color">#C4C4C4</item> 
  7.     <item name="note_gray_translucent" type="color">#55C4C4C4</item> 
  8.  
  9.     <item name="note_green" type="color">#9BED9B</item> 
  10.     <item name="note_green_translucent" type="color">#559BED9B</item> 
  11.  
  12.     <item name="note_pink" type="color">#FFB2D1</item> 
  13.     <item name="note_pink_translucent" type="color">#55FFB2D1</item> 
  14.  
  15.     <item name="note_purple" type="color">#D1B2F0</item> 
  16.     <item name="note_purple_translucent" type="color">#55D1B2F0</item> 
  17.  
  18.     <item name="note_yellow" type="color">#FFFF99</item> 
  19.     <item name="note_yellow_translucent" type="color">#55FFFF99</item> 
  20. </resources> 

The selector switches the drawable used based on the state of the View.  In this case, my View is a ToggleButton so it supports the checked state. 


Elevation Change


This is done using a State List Animator that translates the View along the Z-Axis on a state change.


animator/color_filter_item_selector.xml:


All this does is change the elevation level based on the checked state of the View using the






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <!-- animate the elevation change when the color filter item is selected vs unselected. --> 
  3. <selector xmlns:android="http://schemas.android.com/apk/res/android"> 
  4.     <item android:state_checked="true"> 
  5.         <objectAnimator 
  6.             android:duration="@android:integer/config_shortAnimTime" 
  7.             android:propertyName="translationZ" 
  8.             android:valueFrom="@dimen/color_filter_item_off_elevation" 
  9.             android:valueTo="@dimen/color_filter_item_on_elevation" 
  10.             android:valueType="floatType"/> 
  11.     </item> 
  12.  
  13.     <item> 
  14.         <objectAnimator 
  15.             android:duration="@android:integer/config_shortAnimTime" 
  16.             android:propertyName="translationZ" 
  17.             android:valueFrom="@dimen/color_filter_item_off_elevation" 
  18.             android:valueTo="@dimen/color_filter_item_on_elevation" 
  19.             android:valueType="floatType"/> 
  20.     </item> 
  21. </selector> 

All this does is change the elevation level based on the checked state of the View.


The next step is where things get tricky.  Since I am supporting pre-Lollipop devices I wanted to use the StateListAnimator without getting a bunch of warnings.  To get around this I defined three Styles:


values/styles.xml:






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <resources> 
  3. ...  
  4.     <!-- The common attributes for the color filter items --> 
  5.     <style name="ColorFilterItemParent"> 
  6.         <item name="android:textOn"></item> 
  7.         <item name="android:textOff"></item> 
  8.     </style> 
  9. ...  
  10. </resources> 

This file contains the base styles.  The base styles contain the common attributes across all of the versions.  Then I derive the base style in styles_derived.xml.


values/styles_derived.xml






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <!-- This file contains the unique configurations for the derived styles from styles.xml  
  3.      In this case there aren't any additional details but this file exists to prevent the need  
  4.      to reduce the need to branch the layout folders. --> 
  5. <resources> 
  6.     ...  
  7.     <style name="ColorFilterItem" parent="ColorFilterItemParent" /> 
  8.     ...  
  9. </resources> 

This file isn’t that interesting because nothing really fun happens before Lollipop.  The lollipop specific attributes are in values-21/styles_derived.xml.


values-21/styles_derived.xml:







  1. <?xml version="1.0" encoding="utf-8"?> 
  2.  
  3. <!-- This file contains the unique configurations for the lollipop derived styles from styles.xml  
  4.      This file primarily exists to prevent the need to reduce the need to branch the layout  
  5.      folders. --> 
  6. <resources> 
  7.     ...  
  8.     <style name="ColorFilterItem" parent="ColorFilterItemParent"> 
  9.         <item name="android:stateListAnimator">@animator/color_filter_item_selector</item> 
  10.     </style> 
  11.     ...  
  12. </resources> 

This comes together in the layout file.


layout/recycler_item_color_filter_item.xml:






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <ToggleButton 
  3.     android:id="@+id/recycler_item_color_filter_check_view" 
  4.     xmlns:android="http://schemas.android.com/apk/res/android" 
  5.     style="@style/ColorFilterItem" 
  6.     android:layout_width="match_parent" 
  7.     android:layout_height="@dimen/color_filter_item_long_side" /> 

The style attribute will pick the correct style for the SDK version being run on the device.  This way only the Lollipop versions get the stateListAnimator and I don’t get a bunch of warnings in the xml.


Ripple Animation


This is only supported in Lollipop, but I want a common background resource.  To fix this I take advantage of the different resource configurations.  The ripple is in the v21 drawable folders.


drawable-v21/blue_color_filter_background.xml:






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <ripple xmlns:android="http://schemas.android.com/apk/res/android" 
  3.         android:color="@android:color/white"> 
  4.     <item android:drawable="@drawable/blue_note_selector"/> 
  5. </ripple> 

What this is doing is creating a white ripple on press.  I still want my normal selector to work, so I specified that as the item drawable.


Since this only works in Lollipop, I created a version of the blue_color_filter_background.xml drawable for the pre-Lollipop builds:


drawable/blue_color_filter_background.xml:






  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <!-- Exists to support Pre-Lollipop Versions with a single NoteColors enum --> 
  3. <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> 
  4.     <item android:drawable="@drawable/blue_note_selector"/> 
  5. </layer-list> 

Commit


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