diff --git a/NEWS.mkd b/NEWS.mkd index 6c89929..285a04e 100644 --- a/NEWS.mkd +++ b/NEWS.mkd @@ -1,6 +1,10 @@ -### version 1.2 +### version 1.3.0 +* No more pauses when scrolling through the history +* Fixed an error where the wrong razor could be shown on the modify screen + +### version 1.2.0 * new feature: keep track of multiple razors! * update to the latest libraries * adjustments have been made to make for a more pleasant experience diff --git a/bladeReminder/build.gradle b/bladeReminder/build.gradle index 2ba8ddc..93326f0 100644 --- a/bladeReminder/build.gradle +++ b/bladeReminder/build.gradle @@ -93,14 +93,15 @@ preBuild.dependsOn myPrebuildTask dependencies { compile 'org.florescu.android.rangeseekbar:rangeseekbar-library:0.3.0' - compile 'com.android.support:appcompat-v7:23.3.0' - compile 'com.android.support:design:23.3.0' - compile 'com.android.support:support-v4:23.3.0' - compile 'com.android.support:support-annotations:23.3.0' - compile 'com.android.support:preference-v7:23.3.0' + compile 'com.android.support:appcompat-v7:24.0.0' + compile 'com.android.support:design:24.0.0' + compile 'com.android.support:support-v4:24.0.0' + compile 'com.android.support:support-annotations:24.0.0' + compile 'com.android.support:preference-v7:23.4.0' + compile 'com.android.support:recyclerview-v7:24.0.0' compile 'com.google.guava:guava:19.0' - compile 'com.jakewharton:butterknife:8.0.1' - apt 'com.jakewharton:butterknife-compiler:8.0.1' + compile 'com.jakewharton:butterknife:8.1.0' + apt 'com.jakewharton:butterknife-compiler:8.1.0' compile 'com.jakewharton.timber:timber:4.1.2' compile 'com.opencsv:opencsv:3.7' // Testing-only dependencies diff --git a/bladeReminder/src/main/AndroidManifest.xml b/bladeReminder/src/main/AndroidManifest.xml index 3b80336..02cc308 100644 --- a/bladeReminder/src/main/AndroidManifest.xml +++ b/bladeReminder/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="130" + android:versionName="1.3.0" > extends RecyclerView.Adapter implements Filterable, + CursorFilter.CursorFilterClient { + + /** + * Call when bind view with the cursor + * + * @param holder RecyclerView.ViewHolder + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param position the position. + */ + public abstract void onBindViewHolder(VH holder, Cursor cursor, int position); + + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mDataValid; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Cursor mCursor; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int mRowIDColumn; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected ChangeObserver mChangeObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected DataSetObserver mDataSetObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected CursorFilter mCursorFilter; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected FilterQueryProvider mFilterQueryProvider; + + /** + * If set the adapter will register a content observer on the cursor and will call + * {@link #onContentChanged()} when a notification comes in. Be careful when + * using this flag: you will need to unset the current Cursor from the adapter + * to avoid leaks due to its registered observers. This flag is not needed + * when using a CursorAdapter with a + * {@link android.content.CursorLoader}. + */ + public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; + + /** + * Constructor that flags always FLAG_REGISTER_CONTENT_OBSERVER. + * + * @param c The cursor from which to get the data. + * @param context The context + * This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + public BaseAbstractRecycleCursorAdapter(Context context, Cursor c) { + this(context, c, FLAG_REGISTER_CONTENT_OBSERVER); + } + + /** + * Recommended constructor. + * + * @param c The cursor from which to get the data. + * @param context The context + * @param flags Flags used to determine the behavior of the adapter; + * Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}. + */ + public BaseAbstractRecycleCursorAdapter(Context context, Cursor c, int flags) { + init(context, c, flags); + } + + void init(Context context, Cursor c, int flags) { + boolean cursorPresent = c != null; + mCursor = c; + mDataValid = cursorPresent; + mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; + if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { + mChangeObserver = new ChangeObserver(); + mDataSetObserver = new MyDataSetObserver(); + } else { + mChangeObserver = null; + mDataSetObserver = null; + } + + if (cursorPresent) { + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); + } + + setHasStableIds(true);//这个地方要注意一下,需要将表关联ID设置为true + } + + /** + * Returns the cursor. + * + * @return the cursor. + */ + public Cursor getCursor() { + return mCursor; + } + + /** + * @see android.support.v7.widget.RecyclerView.Adapter#getItemCount() + */ + @Override + public int getItemCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + public Object getItem(int position) { + if (mDataValid && mCursor != null) { + mCursor.moveToPosition(position); + return mCursor; + } else { + return null; + } + } + + /** + * @param position Adapter position to query + * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int) + */ + @Override + public long getItemId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + @Override + public void onBindViewHolder(VH holder, int position) { + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + onBindViewHolder(holder, mCursor, position); + } + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + * + * @param cursor The new cursor to be used + */ + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(Cursor)}, the returned old Cursor is not + * closed. + * + * @param newCursor The new cursor to be used. + * @return Returns the previously set Cursor, or null if there wasa not one. + * If the given new Cursor is the same instance is the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + if (oldCursor != null) { + if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); + if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = newCursor; + if (newCursor != null) { + if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter + notifyDataSetChanged(); + } + return oldCursor; + } + + /** + *

