package org.wordpress.android.ui; import android.content.Context; import android.support.annotation.MenuRes; import android.support.design.widget.AppBarLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; import org.wordpress.android.R; import org.wordpress.android.models.FilterCriteria; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.helpers.SwipeToRefreshHelper; import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; import org.wordpress.android.widgets.RecyclerItemDecoration; import java.util.ArrayList; import java.util.List; public class FilteredRecyclerView extends RelativeLayout { private ProgressBar mProgressLoadMore; private SwipeToRefreshHelper mSwipeToRefreshHelper; private Spinner mSpinner; private boolean mSelectingRememberedFilterOnCreate = false; private RecyclerView mRecyclerView; private TextView mEmptyView; private View mCustomEmptyView; private Toolbar mToolbar; private AppBarLayout mAppBarLayout; private List<FilterCriteria> mFilterCriteriaOptions; private FilterCriteria mCurrentFilter; private FilterListener mFilterListener; private SpinnerAdapter mSpinnerAdapter; private RecyclerView.Adapter<RecyclerView.ViewHolder> mAdapter; private int mSpinnerTextColor; private int mSpinnerDrawableRight; private AppLog.T mTAG; public FilteredRecyclerView(Context context) { super(context); init(); } public FilteredRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public FilteredRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public void setRefreshing(boolean refreshing) { mSwipeToRefreshHelper.setRefreshing(refreshing); } public boolean isRefreshing(){ return mSwipeToRefreshHelper.isRefreshing(); } public void setCurrentFilter(FilterCriteria filter) { mCurrentFilter = filter; int position = mSpinnerAdapter.getIndexOfCriteria(filter); if (position > -1 && position != mSpinner.getSelectedItemPosition()) { mSpinner.setSelection(position); } } public FilterCriteria getCurrentFilter() { return mCurrentFilter; } public void setFilterListener(FilterListener filterListener){ mFilterListener = filterListener; setup(false); } public void setAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter){ mAdapter = adapter; mRecyclerView.setAdapter(mAdapter); } public RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter(){ return mAdapter; } public void setSwipeToRefreshEnabled(boolean enable){ mSwipeToRefreshHelper.setEnabled(enable); } public void setLogT(AppLog.T tag){ mTAG = tag; } public void setCustomEmptyView(View v){ mCustomEmptyView = v; } private void init() { inflate(getContext(), R.layout.filtered_list_component, this); int spacingHorizontal = 0; int spacingVertical = DisplayUtils.dpToPx(getContext(), 1); mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical)); mToolbar = (Toolbar) findViewById(R.id.toolbar_with_spinner); mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout); mEmptyView = (TextView) findViewById(R.id.empty_view); // progress bar that appears when loading more items mProgressLoadMore = (ProgressBar) findViewById(R.id.progress_loading); mProgressLoadMore.setVisibility(View.GONE); mSwipeToRefreshHelper = new SwipeToRefreshHelper(getContext(), (CustomSwipeRefreshLayout) findViewById(R.id.ptr_layout), new SwipeToRefreshHelper.RefreshListener() { @Override public void onRefreshStarted() { post(new Runnable() { @Override public void run() { if (!NetworkUtils.checkConnection(getContext())) { mSwipeToRefreshHelper.setRefreshing(false); updateEmptyView(EmptyViewMessageType.NETWORK_ERROR); return; } if (mFilterListener != null){ mFilterListener.onLoadData(); } } }); } }); if (mSpinner == null) { mSpinner = (Spinner) findViewById(R.id.filter_spinner); } } private void setup(boolean refresh){ List<FilterCriteria> criterias = mFilterListener.onLoadFilterCriteriaOptions(refresh); if (criterias != null){ mFilterCriteriaOptions = criterias; } if (criterias == null){ mFilterListener.onLoadFilterCriteriaOptionsAsync(new FilterCriteriaAsyncLoaderListener() { @Override public void onFilterCriteriasLoaded(List<FilterCriteria> criteriaList) { if (criteriaList != null) { mFilterCriteriaOptions = new ArrayList<FilterCriteria>(); mFilterCriteriaOptions.addAll(criteriaList); initSpinnerAdapter(); setCurrentFilter(mFilterListener.onRecallSelection()); } } }, refresh); } else { initSpinnerAdapter(); setCurrentFilter(mFilterListener.onRecallSelection()); } } private void initSpinnerAdapter(){ mSpinnerAdapter = new SpinnerAdapter(getContext(), mFilterCriteriaOptions); mSelectingRememberedFilterOnCreate = true; mSpinner.setAdapter(mSpinnerAdapter); mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (mSelectingRememberedFilterOnCreate) { mSelectingRememberedFilterOnCreate = false; return; } FilterCriteria selectedCriteria = (FilterCriteria) mSpinnerAdapter.getItem(position); if (mCurrentFilter == selectedCriteria) { AppLog.d(mTAG, "The selected STATUS is already active: " + selectedCriteria.getLabel()); return; } AppLog.d(mTAG, "NEW STATUS : " + selectedCriteria.getLabel()); setCurrentFilter(selectedCriteria); if (mFilterListener != null) { mFilterListener.onFilterSelected(position, selectedCriteria); setRefreshing(true); mFilterListener.onLoadData(); } } @Override public void onNothingSelected(AdapterView<?> parent) { // nop } }); } private boolean hasAdapter() { return (mAdapter != null); } public boolean emptyViewIsVisible(){ return (mEmptyView != null && mEmptyView.getVisibility() == View.VISIBLE); } public void hideEmptyView() { if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); } } public void updateEmptyView(EmptyViewMessageType emptyViewMessageType) { if (mEmptyView == null) return; if ((hasAdapter() && mAdapter.getItemCount() == 0) || !hasAdapter()) { if (mFilterListener != null){ if (mCustomEmptyView == null){ String msg = mFilterListener.onShowEmptyViewMessage(emptyViewMessageType); if (msg == null){ msg = getContext().getString(R.string.empty_list_default); } mEmptyView.setText(msg); mEmptyView.setVisibility(View.VISIBLE); } else { mEmptyView.setVisibility(View.GONE); mFilterListener.onShowCustomEmptyView(emptyViewMessageType); } } } else { mEmptyView.setVisibility(View.GONE); } } /** * show/hide progress bar which appears at the bottom when loading more items */ public void showLoadingProgress() { if (mProgressLoadMore != null) { mProgressLoadMore.setVisibility(View.VISIBLE); } } public void hideLoadingProgress() { if (mProgressLoadMore != null) { mProgressLoadMore.setVisibility(View.GONE); } } /* * add a menu to the right side of the toolbar, returns the toolbar menu so the caller * can act upon it */ public Menu addToolbarMenu(@MenuRes int menuResId) { mToolbar.inflateMenu(menuResId); return mToolbar.getMenu(); } public void setToolbarBackgroundColor(int color){ mToolbar.setBackgroundColor(color); } public void setToolbarSpinnerTextColor(int color){ mSpinnerTextColor = color; } public void setToolbarSpinnerDrawable(int drawableResId){ mSpinnerDrawableRight = drawableResId; } public void setToolbarLeftPadding(int paddingLeft){ mToolbar.setPadding(paddingLeft, mToolbar.getPaddingTop(), mToolbar.getPaddingRight(), mToolbar.getPaddingBottom()); } public void setToolbarRightPadding(int paddingRight){ mToolbar.setPadding( mToolbar.getPaddingLeft(), mToolbar.getPaddingTop(), paddingRight, mToolbar.getPaddingBottom()); } public void setToolbarLeftAndRightPadding(int paddingLeft, int paddingRight){ mToolbar.setPadding( paddingLeft, mToolbar.getPaddingTop(), paddingRight, mToolbar.getPaddingBottom()); } public void scrollRecycleViewToPosition(int position) { if (mRecyclerView == null) return; mRecyclerView.scrollToPosition(position); } public int getCurrentPosition() { if (mRecyclerView != null && mRecyclerView.getLayoutManager() != null) { return ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } else { return -1; } } public void smoothScrollToPosition(int position){ if (mRecyclerView != null && mRecyclerView.getLayoutManager() != null) { mRecyclerView.getLayoutManager().smoothScrollToPosition(mRecyclerView, null, position); } } public void addItemDecoration(RecyclerView.ItemDecoration decor){ if (mRecyclerView == null) return; mRecyclerView.addItemDecoration(decor); } public void addOnScrollListener(RecyclerView.OnScrollListener listener) { if (mRecyclerView != null) { mRecyclerView.addOnScrollListener(listener); } } public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { if (mRecyclerView != null) { mRecyclerView.removeOnScrollListener(listener); } } public void hideToolbar(){ mAppBarLayout.setExpanded(false, true); } public void showToolbar(){ mAppBarLayout.setExpanded(true, true); } /* * use this if you need to reload the criterias for this FilteredRecyclerView. The actual data loading goes * through the FilteredRecyclerView lifecycle using its listeners: * * - FilterCriteriaAsyncLoaderListener * and * - FilterListener.onLoadFilterCriteriaOptions * */ public void refreshFilterCriteriaOptions(){ setup(true); } /* * adapter used by the filter spinner */ private class SpinnerAdapter extends BaseAdapter { private final List<FilterCriteria> mFilterValues; private final LayoutInflater mInflater; SpinnerAdapter(Context context, List<FilterCriteria> filterValues) { super(); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mFilterValues = filterValues; } @Override public int getCount() { return (mFilterValues != null ? mFilterValues.size() : 0); } @Override public Object getItem(int position) { return mFilterValues.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final View view; if (convertView == null) { view = mInflater.inflate(R.layout.filter_spinner_item, parent, false); final TextView text = (TextView) view.findViewById(R.id.text); FilterCriteria selectedCriteria = (FilterCriteria)getItem(position); text.setText(selectedCriteria.getLabel()); if (mSpinnerTextColor != 0){ text.setTextColor(mSpinnerTextColor); } if (mSpinnerDrawableRight != 0){ text.setCompoundDrawablesWithIntrinsicBounds(0, 0, mSpinnerDrawableRight, 0); text.setCompoundDrawablePadding(getResources().getDimensionPixelSize(R.dimen.margin_medium)); text.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); } } else { view = convertView; } return view; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { FilterCriteria selectedCriteria = (FilterCriteria)getItem(position); final TagViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.toolbar_spinner_dropdown_item, parent, false); holder = new TagViewHolder(convertView); convertView.setTag(holder); } else { holder = (TagViewHolder) convertView.getTag(); } holder.textView.setText(selectedCriteria.getLabel()); return convertView; } private class TagViewHolder { private final TextView textView; TagViewHolder(View view) { textView = (TextView) view.findViewById(R.id.text); } } public int getIndexOfCriteria(FilterCriteria tm) { if (tm != null && mFilterValues != null){ for (int i = 0; i < mFilterValues.size(); i++) { FilterCriteria criteria = mFilterValues.get(i); if (criteria != null && criteria.equals(tm)) { return i; } } } return -1; } } /* * returns true if the first item is still visible in the RecyclerView - will return * false if the first item is scrolled out of view, or if the list is empty */ public boolean isFirstItemVisible() { if (mRecyclerView == null || mRecyclerView.getLayoutManager() == null) { return false; } View child = mRecyclerView.getLayoutManager().getChildAt(0); return (child != null && mRecyclerView.getLayoutManager().getPosition(child) == 0); } /** * implement this interface to use FilterRecyclerView */ public interface FilterListener { /** * Called upon initialization - provide an array of FilterCriterias here. These are the possible criterias * the Spinner is loaded with, and through which the data can be filtered. * * @param refresh "true"if the criterias need be refreshed * @return an array of FilterCriteria to be used on Spinner initialization, or null if going to use the * Async method below */ List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh); /** * Called upon initialization - you can use this callback to start an asynctask to build an array of * FilterCriterias here. Once the AsyncTask is done, it should call the provided listener * The Spinner is then loaded with such array of FilterCriterias, through which the main data can be filtered. * * @param listener to be called to pass the FilterCriteria array when done * @param refresh "true"if the criterias need be refreshed */ void onLoadFilterCriteriaOptionsAsync(FilterCriteriaAsyncLoaderListener listener, boolean refresh); /** * Called upon initialization, right after onLoadFilterCriteriaOptions(). * Once the criteria options are set up, use this callback to return the latest option selected on the * screen the last time the user visited it, or a default value for the filter Spinner to be initialized with. * * @return */ FilterCriteria onRecallSelection(); /** * When this method is called, you should load data into the FilteredRecyclerView adapter, using the * latest criteria passed to you in a previous onFilterSelected() call. * Within the FilteredRecyclerView lifecycle, this is triggered in three different moments: * 1 - upon initialisation * 2 - each time a screen refresh is requested * 3 - each time the user changes the filter spinner selection */ void onLoadData(); /** * Called each time the user changes the Spinner selection (i.e. changes the criteria on which to filter * the data). You should only take note of the change, and remember it, as a request to load data with * the newly selected filter shall always arrive through onLoadData(). * The parameters passed in this callback can be used alternatively as per your convenience. * * @param position of the selected criteria within the array returned by onLoadFilterCriteriaOptions() * @param criteria the actual criteria selected */ void onFilterSelected(int position, FilterCriteria criteria); /** * Called when there's no data to show. * * @param emptyViewMsgType this will hint you on the reason why no data is being shown, so you can return * a proper message to be displayed to the user * @return the message to be displayed to the user, or null if using a Custom Empty View (see below) */ String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType); /** * Called when there's no data to show, and only if a custom EmptyView is set (onShowEmptyViewMessage will * be called otherwise). * * @param emptyViewMsgType this will hint you on the reason why no data is being shown, and * also here you should perform any actions on your custom empty view * @return nothing */ void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType); } /** * implement this interface to load filtering options (that is, an array of FilterCriteria) asynchronously */ public interface FilterCriteriaAsyncLoaderListener{ /** * Will be called during initialization of FilteredRecyclerView once you're ready building the FilterCriteria array * * @param criteriaList the array of FilterCriteria objects you just built */ void onFilterCriteriasLoaded(List<FilterCriteria> criteriaList); } }