diff --git a/app/build.gradle b/app/build.gradle index 5d0796f..8d915aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,6 +34,7 @@ ext { dependencies { compile "com.android.support:appcompat-v7:$supportLibraryVersion" + compile "com.android.support:recyclerview-v7:$supportLibraryVersion" compile "com.android.support:cardview-v7:$supportLibraryVersion" compile 'com.github.chrisbanes.photoview:library:1.2.4' } diff --git a/app/src/main/java/android/support/v7/widget/ConcatAdapter.java b/app/src/main/java/android/support/v7/widget/ConcatAdapter.java new file mode 100644 index 0000000..ae41adb --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/ConcatAdapter.java @@ -0,0 +1,202 @@ +package android.support.v7.widget; + +import android.support.annotation.NonNull; +import android.util.SparseArray; +import android.view.ViewGroup; + +import java.util.Arrays; +import java.util.List; + +/** + * Adapter which concatenates the items of multiple adapters. + * Doesn't support stable ids, but properly delegates changes notifications. + *

+ * Adapters may provide multiple view types but they must not overlap. + * It's recommended to always use the item layout id as view type. + *

+ * Warning: You need to use ConcatAdapter.getAdapterPosition(ViewHolder) + * in place of ViewHolder.getAdapterPosition() inside child adapters. + * + * @author Christophe Beyls + */ +public class ConcatAdapter extends RecyclerView.Adapter { + + private final RecyclerView.Adapter[] adapters; + private final RecyclerView.AdapterDataObserver[] adapterObservers; + final int[] offsets; + private final SparseArray> viewTypeAdapters = new SparseArray<>(); + + private class InternalObserver extends RecyclerView.AdapterDataObserver { + + private final int adapterIndex; + + InternalObserver(int adapterIndex) { + this.adapterIndex = adapterIndex; + } + + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + notifyItemRangeChanged(positionStart + offsets[adapterIndex], itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + notifyItemRangeChanged(positionStart + offsets[adapterIndex], itemCount, payload); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + for (int i = adapterIndex + 1, size = offsets.length; i < size; ++i) { + offsets[i] += itemCount; + } + notifyItemRangeInserted(positionStart + offsets[adapterIndex], itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + for (int i = adapterIndex + 1, size = offsets.length; i < size; ++i) { + offsets[i] -= itemCount; + } + notifyItemRangeRemoved(positionStart + offsets[adapterIndex], itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + final int offset = offsets[adapterIndex]; + notifyItemMoved(fromPosition + offset, toPosition + offset); + } + } + + @SuppressWarnings("unchecked") + public ConcatAdapter(RecyclerView.Adapter... adapters) { + this.adapters = (RecyclerView.Adapter[]) adapters; + final int size = adapters.length; + adapterObservers = new RecyclerView.AdapterDataObserver[size]; + for (int i = 0; i < size; ++i) { + adapterObservers[i] = new InternalObserver(i); + } + offsets = new int[size]; + } + + /** + * @return The adapter position relative to the child adapter, if any. + */ + public static int getAdapterPosition(@NonNull RecyclerView.ViewHolder holder) { + int position = holder.getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + RecyclerView.Adapter adapter = holder.mOwnerRecyclerView.getAdapter(); + if (adapter instanceof ConcatAdapter) { + final int[] offsets = ((ConcatAdapter) adapter).offsets; + final int index = Arrays.binarySearch(offsets, position); + if (index >= 0) { + position = 0; + } else { + position -= offsets[~index - 1]; + } + } + } + return position; + } + + private int getAdapterIndexForPosition(int position) { + int index = Arrays.binarySearch(offsets, position); + return (index < 0) ? ~index - 1 : index; + } + + @Override + public int getItemViewType(int position) { + final int index = getAdapterIndexForPosition(position); + RecyclerView.Adapter adapter = adapters[index]; + int viewType = adapter.getItemViewType(position - offsets[index]); + if (viewTypeAdapters.get(viewType) == null) { + viewTypeAdapters.put(viewType, adapter); + } + return viewType; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return viewTypeAdapters.get(viewType).onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + final int index = getAdapterIndexForPosition(position); + adapters[index].onBindViewHolder(holder, position - offsets[index]); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) { + final int index = getAdapterIndexForPosition(position); + adapters[index].onBindViewHolder(holder, position - offsets[index], payloads); + } + + @Override + public int getItemCount() { + int count = 0; + for (int i = 0, size = adapters.length; i < size; ++i) { + offsets[i] = count; + count += adapters[i].getItemCount(); + } + return count; + } + + @Override + public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + if (!hasObservers()) { + for (int i = 0, size = adapters.length; i < size; ++i) { + adapters[i].registerAdapterDataObserver(adapterObservers[i]); + } + } + super.registerAdapterDataObserver(observer); + } + + @Override + public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.unregisterAdapterDataObserver(observer); + if (!hasObservers()) { + for (int i = 0, size = adapters.length; i < size; ++i) { + adapters[i].unregisterAdapterDataObserver(adapterObservers[i]); + } + } + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + for (RecyclerView.Adapter adapter : adapters) { + adapter.onAttachedToRecyclerView(recyclerView); + } + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + for (RecyclerView.Adapter adapter : adapters) { + adapter.onDetachedFromRecyclerView(recyclerView); + } + } + + @Override + public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { + viewTypeAdapters.get(holder.getItemViewType()).onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) { + viewTypeAdapters.get(holder.getItemViewType()).onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + viewTypeAdapters.get(holder.getItemViewType()).onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) { + return viewTypeAdapters.get(holder.getItemViewType()).onFailedToRecycleView(holder); + } +} diff --git a/app/src/main/java/android/support/v7/widget/DividerItemDecoration.java b/app/src/main/java/android/support/v7/widget/DividerItemDecoration.java new file mode 100644 index 0000000..47490e8 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/DividerItemDecoration.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * 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. + */ + + +package android.support.v7.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.view.View; +import android.widget.LinearLayout; + +/** + * DividerItemDecoration is a {@link RecyclerView.ItemDecoration} that can be used as a divider + * between items of a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and + * {@link #VERTICAL} orientations. + *

+ *

+ *     mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
+ *             mLayoutManager.getOrientation());
+ *     recyclerView.addItemDecoration(mDividerItemDecoration);
+ * 
+ */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + + private Drawable mDivider; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + private int mOrientation; + + private final Rect mBounds = new Rect(); + + /** + * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a + * {@link LinearLayoutManager}. + * + * @param context Current context, it will be used to access resources. + * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + /** + * Sets the orientation for this divider. This should be called if + * {@link RecyclerView.LayoutManager} changes orientation. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException( + "Invalid orientation. It should be either HORIZONTAL or VERTICAL"); + } + mOrientation = orientation; + } + + /** + * Sets the {@link Drawable} for this divider. + * + * @param drawable Drawable that should be used as a divider. + */ + public void setDrawable(@NonNull Drawable drawable) { + if (drawable == null) { + throw new IllegalArgumentException("Drawable cannot be null."); + } + mDivider = drawable; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (parent.getLayoutManager() == null) { + return; + } + if (mOrientation == VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + private static void getDecoratedBoundsWithMargins(View view, Rect outBounds) { + final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + outBounds.set(view.getLeft() - insets.left - lp.leftMargin, + view.getTop() - insets.top - lp.topMargin, + view.getRight() + insets.right + lp.rightMargin, + view.getBottom() + insets.bottom + lp.bottomMargin); + } + + private void drawVertical(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int left = 0; + final int right = parent.getWidth(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + getDecoratedBoundsWithMargins(child, mBounds); + final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child)); + final int top = bottom - mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int top = 0; + final int bottom = parent.getHeight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + getDecoratedBoundsWithMargins(child, mBounds); + final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child)); + final int left = right - mDivider.getIntrinsicWidth(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.java b/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.java index 8404797..c1734aa 100644 --- a/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.java +++ b/app/src/main/java/be/digitalia/fosdem/adapters/BookmarksAdapter.java @@ -3,13 +3,19 @@ package be.digitalia.fosdem.adapters; import android.content.Context; import android.database.Cursor; import android.graphics.Typeface; +import android.os.AsyncTask; +import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import java.util.Date; @@ -17,20 +23,76 @@ import java.util.Date; import be.digitalia.fosdem.R; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.model.Event; +import be.digitalia.fosdem.widgets.MultiChoiceHelper; public class BookmarksAdapter extends EventsAdapter { @ColorInt private final int errorColor; + final MultiChoiceHelper multiChoiceHelper; - public BookmarksAdapter(Context context) { - super(context); - errorColor = ContextCompat.getColor(context, R.color.error_material); + public BookmarksAdapter(AppCompatActivity activity) { + super(activity); + errorColor = ContextCompat.getColor(activity, R.color.error_material); + multiChoiceHelper = new MultiChoiceHelper(activity, this); + multiChoiceHelper.setMultiChoiceModeListener(new MultiChoiceHelper.MultiChoiceModeListener() { + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu); + return true; + } + + private void updateSelectedCountDisplay(ActionMode mode) { + int count = multiChoiceHelper.getCheckedItemCount(); + mode.setTitle(multiChoiceHelper.getContext().getResources().getQuantityString(R.plurals.selected, count, count)); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + updateSelectedCountDisplay(mode); + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.delete: + // Remove multiple bookmarks at once + new RemoveBookmarksAsyncTask().execute(multiChoiceHelper.getCheckedItemIds()); + mode.finish(); + return true; + } + return false; + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + updateSelectedCountDisplay(mode); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + }); + } + + public Parcelable onSaveInstanceState() { + return multiChoiceHelper.onSaveInstanceState(); + } + + public void onRestoreInstanceState(Parcelable state) { + multiChoiceHelper.onRestoreInstanceState(state); + } + + public void onDestroyView() { + multiChoiceHelper.clearChoices(); } @Override - public void bindView(View view, Context context, Cursor cursor) { - ViewHolder holder = (ViewHolder) view.getTag(); + public void onBindViewHolder(ViewHolder holder, Cursor cursor) { + final int position = cursor.getPosition(); + Context context = holder.itemView.getContext(); Event event = DatabaseManager.toEvent(cursor, holder.event); holder.event = event; @@ -60,12 +122,16 @@ public class BookmarksAdapter extends EventsAdapter { holder.details.setText(details); } holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsContentDescription)); + + // Enable MultiChoice selection and update checked state + holder.bind(multiChoiceHelper, position); } /** * Checks if the current event is overlapping with the previous or next one. + * Warning: this methods will update the cursor's position. */ - public static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) { + private static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) { final int position = cursor.getPosition(); if ((startTime != null) && (position > 0) && cursor.moveToPosition(position - 1)) { @@ -86,4 +152,14 @@ public class BookmarksAdapter extends EventsAdapter { return false; } + + static class RemoveBookmarksAsyncTask extends AsyncTask { + + @Override + protected Void doInBackground(long[]... params) { + DatabaseManager.getInstance().removeBookmarks(params[0]); + return null; + } + + } } diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.java b/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.java index 2871e4d..4d848e4 100644 --- a/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.java +++ b/app/src/main/java/be/digitalia/fosdem/adapters/EventsAdapter.java @@ -1,9 +1,9 @@ package be.digitalia.fosdem.adapters; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.graphics.drawable.Drawable; -import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.TextViewCompat; import android.support.v7.widget.AppCompatDrawableManager; import android.text.TextUtils; @@ -16,11 +16,13 @@ import java.text.DateFormat; import java.util.Date; import be.digitalia.fosdem.R; +import be.digitalia.fosdem.activities.EventDetailsActivity; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.utils.DateUtils; +import be.digitalia.fosdem.widgets.MultiChoiceHelper; -public class EventsAdapter extends CursorAdapter { +public class EventsAdapter extends RecyclerViewCursorAdapter { protected final LayoutInflater inflater; protected final DateFormat timeDateFormat; @@ -31,7 +33,6 @@ public class EventsAdapter extends CursorAdapter { } public EventsAdapter(Context context, boolean showDay) { - super(context, null, 0); inflater = LayoutInflater.from(context); timeDateFormat = DateUtils.getTimeDateFormat(context); this.showDay = showDay; @@ -43,22 +44,19 @@ public class EventsAdapter extends CursorAdapter { } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = inflater.inflate(R.layout.item_event, parent, false); - - ViewHolder holder = new ViewHolder(); - holder.title = (TextView) view.findViewById(R.id.title); - holder.persons = (TextView) view.findViewById(R.id.persons); - holder.trackName = (TextView) view.findViewById(R.id.track_name); - holder.details = (TextView) view.findViewById(R.id.details); - view.setTag(holder); - - return view; + public int getItemViewType(int position) { + return R.layout.item_event; } @Override - public void bindView(View view, Context context, Cursor cursor) { - ViewHolder holder = (ViewHolder) view.getTag(); + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = inflater.inflate(R.layout.item_event, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, Cursor cursor) { + Context context = holder.itemView.getContext(); Event event = DatabaseManager.toEvent(cursor, holder.event); holder.event = event; @@ -92,11 +90,29 @@ public class EventsAdapter extends CursorAdapter { holder.details.setContentDescription(context.getString(R.string.details_content_description, details)); } - protected static class ViewHolder { + static class ViewHolder extends MultiChoiceHelper.ViewHolder implements View.OnClickListener { TextView title; TextView persons; TextView trackName; TextView details; + Event event; + + public ViewHolder(View itemView) { + super(itemView); + title = (TextView) itemView.findViewById(R.id.title); + persons = (TextView) itemView.findViewById(R.id.persons); + trackName = (TextView) itemView.findViewById(R.id.track_name); + details = (TextView) itemView.findViewById(R.id.details); + setOnClickListener(this); + } + + @Override + public void onClick(View view) { + Context context = view.getContext(); + Intent intent = new Intent(context, EventDetailsActivity.class) + .putExtra(EventDetailsActivity.EXTRA_EVENT, event); + context.startActivity(intent); + } } } diff --git a/app/src/main/java/be/digitalia/fosdem/adapters/RecyclerViewCursorAdapter.java b/app/src/main/java/be/digitalia/fosdem/adapters/RecyclerViewCursorAdapter.java new file mode 100644 index 0000000..49c1fb6 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/adapters/RecyclerViewCursorAdapter.java @@ -0,0 +1,78 @@ +package be.digitalia.fosdem.adapters; + +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; + +/** + * Simplified CursorAdapter designed for RecyclerView. + * + * @author Christophe Beyls + */ +public abstract class RecyclerViewCursorAdapter extends RecyclerView.Adapter { + + private Cursor cursor; + private int rowIDColumn = -1; + + public RecyclerViewCursorAdapter() { + setHasStableIds(true); + } + + /** + * Swap in a new Cursor, returning the old Cursor. + * The old cursor is not closed. + * + * @return The previously set Cursor, if any. + * If the given new Cursor is the same instance as the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == cursor) { + return null; + } + Cursor oldCursor = cursor; + cursor = newCursor; + rowIDColumn = (newCursor == null) ? -1 : newCursor.getColumnIndexOrThrow("_id"); + notifyDataSetChanged(); + return oldCursor; + } + + public Cursor getCursor() { + return cursor; + } + + @Override + public int getItemCount() { + return (cursor == null) ? 0 : cursor.getCount(); + } + + /** + * @return The cursor initialized to the specified position. + */ + public Object getItem(int position) { + if (cursor != null) { + cursor.moveToPosition(position); + } + return cursor; + } + + @Override + public long getItemId(int position) { + if ((cursor != null) && cursor.moveToPosition(position)) { + return cursor.getLong(rowIDColumn); + } + return RecyclerView.NO_ID; + } + + @Override + public void onBindViewHolder(VH holder, int position) { + if (cursor == null) { + throw new IllegalStateException("this should only be called when the cursor is not null"); + } + if (!cursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + onBindViewHolder(holder, cursor); + } + + public abstract void onBindViewHolder(VH holder, Cursor cursor); +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/BaseLiveListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/BaseLiveListFragment.java index 729155c..e585f31 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/BaseLiveListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/BaseLiveListFragment.java @@ -1,17 +1,16 @@ package be.digitalia.fosdem.fragments; -import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.view.View; -import android.widget.ListView; -import be.digitalia.fosdem.activities.EventDetailsActivity; -import be.digitalia.fosdem.adapters.EventsAdapter; -import be.digitalia.fosdem.model.Event; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; -public abstract class BaseLiveListFragment extends SmoothListFragment implements LoaderCallbacks { +import be.digitalia.fosdem.adapters.EventsAdapter; + +public abstract class BaseLiveListFragment extends RecyclerViewFragment implements LoaderCallbacks { private static final int EVENTS_LOADER_ID = 1; @@ -21,7 +20,13 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); adapter = new EventsAdapter(getActivity(), false); - setListAdapter(adapter); + } + + @Override + protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) { + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL)); + recyclerView.setAdapter(adapter); } @Override @@ -29,7 +34,7 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements super.onActivityCreated(savedInstanceState); setEmptyText(getEmptyText()); - setListShown(false); + setProgressBarVisible(true); getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this); } @@ -42,18 +47,11 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements adapter.swapCursor(data); } - setListShown(true); + setProgressBarVisible(false); } @Override public void onLoaderReset(Loader loader) { adapter.swapCursor(null); } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Event event = adapter.getItem(position); - Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event); - startActivity(intent); - } } diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/BookmarksListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/BookmarksListFragment.java index b53d4ce..fd3e84c 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/BookmarksListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/BookmarksListFragment.java @@ -1,40 +1,38 @@ package be.digitalia.fosdem.fragments; import android.content.Context; -import android.content.Intent; import android.database.Cursor; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.content.SharedPreferencesCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.format.DateUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; import be.digitalia.fosdem.R; -import be.digitalia.fosdem.activities.EventDetailsActivity; import be.digitalia.fosdem.adapters.BookmarksAdapter; -import be.digitalia.fosdem.adapters.EventsAdapter; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.loaders.SimpleCursorLoader; -import be.digitalia.fosdem.model.Event; -import be.digitalia.fosdem.widgets.BookmarksMultiChoiceModeListener; /** * Bookmarks list, optionally filterable. * * @author Christophe Beyls */ -public class BookmarksListFragment extends SmoothListFragment implements LoaderCallbacks { +public class BookmarksListFragment extends RecyclerViewFragment implements LoaderCallbacks { private static final int BOOKMARKS_LOADER_ID = 1; private static final String PREF_UPCOMING_ONLY = "bookmarks_upcoming_only"; + private static final String STATE_ADAPTER = "adapter"; - private EventsAdapter adapter; + private BookmarksAdapter adapter; private boolean upcomingOnly; private MenuItem filterMenuItem; @@ -44,33 +42,41 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adapter = new BookmarksAdapter(getActivity()); - setListAdapter(adapter); - + adapter = new BookmarksAdapter((AppCompatActivity) getActivity()); + if (savedInstanceState != null) { + adapter.onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER)); + } upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false); setHasOptionsMenu(true); } + @Override + protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) { + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL)); + recyclerView.setAdapter(adapter); + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - BookmarksMultiChoiceModeListener.register(getListView()); - } - setEmptyText(getString(R.string.no_bookmark)); - setListShown(false); + setProgressBarVisible(true); getLoaderManager().initLoader(BOOKMARKS_LOADER_ID, null, this); } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_ADAPTER, adapter.onSaveInstanceState()); + } + @Override public void onDestroyView() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - BookmarksMultiChoiceModeListener.unregister(getListView()); - } + adapter.onDestroyView(); super.onDestroyView(); } @@ -116,7 +122,7 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC private static class BookmarksLoader extends SimpleCursorLoader { // Events that just started are still shown for 5 minutes - private static final long TIME_OFFSET = 5L * 60L * 1000L; + private static final long TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS; private final boolean upcomingOnly; private final Handler handler; @@ -179,18 +185,11 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC adapter.swapCursor(data); } - setListShown(true); + setProgressBarVisible(false); } @Override public void onLoaderReset(Loader loader) { adapter.swapCursor(null); } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Event event = adapter.getItem(position); - Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event); - startActivity(intent); - } } diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/PersonInfoListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/PersonInfoListFragment.java index 6c6bd65..1a6d9ac 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/PersonInfoListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/PersonInfoListFragment.java @@ -7,22 +7,23 @@ import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v7.widget.ConcatAdapter; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.ListView; +import android.view.ViewGroup; import be.digitalia.fosdem.R; -import be.digitalia.fosdem.activities.EventDetailsActivity; import be.digitalia.fosdem.adapters.EventsAdapter; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.loaders.SimpleCursorLoader; -import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.model.Person; -public class PersonInfoListFragment extends SmoothListFragment implements LoaderCallbacks { +public class PersonInfoListFragment extends RecyclerViewFragment implements LoaderCallbacks { private static final int PERSON_EVENTS_LOADER_ID = 1; private static final String ARG_PERSON = "person"; @@ -63,23 +64,23 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader return false; } + @Override + protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) { + final int contentMargin = getResources().getDimensionPixelSize(R.dimen.content_margin); + recyclerView.setPadding(contentMargin, contentMargin, contentMargin, contentMargin); + recyclerView.setClipToPadding(false); + recyclerView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.setAdapter(new ConcatAdapter(new HeaderAdapter(), adapter)); + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setEmptyText(getString(R.string.no_data)); - - int contentMargin = getResources().getDimensionPixelSize(R.dimen.content_margin); - ListView listView = getListView(); - listView.setPadding(contentMargin, contentMargin, contentMargin, contentMargin); - listView.setClipToPadding(false); - listView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); - - View headerView = LayoutInflater.from(getActivity()).inflate(R.layout.header_person_info, null); - getListView().addHeaderView(headerView, null, false); - - setListAdapter(adapter); - setListShown(false); + setProgressBarVisible(true); getLoaderManager().initLoader(PERSON_EVENTS_LOADER_ID, null, this); } @@ -110,7 +111,7 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader adapter.swapCursor(data); } - setListShown(true); + setProgressBarVisible(false); } @Override @@ -118,10 +119,34 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader adapter.swapCursor(null); } - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Event event = adapter.getItem(position - 1); - Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event); - startActivity(intent); + static class HeaderAdapter extends RecyclerView.Adapter { + + @Override + public int getItemCount() { + return 1; + } + + @Override + public int getItemViewType(int position) { + return R.layout.header_person_info; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.header_person_info, null); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + // Nothing to bind + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View itemView) { + super(itemView); + } + } } } diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/RecycledViewPoolProvider.java b/app/src/main/java/be/digitalia/fosdem/fragments/RecycledViewPoolProvider.java new file mode 100644 index 0000000..bca0885 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/fragments/RecycledViewPoolProvider.java @@ -0,0 +1,10 @@ +package be.digitalia.fosdem.fragments; + +import android.support.v7.widget.RecyclerView; + +/** + * Components implementing this interface allow to share a RecycledViewPool between similar fragments. + */ +public interface RecycledViewPoolProvider { + RecyclerView.RecycledViewPool getRecycledViewPool(); +} diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/RecyclerViewFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/RecyclerViewFragment.java new file mode 100644 index 0000000..e77332d --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/fragments/RecyclerViewFragment.java @@ -0,0 +1,191 @@ +package be.digitalia.fosdem.fragments; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import be.digitalia.fosdem.R; +import be.digitalia.fosdem.widgets.ContentLoadingProgressBar; + +/** + * Fragment providing a RecyclerView, an empty view and a progress bar. + * + * @author Christophe Beyls + */ +public class RecyclerViewFragment extends Fragment { + + private static final int DEFAULT_EMPTY_VIEW_PADDING_DIPS = 16; + + static class ViewHolder { + FrameLayout container; + RecyclerView recyclerView; + View emptyView; + ContentLoadingProgressBar progress; + } + + private class EmptyViewAwareRecyclerView extends RecyclerView { + + private final AdapterDataObserver mEmptyObserver = new AdapterDataObserver() { + @Override + public void onChanged() { + updateEmptyViewVisibility(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + updateEmptyViewVisibility(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + updateEmptyViewVisibility(); + } + }; + + public EmptyViewAwareRecyclerView(Context context) { + super(context); + } + + public EmptyViewAwareRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public EmptyViewAwareRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setAdapter(Adapter adapter) { + final Adapter oldAdapter = getAdapter(); + if (oldAdapter != null) { + oldAdapter.unregisterAdapterDataObserver(mEmptyObserver); + } + super.setAdapter(adapter); + if (adapter != null) { + adapter.registerAdapterDataObserver(mEmptyObserver); + } + + updateEmptyViewVisibility(); + } + } + + private ViewHolder mHolder; + private boolean mIsProgressBarVisible; + + /** + * Override this method to provide a custom Empty View. + * The default one is a TextView with some padding. + */ + @NonNull + protected View onCreateEmptyView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) { + TextView textView = new TextView(inflater.getContext()); + textView.setGravity(Gravity.CENTER); + int textPadding = (int) (getResources().getDisplayMetrics().density * DEFAULT_EMPTY_VIEW_PADDING_DIPS); + textView.setPadding(textPadding, textPadding, textPadding, textPadding); + return textView; + } + + /** + * Override this method to setup the RecyclerView (LayoutManager, ItemDecoration, Adapter) + */ + protected void onRecyclerViewCreated(RecyclerView recyclerView, @Nullable Bundle savedInstanceState) { + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + + mHolder = new ViewHolder(); + + mHolder.container = new FrameLayout(context); + + mHolder.recyclerView = new EmptyViewAwareRecyclerView(context, null, R.attr.recyclerViewStyle); + mHolder.recyclerView.setId(android.R.id.list); + mHolder.recyclerView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + mHolder.recyclerView.setHasFixedSize(true); + mHolder.container.addView(mHolder.recyclerView, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + mHolder.emptyView = onCreateEmptyView(inflater, mHolder.container, savedInstanceState); + mHolder.emptyView.setId(android.R.id.empty); + mHolder.emptyView.setVisibility(View.GONE); + mHolder.container.addView(mHolder.emptyView, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + mHolder.progress = new ContentLoadingProgressBar(context, null, android.R.attr.progressBarStyleLarge); + mHolder.progress.setId(android.R.id.progress); + mHolder.progress.hide(); + mHolder.container.addView(mHolder.progress, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + + mHolder.container.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + onRecyclerViewCreated(mHolder.recyclerView, savedInstanceState); + + return mHolder.container; + } + + @Override + public void onDestroyView() { + // Ensure the RecyclerView is properly unregistered as an observer of the adapter + mHolder.recyclerView.setAdapter(null); + mHolder = null; + mIsProgressBarVisible = false; + super.onDestroyView(); + } + + /** + * Get the fragments's RecyclerView widget. + */ + public RecyclerView getRecyclerView() { + return mHolder.recyclerView; + } + + /** + * The default content for a RecyclerViewFragment has a TextView that can be shown when the list is empty. + * Call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ((TextView) mHolder.emptyView).setText(text); + } + + void updateEmptyViewVisibility() { + if (!mIsProgressBarVisible) { + RecyclerView.Adapter adapter = mHolder.recyclerView.getAdapter(); + final boolean isEmptyViewVisible = (adapter != null) && (adapter.getItemCount() == 0); + mHolder.recyclerView.setVisibility(isEmptyViewVisible ? View.GONE : View.VISIBLE); + mHolder.emptyView.setVisibility(isEmptyViewVisible ? View.VISIBLE : View.GONE); + } + } + + /** + * Call this method to show or hide the indeterminate progress bar. + * When shown, the RecyclerView will be hidden. + * + * @param visible true to show the progress bar, false to hide it. The initial value is false. + */ + public void setProgressBarVisible(boolean visible) { + if (mIsProgressBarVisible != visible) { + mIsProgressBarVisible = visible; + + if (visible) { + mHolder.recyclerView.setVisibility(View.GONE); + mHolder.emptyView.setVisibility(View.GONE); + mHolder.progress.show(); + } else { + updateEmptyViewVisibility(); + mHolder.progress.hide(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/SearchResultListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/SearchResultListFragment.java index 15dd850..49c5483 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/SearchResultListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/SearchResultListFragment.java @@ -1,21 +1,20 @@ package be.digitalia.fosdem.fragments; import android.content.Context; -import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.view.View; -import android.widget.ListView; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + import be.digitalia.fosdem.R; -import be.digitalia.fosdem.activities.EventDetailsActivity; import be.digitalia.fosdem.adapters.EventsAdapter; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.loaders.SimpleCursorLoader; -import be.digitalia.fosdem.model.Event; -public class SearchResultListFragment extends SmoothListFragment implements LoaderCallbacks { +public class SearchResultListFragment extends RecyclerViewFragment implements LoaderCallbacks { private static final int EVENTS_LOADER_ID = 1; private static final String ARG_QUERY = "query"; @@ -34,7 +33,13 @@ public class SearchResultListFragment extends SmoothListFragment implements Load public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); adapter = new EventsAdapter(getActivity()); - setListAdapter(adapter); + } + + @Override + protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) { + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL)); + recyclerView.setAdapter(adapter); } @Override @@ -42,7 +47,7 @@ public class SearchResultListFragment extends SmoothListFragment implements Load super.onActivityCreated(savedInstanceState); setEmptyText(getString(R.string.no_search_result)); - setListShown(false); + setProgressBarVisible(true); getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this); } @@ -74,18 +79,11 @@ public class SearchResultListFragment extends SmoothListFragment implements Load adapter.swapCursor(data); } - setListShown(true); + setProgressBarVisible(false); } @Override public void onLoaderReset(Loader loader) { adapter.swapCursor(null); } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Event event = adapter.getItem(position); - Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event); - startActivity(intent); - } } diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java index c15feed..4e0c12d 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/SmoothListFragment.java @@ -59,7 +59,7 @@ public class SmoothListFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - final Context context = getActivity(); + final Context context = inflater.getContext(); mHolder = new ViewHolder(); @@ -138,7 +138,7 @@ public class SmoothListFragment extends Fragment { } /** - * The default content for a SwipeRefreshListFragment has a TextView that can be shown when the list is empty. + * The default content for a SmoothListFragment has a TextView that can be shown when the list is empty. * Call this method to supply the text it should use. */ public void setEmptyText(CharSequence text) { diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java index 124fd42..f9f41cf 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java @@ -14,20 +14,20 @@ import android.support.v4.content.Loader; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.SharedPreferencesCompat; import android.support.v4.view.ViewPager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import be.digitalia.fosdem.widgets.SlidingTabLayout; - import java.util.List; import be.digitalia.fosdem.R; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.loaders.GlobalCacheLoader; import be.digitalia.fosdem.model.Day; +import be.digitalia.fosdem.widgets.SlidingTabLayout; -public class TracksFragment extends Fragment implements LoaderCallbacks> { +public class TracksFragment extends Fragment implements RecycledViewPoolProvider, LoaderCallbacks> { static class ViewHolder { View contentView; @@ -35,6 +35,7 @@ public class TracksFragment extends Fragment implements LoaderCallbacks> { private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() { diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/TracksListFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/TracksListFragment.java index 58be8b7..b315dd3 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/TracksListFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/TracksListFragment.java @@ -4,28 +4,31 @@ import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; +import android.support.v7.widget.DividerItemDecoration; +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.ListView; import android.widget.TextView; import be.digitalia.fosdem.R; import be.digitalia.fosdem.activities.TrackScheduleActivity; +import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.loaders.SimpleCursorLoader; import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.model.Track; -public class TracksListFragment extends SmoothListFragment implements LoaderCallbacks { +public class TracksListFragment extends RecyclerViewFragment implements LoaderCallbacks { private static final int TRACKS_LOADER_ID = 1; private static final String ARG_DAY = "day"; - private Day day; + Day day; private TracksAdapter adapter; public static TracksListFragment newInstance(Day day) { @@ -39,9 +42,20 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - adapter = new TracksAdapter(getActivity()); + adapter = new TracksAdapter(); day = getArguments().getParcelable(ARG_DAY); - setListAdapter(adapter); + } + + @Override + protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) { + Fragment parentFragment = getParentFragment(); + if (parentFragment instanceof RecycledViewPoolProvider) { + recyclerView.setRecycledViewPool(((RecycledViewPoolProvider) parentFragment).getRecycledViewPool()); + } + + recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext())); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL)); + recyclerView.setAdapter(adapter); } @Override @@ -49,7 +63,7 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall super.onActivityCreated(savedInstanceState); setEmptyText(getString(R.string.no_data)); - setListShown(false); + setProgressBarVisible(true); getLoaderManager().initLoader(TRACKS_LOADER_ID, null, this); } @@ -80,7 +94,7 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall adapter.swapCursor(data); } - setListShown(true); + setProgressBarVisible(false); } @Override @@ -88,21 +102,12 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall adapter.swapCursor(null); } - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Track track = adapter.getItem(position); - Intent intent = new Intent(getActivity(), TrackScheduleActivity.class).putExtra(TrackScheduleActivity.EXTRA_DAY, day).putExtra( - TrackScheduleActivity.EXTRA_TRACK, track); - startActivity(intent); - } - - private static class TracksAdapter extends CursorAdapter { + private class TracksAdapter extends RecyclerViewCursorAdapter { private final LayoutInflater inflater; - public TracksAdapter(Context context) { - super(context, null, 0); - inflater = LayoutInflater.from(context); + public TracksAdapter() { + inflater = LayoutInflater.from(getContext()); } @Override @@ -111,29 +116,41 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { + public TrackViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = inflater.inflate(R.layout.simple_list_item_2_material, parent, false); - - ViewHolder holder = new ViewHolder(); - holder.name = (TextView) view.findViewById(android.R.id.text1); - holder.type = (TextView) view.findViewById(android.R.id.text2); - view.setTag(holder); - - return view; + return new TrackViewHolder(view); } @Override - public void bindView(View view, Context context, Cursor cursor) { - ViewHolder holder = (ViewHolder) view.getTag(); + public void onBindViewHolder(TrackViewHolder holder, Cursor cursor) { + holder.day = day; holder.track = DatabaseManager.toTrack(cursor, holder.track); holder.name.setText(holder.track.getName()); holder.type.setText(holder.track.getType().getNameResId()); } + } - static class ViewHolder { - TextView name; - TextView type; - Track track; + static class TrackViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + TextView name; + TextView type; + + Day day; + Track track; + + TrackViewHolder(View itemView) { + super(itemView); + name = (TextView) itemView.findViewById(android.R.id.text1); + type = (TextView) itemView.findViewById(android.R.id.text2); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + Context context = view.getContext(); + Intent intent = new Intent(context, TrackScheduleActivity.class) + .putExtra(TrackScheduleActivity.EXTRA_DAY, day) + .putExtra(TrackScheduleActivity.EXTRA_TRACK, track); + context.startActivity(intent); } } } diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/BookmarksMultiChoiceModeListener.java b/app/src/main/java/be/digitalia/fosdem/widgets/BookmarksMultiChoiceModeListener.java deleted file mode 100644 index 4ca8717..0000000 --- a/app/src/main/java/be/digitalia/fosdem/widgets/BookmarksMultiChoiceModeListener.java +++ /dev/null @@ -1,87 +0,0 @@ -package be.digitalia.fosdem.widgets; - -import android.annotation.TargetApi; -import android.os.AsyncTask; -import android.os.Build; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.AbsListView; -import android.widget.AbsListView.MultiChoiceModeListener; - -import be.digitalia.fosdem.R; -import be.digitalia.fosdem.db.DatabaseManager; - -/** - * Context menu for the bookmarks list items, available for API 11+ only. - * - * @author Christophe Beyls - */ -@TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class BookmarksMultiChoiceModeListener implements MultiChoiceModeListener { - - private AbsListView listView; - - public static void register(AbsListView listView) { - listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); - BookmarksMultiChoiceModeListener listener = new BookmarksMultiChoiceModeListener(listView); - listView.setMultiChoiceModeListener(listener); - } - - public static void unregister(AbsListView listView) { - // Will close the ActionMode if open - listView.setChoiceMode(AbsListView.CHOICE_MODE_NONE); - } - - private BookmarksMultiChoiceModeListener(AbsListView listView) { - this.listView = listView; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu); - return true; - } - - private void updateSelectedCountDisplay(ActionMode mode) { - int count = listView.getCheckedItemCount(); - mode.setTitle(listView.getContext().getResources().getQuantityString(R.plurals.selected, count, count)); - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - updateSelectedCountDisplay(mode); - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.delete: - // Remove multiple bookmarks at once - new RemoveBookmarksAsyncTask().execute(listView.getCheckedItemIds()); - mode.finish(); - return true; - } - return false; - } - - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { - updateSelectedCountDisplay(mode); - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - } - - static class RemoveBookmarksAsyncTask extends AsyncTask { - - @Override - protected Void doInBackground(long[]... params) { - DatabaseManager.getInstance().removeBookmarks(params[0]); - return null; - } - - } -} diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/CheckableLinearLayout.java b/app/src/main/java/be/digitalia/fosdem/widgets/CheckableLinearLayout.java new file mode 100644 index 0000000..eb6056d --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/widgets/CheckableLinearLayout.java @@ -0,0 +1,43 @@ +package be.digitalia.fosdem.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; + +/** + * {@link android.widget.LinearLayout} implementing the {@link android.widget.Checkable} + * interface by keeping an internal 'checked' state flag. + */ +public class CheckableLinearLayout extends ForegroundLinearLayout implements Checkable { + private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; + + private boolean mChecked = false; + + public CheckableLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public boolean isChecked() { + return mChecked; + } + + public void setChecked(boolean b) { + if (b != mChecked) { + mChecked = b; + refreshDrawableState(); + } + } + + public void toggle() { + setChecked(!mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} \ No newline at end of file diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/ForegroundLinearLayout.java b/app/src/main/java/be/digitalia/fosdem/widgets/ForegroundLinearLayout.java new file mode 100644 index 0000000..f247d5e --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/widgets/ForegroundLinearLayout.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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. + */ + +package be.digitalia.fosdem.widgets; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutCompat; +import android.util.AttributeSet; +import android.view.Gravity; + +import be.digitalia.fosdem.R; + +public class ForegroundLinearLayout extends LinearLayoutCompat { + + private Drawable mForeground; + + private final Rect mSelfBounds = new Rect(); + + private final Rect mOverlayBounds = new Rect(); + + private int mForegroundGravity = Gravity.FILL; + + protected boolean mForegroundInPadding = true; + + boolean mForegroundBoundsChanged = false; + + public ForegroundLinearLayout(Context context) { + this(context, null); + } + + public ForegroundLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout, + defStyle, 0); + + mForegroundGravity = a.getInt( + R.styleable.ForegroundLinearLayout_android_foregroundGravity, mForegroundGravity); + + final Drawable d = a.getDrawable(R.styleable.ForegroundLinearLayout_android_foreground); + if (d != null) { + setForeground(d); + } + + mForegroundInPadding = a.getBoolean( + R.styleable.ForegroundLinearLayout_foregroundInsidePadding, true); + + a.recycle(); + } + + /** + * Describes how the foreground is positioned. + * + * @return foreground gravity. + * @see #setForegroundGravity(int) + */ + public int getForegroundGravity() { + return mForegroundGravity; + } + + /** + * Describes how the foreground is positioned. Defaults to START and TOP. + * + * @param foregroundGravity See {@link Gravity} + * @see #getForegroundGravity() + */ + public void setForegroundGravity(int foregroundGravity) { + if (mForegroundGravity != foregroundGravity) { + if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.START; + } + + if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.TOP; + } + + mForegroundGravity = foregroundGravity; + + if (mForegroundGravity == Gravity.FILL && mForeground != null) { + Rect padding = new Rect(); + mForeground.getPadding(padding); + } + + requestLayout(); + } + } + + @Override + protected boolean verifyDrawable(@NonNull Drawable who) { + return super.verifyDrawable(who) || (who == mForeground); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mForeground != null) { + mForeground.jumpToCurrentState(); + } + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mForeground != null && mForeground.isStateful()) { + mForeground.setState(getDrawableState()); + } + } + + /** + * Supply a Drawable that is to be rendered on top of all of the child + * views in the frame layout. Any padding in the Drawable will be taken + * into account by ensuring that the children are inset to be placed + * inside of the padding area. + * + * @param drawable The Drawable to be drawn on top of the children. + */ + public void setForeground(Drawable drawable) { + if (mForeground != drawable) { + if (mForeground != null) { + mForeground.setCallback(null); + unscheduleDrawable(mForeground); + } + + mForeground = drawable; + + if (drawable != null) { + setWillNotDraw(false); + drawable.setCallback(this); + if (drawable.isStateful()) { + drawable.setState(getDrawableState()); + } + if (mForegroundGravity == Gravity.FILL) { + Rect padding = new Rect(); + drawable.getPadding(padding); + } + } else { + setWillNotDraw(true); + } + requestLayout(); + invalidate(); + } + } + + /** + * Returns the drawable used as the foreground of this FrameLayout. The + * foreground drawable, if non-null, is always drawn on top of the children. + * + * @return A Drawable or null if no foreground was set. + */ + public Drawable getForeground() { + return mForeground; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mForegroundBoundsChanged |= changed; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mForegroundBoundsChanged = true; + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + final Drawable foreground = mForeground; + + if (mForegroundBoundsChanged) { + mForegroundBoundsChanged = false; + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + final int w = getRight() - getLeft(); + final int h = getBottom() - getTop(); + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(getPaddingLeft(), getPaddingTop(), + w - getPaddingRight(), h - getPaddingBottom()); + } + + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + foreground.setBounds(overlayBounds); + } + + foreground.draw(canvas); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void drawableHotspotChanged(float x, float y) { + super.drawableHotspotChanged(x, y); + if (mForeground != null) { + mForeground.setHotspot(x, y); + } + } + +} diff --git a/app/src/main/java/be/digitalia/fosdem/widgets/MultiChoiceHelper.java b/app/src/main/java/be/digitalia/fosdem/widgets/MultiChoiceHelper.java new file mode 100644 index 0000000..448a955 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/widgets/MultiChoiceHelper.java @@ -0,0 +1,483 @@ +package be.digitalia.fosdem.widgets; + +import android.content.Context; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; +import android.support.v7.widget.RecyclerView; +import android.util.SparseBooleanArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Checkable; + +/** + * Helper class to reproduce ListView's modal MultiChoice mode with a RecyclerView. + * Compatible with API 7+. + * Declare and use this class from inside your Adapter. + * + * @author Christophe Beyls + */ +public class MultiChoiceHelper { + + /** + * A handy ViewHolder base class which works with the MultiChoiceHelper + * and reproduces the default behavior of a ListView. + */ + public static abstract class ViewHolder extends RecyclerView.ViewHolder { + + View.OnClickListener clickListener; + MultiChoiceHelper multiChoiceHelper; + + public ViewHolder(View itemView) { + super(itemView); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (isMultiChoiceActive()) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + multiChoiceHelper.toggleItemChecked(position, false); + updateCheckedState(position); + } + } else { + if (clickListener != null) { + clickListener.onClick(view); + } + } + } + }); + itemView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if ((multiChoiceHelper == null) || isMultiChoiceActive()) { + return false; + } + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + multiChoiceHelper.setItemChecked(position, true, false); + updateCheckedState(position); + } + return true; + } + }); + } + + void updateCheckedState(int position) { + final boolean isChecked = multiChoiceHelper.isItemChecked(position); + if (itemView instanceof Checkable) { + ((Checkable) itemView).setChecked(isChecked); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + itemView.setActivated(isChecked); + } + } + + public void setOnClickListener(View.OnClickListener clickListener) { + this.clickListener = clickListener; + } + + public void bind(MultiChoiceHelper multiChoiceHelper, int position) { + this.multiChoiceHelper = multiChoiceHelper; + if (multiChoiceHelper != null) { + updateCheckedState(position); + } + } + + public boolean isMultiChoiceActive() { + return (multiChoiceHelper != null) && (multiChoiceHelper.getCheckedItemCount() > 0); + } + } + + public interface MultiChoiceModeListener extends ActionMode.Callback { + /** + * Called when an item is checked or unchecked during selection mode. + * + * @param mode The {@link ActionMode} providing the selection startSupportActionModemode + * @param position Adapter position of the item that was checked or unchecked + * @param id Adapter ID of the item that was checked or unchecked + * @param checked true if the item is now checked, false + * if the item is now unchecked. + */ + void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked); + } + + private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; + + private final AppCompatActivity activity; + private final RecyclerView.Adapter adapter; + private SparseBooleanArray checkStates; + private LongSparseArray checkedIdStates; + private int checkedItemCount = 0; + private MultiChoiceModeWrapper multiChoiceModeCallback; + ActionMode choiceActionMode; + + /** + * Make sure this constructor is called before setting the adapter on the RecyclerView + * so this class will be notified before the RecyclerView in case of data set changes. + */ + public MultiChoiceHelper(@NonNull AppCompatActivity activity, @NonNull RecyclerView.Adapter adapter) { + this.activity = activity; + this.adapter = adapter; + adapter.registerAdapterDataObserver(new AdapterDataSetObserver()); + checkStates = new SparseBooleanArray(0); + if (adapter.hasStableIds()) { + checkedIdStates = new LongSparseArray<>(0); + } + } + + public Context getContext() { + return activity; + } + + public void setMultiChoiceModeListener(MultiChoiceModeListener listener) { + if (listener == null) { + multiChoiceModeCallback = null; + return; + } + if (multiChoiceModeCallback == null) { + multiChoiceModeCallback = new MultiChoiceModeWrapper(); + } + multiChoiceModeCallback.setWrapped(listener); + } + + public int getCheckedItemCount() { + return checkedItemCount; + } + + public boolean isItemChecked(int position) { + return checkStates.get(position); + } + + public SparseBooleanArray getCheckedItemPositions() { + return checkStates; + } + + public long[] getCheckedItemIds() { + final LongSparseArray idStates = checkedIdStates; + if (idStates == null) { + return new long[0]; + } + + final int count = idStates.size(); + final long[] ids = new long[count]; + + for (int i = 0; i < count; i++) { + ids[i] = idStates.keyAt(i); + } + + return ids; + } + + public void clearChoices() { + if (checkedItemCount > 0) { + final int start = checkStates.keyAt(0); + final int end = checkStates.keyAt(checkStates.size() - 1); + checkStates.clear(); + if (checkedIdStates != null) { + checkedIdStates.clear(); + } + checkedItemCount = 0; + + adapter.notifyItemRangeChanged(start, end - start + 1); + + if (choiceActionMode != null) { + choiceActionMode.finish(); + } + } + } + + public void setItemChecked(int position, boolean value, boolean notifyChanged) { + // Start selection mode if needed. We don't need to if we're unchecking something. + if (value) { + startSupportActionModeIfNeeded(); + } + + boolean oldValue = checkStates.get(position); + checkStates.put(position, value); + + if (oldValue != value) { + final long id = adapter.getItemId(position); + + if (checkedIdStates != null) { + if (value) { + checkedIdStates.put(id, position); + } else { + checkedIdStates.delete(id); + } + } + + if (value) { + checkedItemCount++; + } else { + checkedItemCount--; + } + + if (notifyChanged) { + adapter.notifyItemChanged(position); + } + + if (choiceActionMode != null) { + multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, position, id, value); + if (checkedItemCount == 0) { + choiceActionMode.finish(); + } + } + } + } + + public void toggleItemChecked(int position, boolean notifyChanged) { + setItemChecked(position, !isItemChecked(position), notifyChanged); + } + + public Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(); + savedState.checkedItemCount = checkedItemCount; + savedState.checkStates = clone(checkStates); + if (checkedIdStates != null) { + savedState.checkedIdStates = checkedIdStates.clone(); + } + return savedState; + } + + private static SparseBooleanArray clone(SparseBooleanArray original) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return original.clone(); + } + final int size = original.size(); + SparseBooleanArray clone = new SparseBooleanArray(size); + for (int i = 0; i < size; ++i) { + clone.append(original.keyAt(i), original.valueAt(i)); + } + return clone; + } + + public void onRestoreInstanceState(Parcelable state) { + if ((state != null) && (checkedItemCount == 0)) { + SavedState savedState = (SavedState) state; + checkedItemCount = savedState.checkedItemCount; + checkStates = savedState.checkStates; + checkedIdStates = savedState.checkedIdStates; + + if (checkedItemCount > 0) { + // Empty adapter is given a chance to be populated before completeRestoreInstanceState() + if (adapter.getItemCount() > 0) { + confirmCheckedPositions(); + } + activity.getWindow().getDecorView().post(new Runnable() { + @Override + public void run() { + completeRestoreInstanceState(); + } + }); + } + } + } + + void completeRestoreInstanceState() { + if (checkedItemCount > 0) { + if (adapter.getItemCount() == 0) { + // Adapter was not populated, clear the selection + confirmCheckedPositions(); + } else { + startSupportActionModeIfNeeded(); + } + } + } + + private void startSupportActionModeIfNeeded() { + if (choiceActionMode == null) { + if (multiChoiceModeCallback == null) { + throw new IllegalStateException("No callback set"); + } + choiceActionMode = activity.startSupportActionMode(multiChoiceModeCallback); + } + } + + public static class SavedState implements Parcelable { + + int checkedItemCount; + SparseBooleanArray checkStates; + LongSparseArray checkedIdStates; + + SavedState() { + } + + SavedState(Parcel in) { + checkedItemCount = in.readInt(); + checkStates = in.readSparseBooleanArray(); + final int n = in.readInt(); + if (n >= 0) { + checkedIdStates = new LongSparseArray<>(n); + for (int i = 0; i < n; i++) { + final long key = in.readLong(); + final int value = in.readInt(); + checkedIdStates.append(key, value); + } + } + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(checkedItemCount); + out.writeSparseBooleanArray(checkStates); + final int n = checkedIdStates != null ? checkedIdStates.size() : -1; + out.writeInt(n); + for (int i = 0; i < n; i++) { + out.writeLong(checkedIdStates.keyAt(i)); + out.writeInt(checkedIdStates.valueAt(i)); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + void confirmCheckedPositions() { + if (checkedItemCount == 0) { + return; + } + + final int itemCount = adapter.getItemCount(); + boolean checkedCountChanged = false; + + if (itemCount == 0) { + // Optimized path for empty adapter: remove all items. + checkStates.clear(); + if (checkedIdStates != null) { + checkedIdStates.clear(); + } + checkedItemCount = 0; + checkedCountChanged = true; + } else if (checkedIdStates != null) { + // Clear out the positional check states, we'll rebuild it below from IDs. + checkStates.clear(); + + for (int checkedIndex = 0; checkedIndex < checkedIdStates.size(); checkedIndex++) { + final long id = checkedIdStates.keyAt(checkedIndex); + final int lastPos = checkedIdStates.valueAt(checkedIndex); + + if ((lastPos >= itemCount) || (id != adapter.getItemId(lastPos))) { + // Look around to see if the ID is nearby. If not, uncheck it. + final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); + final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, itemCount); + boolean found = false; + for (int searchPos = start; searchPos < end; searchPos++) { + final long searchId = adapter.getItemId(searchPos); + if (id == searchId) { + found = true; + checkStates.put(searchPos, true); + checkedIdStates.setValueAt(checkedIndex, searchPos); + break; + } + } + + if (!found) { + checkedIdStates.delete(id); + checkedIndex--; + checkedItemCount--; + checkedCountChanged = true; + if (choiceActionMode != null && multiChoiceModeCallback != null) { + multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, lastPos, id, false); + } + } + } else { + checkStates.put(lastPos, true); + } + } + } else { + // If the total number of items decreased, remove all out-of-range check indexes. + for (int i = checkStates.size() - 1; (i >= 0) && (checkStates.keyAt(i) >= itemCount); i--) { + if (checkStates.valueAt(i)) { + checkedItemCount--; + checkedCountChanged = true; + } + checkStates.delete(checkStates.keyAt(i)); + } + } + + if (checkedCountChanged && choiceActionMode != null) { + if (checkedItemCount == 0) { + choiceActionMode.finish(); + } else { + choiceActionMode.invalidate(); + } + } + } + + class AdapterDataSetObserver extends RecyclerView.AdapterDataObserver { + + @Override + public void onChanged() { + confirmCheckedPositions(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + confirmCheckedPositions(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + confirmCheckedPositions(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + confirmCheckedPositions(); + } + } + + class MultiChoiceModeWrapper implements MultiChoiceModeListener { + + private MultiChoiceModeListener wrapped; + + public void setWrapped(@NonNull MultiChoiceModeListener wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return wrapped.onCreateActionMode(mode, menu); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return wrapped.onPrepareActionMode(mode, menu); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return wrapped.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + wrapped.onDestroyActionMode(mode); + choiceActionMode = null; + clearChoices(); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + wrapped.onItemCheckedStateChanged(mode, position, id, checked); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/checkable_foreground.xml b/app/src/main/res/drawable/checkable_foreground.xml new file mode 100644 index 0000000..a98c8b5 --- /dev/null +++ b/app/src/main/res/drawable/checkable_foreground.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_event.xml b/app/src/main/res/layout/item_event.xml index c7f6893..685938c 100644 --- a/app/src/main/res/layout/item_event.xml +++ b/app/src/main/res/layout/item_event.xml @@ -1,14 +1,17 @@ - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/simple_list_item_2_material.xml b/app/src/main/res/layout/simple_list_item_2_material.xml index 1330194..2e58de7 100644 --- a/app/src/main/res/layout/simple_list_item_2_material.xml +++ b/app/src/main/res/layout/simple_list_item_2_material.xml @@ -2,6 +2,8 @@ + @@ -26,6 +27,12 @@ + + + + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b10c5b1..8e7d167 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @null @drawable/activated_background + @style/RecyclerView +