Converts the cursor into a CharSequence. Subclasses should override this + * method to convert their results. The default implementation returns an + * empty String for null values or the default String representation of + * the value.

+ * + * @param cursor the cursor to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + *

+ * The query is provided by a + * {@link android.widget.FilterQueryProvider}. + * If no provider is specified, the current cursor is not filtered and returned. + *

+ * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} + * and the previous cursor is closed. + *

+ * This method is always executed on a background thread, not on the + * application's main thread (or UI thread.) + *

+ * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must be filtered + * @return a Cursor representing the results of the new query + * @see #getFilter() + * @see #getFilterQueryProvider() + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mCursor; + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * Returns the query filter provider used for filtering. When the + * provider is null, no filtering occurs. + * + * @return the current filter query provider or null if it does not exist + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * Sets the query filter provider used to filter the current Cursor. + * The provider's + * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} + * method is invoked when filtering is requested by a client of + * this adapter. + * + * @param filterQueryProvider the filter query provider or null to remove it + * @see #getFilterQueryProvider() + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + /** + * Called when the {@link android.database.ContentObserver} on the cursor receives a change notification. + * The default implementation provides the auto-requery logic, but may be overridden by + * sub classes. + * + * @see android.database.ContentObserver#onChange(boolean) + */ + protected void onContentChanged() { + + } + + private class ChangeObserver extends ContentObserver { + public ChangeObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter + notifyDataSetChanged(); + } + } +} diff --git a/bladeReminder/src/main/java/com/frankzhu/recyclerviewdemo/adapter/CursorFilter.java b/bladeReminder/src/main/java/com/frankzhu/recyclerviewdemo/adapter/CursorFilter.java new file mode 100644 index 0000000..aa8d8cf --- /dev/null +++ b/bladeReminder/src/main/java/com/frankzhu/recyclerviewdemo/adapter/CursorFilter.java @@ -0,0 +1,59 @@ +package com.frankzhu.recyclerviewdemo.adapter; + +import android.database.Cursor; +import android.widget.Filter; + +/** + * Author: ZhuWenWu + * Version V1.0 + * Date: 2014/12/30 17:15. + * Description: 数据库Cursor筛选 + * Modification History: + * Date Author Version Description + * ----------------------------------------------------------------------------------- + * 2014/12/30 ZhuWenWu 1.0 1.0 + * Why & What is modified: + */ +public class CursorFilter extends Filter { + CursorFilterClient mClient; + + interface CursorFilterClient { + CharSequence convertToString(Cursor cursor); + Cursor runQueryOnBackgroundThread(CharSequence constraint); + Cursor getCursor(); + void changeCursor(Cursor cursor); + } + + CursorFilter(CursorFilterClient client) { + mClient = client; + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + return mClient.convertToString((Cursor) resultValue); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); + + FilterResults results = new FilterResults(); + if (cursor != null) { + results.count = cursor.getCount(); + results.values = cursor; + } else { + results.count = 0; + results.values = null; + } + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + Cursor oldCursor = mClient.getCursor(); + + if (results.values != null && results.values != oldCursor) { + mClient.changeCursor((Cursor) results.values); + } + } +} diff --git a/bladeReminder/src/main/java/com/github/gist/ssins/EndlessRecyclerOnScrollListener.java b/bladeReminder/src/main/java/com/github/gist/ssins/EndlessRecyclerOnScrollListener.java new file mode 100644 index 0000000..f27d80c --- /dev/null +++ b/bladeReminder/src/main/java/com/github/gist/ssins/EndlessRecyclerOnScrollListener.java @@ -0,0 +1,45 @@ +package com.github.gist.ssins; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +public abstract class EndlessRecyclerOnScrollListener extends RecyclerView.OnScrollListener { + + private int mPreviousTotal = 0; // The total number of items in the dataset after the last load + private boolean mLoading = true; // True if we are still waiting for the last set of data to load. + private int mVisibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more. + int mFirstVisibleItem, mVisibleItemCount, mTotalItemCount; + + private int mCurrentPage = 1; + + private final LinearLayoutManager mLinearLayoutManager; + + public EndlessRecyclerOnScrollListener(LinearLayoutManager linearLayoutManager) { + mLinearLayoutManager = linearLayoutManager; + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + mVisibleItemCount = recyclerView.getChildCount(); + mTotalItemCount = mLinearLayoutManager.getItemCount(); + mFirstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition(); + + if (mLoading && mTotalItemCount > mPreviousTotal) { + mLoading = false; + mPreviousTotal = mTotalItemCount; + } + if (!mLoading && + (mTotalItemCount - mVisibleItemCount) <= (mFirstVisibleItem + mVisibleThreshold)) { + // End has been reached + // Do something + mCurrentPage++; + onLoadMore(mCurrentPage); + mLoading = true; + } + } + + public abstract void onLoadMore(int mCurrentPage); +} + diff --git a/bladeReminder/src/main/java/es/quirk/bladereminder/activities/ModifyShaveActivity.java b/bladeReminder/src/main/java/es/quirk/bladereminder/activities/ModifyShaveActivity.java index 0c44b28..635ec2a 100644 --- a/bladeReminder/src/main/java/es/quirk/bladereminder/activities/ModifyShaveActivity.java +++ b/bladeReminder/src/main/java/es/quirk/bladereminder/activities/ModifyShaveActivity.java @@ -109,7 +109,7 @@ public void onClick(View arg0) { String [] ra = new String[size]; mRazorChoice.setDisplayedValues(razors.toArray(ra)); try { - mRazorChoice.setValue(Integer.parseInt(originalEntry.getRazor()) - 1); + mRazorChoice.setValue(dataSource.getRazorPosition(Integer.parseInt(originalEntry.getRazor()))); } catch (NumberFormatException ex) { // can't deal.. setRazorChoiceVisibility(View.GONE); diff --git a/bladeReminder/src/main/java/es/quirk/bladereminder/adapter/ShaveEntryAdapter.java b/bladeReminder/src/main/java/es/quirk/bladereminder/adapter/ShaveEntryAdapter.java new file mode 100644 index 0000000..fce5255 --- /dev/null +++ b/bladeReminder/src/main/java/es/quirk/bladereminder/adapter/ShaveEntryAdapter.java @@ -0,0 +1,119 @@ +package es.quirk.bladereminder.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import android.widget.TextView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import com.frankzhu.recyclerviewdemo.adapter.BaseAbstractRecycleCursorAdapter; + +import es.quirk.bladereminder.R; +import es.quirk.bladereminder.ShaveEntry; +import es.quirk.bladereminder.database.DataSource; +import es.quirk.bladereminder.widgets.DateLabel; +import es.quirk.bladereminder.widgets.UsesView; + +public class ShaveEntryAdapter extends BaseAbstractRecycleCursorAdapter { + + private final LayoutInflater mLayoutInflater; + private final IClickListener mClickListener; + private int mSelectedPosition = 0; + + public ShaveEntryAdapter(Context context, IClickListener clickListener) { + super(context, null); + mLayoutInflater = LayoutInflater.from(context); + mClickListener = clickListener; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (position > 0) { + // this ends up calling the (holder, Cursor, position) version + super.onBindViewHolder(holder, position - 1); + } else { + // do stuff with header here + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, Cursor cursor, int position) { + if (holder instanceof NormalTextViewHolder) { + ShaveEntry entry = ShaveEntry.fromCursor(cursor); + holder.itemView.setSelected(mSelectedPosition == position); + ((NormalTextViewHolder) holder).fillFrom(entry); + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == 0) { + return new HeaderViewHolder(mLayoutInflater.inflate(R.layout.view_list_item_header, parent, false)); + } else { + return new NormalTextViewHolder(mLayoutInflater.inflate(R.layout.shaveentry, parent, false)); + } + } + + @Override + public int getItemViewType(int position) { + return position; + } + + public interface IClickListener { + public void onItemClick(long id); + public int getRazorId(); + } + + public class NormalTextViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.date_label) DateLabel mDateLabel; + @BindView(R.id.count_label) UsesView mCountLabel; + @BindView(R.id.comment) TextView mComment; + long mShaveId; + + NormalTextViewHolder(View view) { + super(view); + ButterKnife.bind(this, view); + } + + public void fillFrom(ShaveEntry entry) { + int count = entry.getCount(); + mDateLabel.setText(entry.getDate()); + mCountLabel.setText(Integer.toString(count)); + mComment.setText(entry.getComment()); + mShaveId = entry.getID(); + + int razorId = mClickListener.getRazorId(); + try { + razorId = Integer.parseInt(entry.getRazor()); + } catch (NumberFormatException ex) { + } + boolean isEnabled = count == 0 || razorId == mClickListener.getRazorId(); + mDateLabel.setEnabled(isEnabled); + mCountLabel.setEnabled(isEnabled); + mComment.setEnabled(isEnabled); + } + + @OnClick(R.id.linear_container) + void onItemClick() { + mSelectedPosition = getLayoutPosition(); + notifyItemChanged(mSelectedPosition); + mClickListener.onItemClick(mShaveId); + } + } + + public class HeaderViewHolder extends RecyclerView.ViewHolder { + @BindView(R.id.column_header1) View mDateLabel; + @BindView(R.id.column_header2) View mCountLabel; + @BindView(R.id.column_header3) View mComment; + + HeaderViewHolder(View view) { + super(view); + ButterKnife.bind(this, view); + } + } +} diff --git a/bladeReminder/src/main/java/es/quirk/bladereminder/database/DataSource.java b/bladeReminder/src/main/java/es/quirk/bladereminder/database/DataSource.java index ae0de55..396845e 100644 --- a/bladeReminder/src/main/java/es/quirk/bladereminder/database/DataSource.java +++ b/bladeReminder/src/main/java/es/quirk/bladereminder/database/DataSource.java @@ -216,6 +216,11 @@ public void toCSV(@NonNull FileOutputStream outstream) throws IOException { countStr = Integer.toString(count); cursor.moveToNext(); + // avoid spitting out a razor name for count + if (count == 0) { + razorName = ""; + countStr = "-"; + } } outputData[0] = thisDay; outputData[1] = countStr; @@ -456,6 +461,41 @@ public void editRazor(String position, String newName) { } } + /** + * Given an ID, get the index in the table of this ID. + */ + public int getRazorPosition(int id) { + // since _ID is random, just the the 1st, 2nd, 3rd... from the list + int result = 0; + boolean didOpen = openIfNeeded(); + try { + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(Contract.Razors.TABLE_NAME); + String[] projection = { Contract.Razors._ID }; + Cursor cursor = queryBuilder.query(mDatabase, projection, null, + null, null, null, null); + cursor.moveToFirst(); + boolean found = false; + while (!cursor.isAfterLast()) { + int cursorId = cursor.getInt(0); + cursor.moveToNext(); + if (cursorId == id) { + found = true; + break; + } + result++; + } + if (!found) { + result = 0; + } + cursor.close(); + } finally { + if (didOpen) + close(); + } + return result; + } + public int getRazorId(int position) { // since _ID is random, just the the 1st, 2nd, 3rd... from the list int result = 1; diff --git a/bladeReminder/src/main/java/es/quirk/bladereminder/fragments/ShaveListFragment.java b/bladeReminder/src/main/java/es/quirk/bladereminder/fragments/ShaveListFragment.java index f7daca7..2b29a65 100644 --- a/bladeReminder/src/main/java/es/quirk/bladereminder/fragments/ShaveListFragment.java +++ b/bladeReminder/src/main/java/es/quirk/bladereminder/fragments/ShaveListFragment.java @@ -24,25 +24,29 @@ import android.support.v4.content.Loader; import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode.Callback; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import com.google.common.base.Optional; +import com.github.gist.ssins.EndlessRecyclerOnScrollListener; + import java.text.DateFormat; import java.util.Calendar; import java.util.Date; import butterknife.BindView; import butterknife.ButterKnife; -import butterknife.OnItemClick; import butterknife.Unbinder; + +import timber.log.Timber; + import es.quirk.bladereminder.EditShaveMenu; import es.quirk.bladereminder.FabIconUpdater; import es.quirk.bladereminder.NewShaveMenu; @@ -51,20 +55,22 @@ import es.quirk.bladereminder.SoundHelper; import es.quirk.bladereminder.Utils; import es.quirk.bladereminder.activities.BladeReminderActivity; +import es.quirk.bladereminder.adapter.ShaveEntryAdapter; import es.quirk.bladereminder.contentprovider.ShaveEntryContentProvider; import es.quirk.bladereminder.database.Contract; import es.quirk.bladereminder.database.Contract.Shaves; import es.quirk.bladereminder.database.DataSource; import es.quirk.bladereminder.tasks.BackFiller; +import es.quirk.bladereminder.widgets.DividerItemDecoration; import es.quirk.bladereminder.widgets.TextDrawable; import es.quirk.bladereminder.widgets.TextDrawableFactory; -import timber.log.Timber; /** * Fragment that holds the main list and floating action button. */ public class ShaveListFragment extends Fragment - implements OnScrollListener, + implements + ShaveEntryAdapter.IClickListener, INotesEditorListener, IAddRazorListener, LoaderCallbacks { @@ -74,16 +80,12 @@ public class ShaveListFragment extends Fragment private final static int LIST_THRESHOLD = 2; private final static String[] PROJECTION = { Shaves._ID, Shaves.DATE, Shaves.COUNT, Shaves.COMMENT, Shaves.RAZOR }; private static final int DIALOG_FRAGMENT = 1; - private int mListViewLastCount; - private View mHeaderView; - @BindView(R.id.shaveList) ListView mListView; + @BindView(R.id.shaveList) RecyclerView mRecyclerView; @BindView(R.id.action_button) FloatingActionButton mFAB; @BindView(R.id.coordinator_layout) CoordinatorLayout mCoordLayout; - private int mPreviousVisibleItem; - private final View [] mColumnHeaders = new View[3]; @Nullable private BladeReminderActivity mMainActivity; - private SimpleCursorAdapter mAdapter; + private ShaveEntryAdapter mAdapter; private DataSource mDataSource; private final Callback mEditModeCallback = new NewShaveMenu(this); private final Callback mEditEntryCallback = new EditShaveMenu(this); @@ -107,11 +109,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, View rootView = inflater.inflate(R.layout.fragment_shave_list, container, false); mUnbinder = ButterKnife.bind(this, rootView); mRootView = rootView; - mHeaderView = inflater.inflate(R.layout.view_list_item_header, mListView, false); - mColumnHeaders[0] = ButterKnife.findById(mHeaderView, R.id.column_header1); - mColumnHeaders[1] = ButterKnife.findById(mHeaderView, R.id.column_header2); - mColumnHeaders[2] = ButterKnife.findById(mHeaderView, R.id.column_header3); - mListView.addHeaderView(mHeaderView, null, false); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); updateFabIcon(); FabIconUpdater.register(this); @@ -126,53 +123,38 @@ public void onClick(View v) { } }); - String[] columns = new String[] { Shaves.DATE, Shaves.COUNT, Shaves.COMMENT, Shaves.RAZOR}; - // Fields on the UI to which we map - int[] layoutIds = new int[] { - R.id.date_label, - R.id.count_label, - R.id.comment, - R.id.razor, - }; getLoaderManager().initLoader(0, null, this); - mAdapter = new SimpleCursorAdapter(getActivity(), R.layout.shaveentry, null, columns, - layoutIds, 0) { - @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - String countStr = cursor.getString(2); - String razorStr = cursor.getString(4); - view.setEnabled(true); - - if (!(view instanceof LinearLayout)) - return; - LinearLayout layout = (LinearLayout) view; - for (int i = 0; i < layout.getChildCount(); i++) { - View child = layout.getChildAt(i); - child.setEnabled(true); - } - try { - int count = Integer.parseInt(countStr); - int razorId = Integer.parseInt(razorStr); - if (count > 0 && razorId != mRazorId) { - Timber.d(" view.setBackgroundColor WHITE! %s", view.toString()); - for (int i = 0; i < layout.getChildCount(); i++) { - View child = layout.getChildAt(i); - child.setEnabled(false); - } - } - } catch (NumberFormatException ex) { - } - } - }; - mListView.setAdapter(mAdapter); - mListView.setOnScrollListener(this); backfill(); mCurrentPage = 0; mSoundHelper = new SoundHelper(rootView.getContext()); return rootView; } + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + LinearLayoutManager llm = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(llm); + mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity())); + mAdapter = new ShaveEntryAdapter(getActivity(), this); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.addOnScrollListener(new EndlessRecyclerOnScrollListener(llm) { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + if (dy > 0) + mFAB.setVisibility(View.INVISIBLE); + else + mFAB.setVisibility(View.VISIBLE); + } + @Override + public void onLoadMore(int currentPage) { + mCurrentPage = currentPage - 1; + getLoaderManager().restartLoader(0, null, ShaveListFragment.this); + } + }); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -288,8 +270,8 @@ protected void onPostExecute(Optional maybeSelected) { } } - @OnItemClick(R.id.shaveList) - void onItemClick(long id) { + @Override + public void onItemClick(long id) { if (mActionMode.isPresent()) { mActionMode.get().finish(); } @@ -297,6 +279,11 @@ void onItemClick(long id) { new ItemClickTask().execute(); } + @Override + public int getRazorId() { + return mRazorId; + } + private void heroaction() { // find newest entry in database // "select id from shaves order by date DESC LIMIT 1"; @@ -554,45 +541,6 @@ private void doUndo(@NonNull ShaveEntry lastDeleted, int lastDeletedPosition) { new SaveNew(lastDeletedPosition, getActivity().getContentResolver()).execute(lastDeleted); } - private void updateFloatingActionButton(int firstVisibleItem) { - // go up = vis, down = hidden - if (firstVisibleItem > mPreviousVisibleItem) { - mFAB.setVisibility(View.INVISIBLE); - } else if (firstVisibleItem < mPreviousVisibleItem) { - mFAB.setVisibility(View.VISIBLE); - } - mPreviousVisibleItem = firstVisibleItem; - } - - /** - * Required for the OnScrollListener interface. - */ - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - updateFloatingActionButton(firstVisibleItem); - final float top = -mHeaderView.getTop(); - float height = mHeaderView.getHeight(); - if (top > height) - return; - for (View textView : mColumnHeaders) { - textView.setTranslationY(top / 2f); - } - - } - - @Override - public void onScrollStateChanged(AbsListView listView, int scrollState) { - int count = mListView.getCount(); - if (scrollState == SCROLL_STATE_IDLE) { - if (count > mListViewLastCount && - mListView.getLastVisiblePosition() >= (count - 1 - LIST_THRESHOLD)) { - mCurrentPage++; - getLoaderManager().restartLoader(0, null, this); - mListViewLastCount = count; - Timber.d("mCurrentPage = %d", mCurrentPage); - } - } - } public void setPagerAdapter(FragmentStatePagerAdapter adapter) { mPagerAdapter = adapter; @@ -678,8 +626,8 @@ public Loader onCreateLoader(int id, Bundle args) { } @Override - public void onLoadFinished(Loader loader, Cursor data) { - mAdapter.swapCursor(data); + public void onLoadFinished(Loader loader, Cursor cursor) { + mAdapter.changeCursor(cursor); if (mMainActivity != null) mMainActivity.stop(); } @@ -687,6 +635,6 @@ public void onLoadFinished(Loader loader, Cursor data) { @Override public void onLoaderReset(Loader loader) { // data is not available anymore, delete reference - mAdapter.swapCursor(null); + mAdapter.changeCursor(null); } } diff --git a/bladeReminder/src/main/java/es/quirk/bladereminder/widgets/DividerItemDecoration.java b/bladeReminder/src/main/java/es/quirk/bladereminder/widgets/DividerItemDecoration.java new file mode 100644 index 0000000..d923b8d --- /dev/null +++ b/bladeReminder/src/main/java/es/quirk/bladereminder/widgets/DividerItemDecoration.java @@ -0,0 +1,46 @@ +package es.quirk.bladereminder.widgets; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +// Taken from https://stackoverflow.com/a/27037230 + +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private Drawable mDivider; + + /** + * Default divider will be used + */ + public DividerItemDecoration(Context context) { + final TypedArray styledAttributes = context.obtainStyledAttributes(ATTRS); + mDivider = styledAttributes.getDrawable(0); + styledAttributes.recycle(); + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + int left = parent.getPaddingLeft(); + int right = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + + int top = child.getBottom() + params.bottomMargin; + int bottom = top + mDivider.getIntrinsicHeight(); + + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } +} diff --git a/bladeReminder/src/main/res/layout/fragment_shave_list.xml b/bladeReminder/src/main/res/layout/fragment_shave_list.xml index 2bc4f68..a66b993 100644 --- a/bladeReminder/src/main/res/layout/fragment_shave_list.xml +++ b/bladeReminder/src/main/res/layout/fragment_shave_list.xml @@ -15,7 +15,7 @@ android:layout_height="match_parent" android:orientation="vertical" > - +

    +
  • RecyclerView Code by Frank Zhu
  • +
+
+Copyright 2014 Frank Zhu
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+ diff --git a/build.gradle b/build.gradle index 193b0f3..ac02cd2 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }