package com.philliphsu.clock2; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; 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.TextView; import com.philliphsu.clock2.alarms.ScrollHandler; import com.philliphsu.clock2.aospdatetimepicker.Utils; import com.philliphsu.clock2.model.BaseItemCursor; import com.philliphsu.clock2.model.ObjectWithId; import butterknife.Bind; /** * Created by Phillip Hsu on 7/26/2016. */ public abstract class RecyclerViewFragment< T extends ObjectWithId, VH extends BaseViewHolder, C extends BaseItemCursor, A extends BaseCursorAdapter> extends BaseFragment implements LoaderManager.LoaderCallbacks, OnListItemInteractionListener, ScrollHandler { private A mAdapter; private long mScrollToStableId = RecyclerView.NO_ID; // TODO: Rename id to recyclerView? // TODO: Rename variable to mRecyclerView? @Bind(R.id.list) RecyclerView mList; @Nullable // Subclasses are not required to use the default content layout, so this may not be present. @Bind(R.id.empty_view) TextView mEmptyView; public abstract void onFabClick(); /** * Callback invoked when we have scrolled to the stable id as set in * {@link #setScrollToStableId(long)}. * @param id the stable id we have scrolled to * @param position the position of the item with this stable id */ protected abstract void onScrolledToStableId(long id, int position); /** * @return the adapter to set on the RecyclerView. Called in onCreateView(). */ protected abstract A onCreateAdapter(); /** * @return a resource to a String that will be displayed when the list is empty */ @StringRes protected int emptyMessage() { // The reason this isn't abstract is so we don't require subclasses that // don't have an empty view to implement this. return 0; } /** * @return a resource to a Drawable that will be displayed when the list is empty */ @DrawableRes protected int emptyMessageIcon() { // TODO: If this is the same for all RecyclerViewFragments, then why not just specify // the compound drawable in XML? return R.drawable.ic_empty_list_96dp; } /** * @return whether the list should show an empty view when its adapter has an item count of zero */ protected boolean hasEmptyView() { return true; } /** * @return the adapter instance created from {@link #onCreateAdapter()} */ protected final A getAdapter() { return mAdapter; } /** * @return the LayoutManager to set on the RecyclerView. The default implementation * returns a vertical LinearLayoutManager. */ protected RecyclerView.LayoutManager getLayoutManager() { // Called in onCreateView(), so the host activity is alive already. return new LinearLayoutManager(getActivity()); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); mList.setLayoutManager(getLayoutManager()); mList.setAdapter(mAdapter = onCreateAdapter()); if (hasEmptyView() && mEmptyView != null) { // Configure the empty view, even if there currently are items. mEmptyView.setText(emptyMessage()); int iconColor = Utils.getTextColorFromThemeAttr(getActivity(), R.attr.themedIconTint); Drawable emptyMessageIcon = Utils.getTintedDrawable(getActivity(), emptyMessageIcon(), iconColor); mEmptyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, emptyMessageIcon, null, null); } return view; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // http://stackoverflow.com/a/14632434/5055032 // A Loader's lifecycle is bound to its Activity, not its Fragment. getLoaderManager().initLoader(0, null, this); } @Override public void onLoadFinished(Loader loader, C data) { mAdapter.swapCursor(data); if (hasEmptyView() && mEmptyView != null) { // TODO: Last I checked after a fresh install, this worked fine. // However, previous attempts (without fresh installs) didn't hide the empty view // upon an item being added. Verify this is no longer the case. mEmptyView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); } // This may have been a requery due to content change. If the change // was an insertion, scroll to the last modified alarm. performScrollToStableId(); } @Override public void onLoaderReset(Loader loader) { mAdapter.swapCursor(null); } /** * @return a layout resource that MUST contain a RecyclerView. The default implementation * returns a layout that has just a single RecyclerView in its hierarchy. */ @Override protected int contentLayout() { return R.layout.fragment_recycler_view; } @Override public void setScrollToStableId(long id) { mScrollToStableId = id; } @Override public void scrollToPosition(int position) { mList.smoothScrollToPosition(position); } private void performScrollToStableId() { if (mScrollToStableId != RecyclerView.NO_ID) { int position = -1; for (int i = 0; i < mAdapter.getItemCount(); i++) { if (mAdapter.getItemId(i) == mScrollToStableId) { position = i; break; } } if (position >= 0) { scrollToPosition(position); onScrolledToStableId(mScrollToStableId, position); } } // Reset mScrollToStableId = RecyclerView.NO_ID; } }