package com.boardgamegeek.ui; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView.OnItemClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import com.boardgamegeek.R; import com.boardgamegeek.events.SyncCompleteEvent; import com.boardgamegeek.events.SyncEvent; import com.boardgamegeek.service.SyncService; import com.boardgamegeek.util.HttpUtils; import com.boardgamegeek.util.PresentationUtils; import com.squareup.picasso.Picasso; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; import hugo.weaving.DebugLog; import icepick.Icepick; import icepick.State; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; public abstract class StickyHeaderListFragment extends Fragment implements OnRefreshListener { final private Handler focusHandler = new Handler(); final private Runnable listViewFocusRunnable = new Runnable() { public void run() { listView.focusableViewAvailable(listView); } }; final private OnItemClickListener onItemClickListener = new OnItemClickListener() { public void onItemClick(android.widget.AdapterView<?> parent, View view, int position, long id) { onListItemClick(view, position, id); } }; @Nullable final private OnScrollListener onScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(@Nullable AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (swipeRefreshLayout != null) { int topRowVerticalPosition = (view == null || view.getChildCount() == 0) ? 0 : view.getChildAt(0).getTop(); swipeRefreshLayout.setEnabled(isRefreshable() && (firstVisibleItem == 0 && topRowVerticalPosition >= 0)); } if (totalItemCount == 0) { return; } int newScrollY = getTopItemScrollY(); if (isSameRow(firstVisibleItem)) { boolean isSignificantDelta = Math.abs(lastScrollY - newScrollY) > SCROLL_THRESHOLD; if (isSignificantDelta) { if (lastScrollY > newScrollY) { onScrollUp(); } else { onScrollDown(); } } } else { if (firstVisibleItem > previousFirstVisibleItem) { onScrollUp(); } else { onScrollDown(); } previousFirstVisibleItem = firstVisibleItem; } lastScrollY = newScrollY; } }; protected boolean isRefreshable() { return getSyncType() != SyncService.FLAG_SYNC_NONE; } private static final int SCROLL_THRESHOLD = 20; private Unbinder unbinder; @BindView(R.id.swipe_refresh) SwipeRefreshLayout swipeRefreshLayout; @BindView(R.id.empty_container) ViewGroup emptyContainer; @BindView(android.R.id.empty) TextView emptyTextView; @BindView(R.id.empty_button) Button emptyButton; @BindView(R.id.progressContainer) View progressContainer; @BindView(R.id.list_container) View listContainer; @BindView(R.id.fab) FloatingActionButton fabView; private StickyListHeadersListView listView; private StickyListHeadersAdapter adapter; private CharSequence emptyText; private boolean isListShown; @State int listViewStatePosition; @State int listViewStateTop; private boolean isSyncing; private int previousFirstVisibleItem; private int lastScrollY; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_sticky_header_list, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ensureList(); final StickyListHeadersListView listView = getListView(); int padding = getResources().getDimensionPixelSize(shouldPadForFab() ? R.dimen.fab_buffer : R.dimen.padding_standard); assert listView != null; listView.setClipToPadding(false); listView.setPadding(0, 0, 0, padding); if (dividerShown()) { int height = getResources().getDimensionPixelSize(R.dimen.divider_height); listView.setDivider(ContextCompat.getDrawable(getActivity(), R.drawable.list_divider)); listView.setDividerHeight(height); } else { listView.setDivider(null); } listView.setFastScrollEnabled(true); } @Override public void onDestroyView() { focusHandler.removeCallbacks(listViewFocusRunnable); listView = null; isListShown = false; progressContainer = listContainer = null; emptyTextView = null; super.onDestroyView(); if (unbinder != null) unbinder.unbind(); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); } @DebugLog @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onPause() { super.onPause(); saveScrollState(); } @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveScrollState(); super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @DebugLog @Override public void onStop() { EventBus.getDefault().unregister(this); super.onStop(); } @SuppressWarnings("unused") @DebugLog @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(@NonNull SyncEvent event) { if ((event.getType() & getSyncType()) == getSyncType()) { isSyncing(true); } } @SuppressWarnings({ "unused", "UnusedParameters" }) @DebugLog @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(SyncCompleteEvent event) { isSyncing(false); } protected int getSyncType() { return SyncService.FLAG_SYNC_NONE; } @DebugLog protected void isSyncing(boolean value) { isSyncing = value; updateRefreshStatus(); } @DebugLog private void updateRefreshStatus() { if (swipeRefreshLayout != null) { swipeRefreshLayout.post(new Runnable() { @Override public void run() { if (swipeRefreshLayout != null) { // this check seems unnecessary, but NullPointerException are getting thrown swipeRefreshLayout.setRefreshing(isSyncing); } } }); } } @Override public void onRefresh() { triggerRefresh(); } protected void triggerRefresh() { } protected boolean dividerShown() { return false; } @SuppressWarnings("UnusedParameters") protected void onListItemClick(View view, int position, long id) { } public void setListAdapter(StickyListHeadersAdapter adapter) { boolean hadAdapter = this.adapter != null; this.adapter = adapter; if (listView != null) { listView.setAdapter(adapter); if (!isListShown && !hadAdapter) { // The list was hidden, and previously didn't have an adapter. It is now time to show it. final View view = getView(); setListShown(true, view != null && view.getWindowToken() != null); } } } @Nullable public StickyListHeadersListView getListView() { ensureList(); return listView; } public void setEmptyText(CharSequence text) { ensureList(); emptyTextView.setText(text); if (emptyText == null) { listView.setEmptyView(emptyContainer); } emptyText = text; } public void setProgressShown(boolean shown) { if (shown) { if (progressContainer.getVisibility() != View.VISIBLE) { progressContainer.clearAnimation(); progressContainer.setVisibility(View.VISIBLE); } } else { if (progressContainer.getVisibility() != View.GONE) { progressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out)); progressContainer.setVisibility(View.GONE); } } } public void setListShown(boolean shown) { setListShown(shown, true); } public void setListShownNoAnimation(boolean shown) { setListShown(shown, false); } private void setListShown(boolean shown, boolean animate) { ensureList(); if (isListShown == shown) { return; } isListShown = shown; if (shown) { if (animate) { progressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out)); listContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); } else { progressContainer.clearAnimation(); listContainer.clearAnimation(); } progressContainer.setVisibility(View.GONE); listContainer.setVisibility(View.VISIBLE); } else { if (animate) { progressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); listContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out)); } else { progressContainer.clearAnimation(); listContainer.clearAnimation(); } progressContainer.setVisibility(View.VISIBLE); listContainer.setVisibility(View.GONE); } } private void saveScrollState() { if (isAdded()) { View v = listView.getWrappedList().getChildAt(0); int top = (v == null) ? 0 : v.getTop(); listViewStatePosition = listView.getFirstVisiblePosition(); listViewStateTop = top; } } protected void restoreScrollState() { if (listViewStatePosition >= 0 && isAdded()) { listView.getWrappedList().setSelectionFromTop(listViewStatePosition, listViewStateTop); } } protected void resetScrollState() { listViewStatePosition = 0; listViewStateTop = -1; } protected void loadThumbnail(String path, ImageView target) { loadThumbnail(path, target, R.drawable.thumbnail_image_empty); } protected void loadThumbnail(String path, ImageView target, int placeholderResId) { Picasso.with(getActivity()).load(HttpUtils.ensureScheme(path)).placeholder(placeholderResId) .error(placeholderResId).resizeDimen(R.dimen.thumbnail_list_size, R.dimen.thumbnail_list_size).centerCrop() .into(target); } private void ensureList() { if (listView != null) { return; } View root = getView(); if (root == null) { throw new IllegalStateException("Content view not yet created"); } unbinder = ButterKnife.bind(this, root); if (swipeRefreshLayout != null) { swipeRefreshLayout.setEnabled(isRefreshable()); swipeRefreshLayout.setOnRefreshListener(this); swipeRefreshLayout.setColorSchemeResources(PresentationUtils.getColorSchemeResources()); } emptyContainer.setVisibility(View.GONE); View rawListView = root.findViewById(android.R.id.list); if (!(rawListView instanceof StickyListHeadersListView)) { throw new RuntimeException("Content has view with id attribute 'android.R.id.list' that is not a StickyListHeadersListView class"); } listView = (StickyListHeadersListView) rawListView; //noinspection ConstantConditions if (listView == null) { throw new RuntimeException("Your content must have a ListView whose id attribute is 'android.R.id.list'"); } if (emptyText != null) { emptyTextView.setText(emptyText); listView.setEmptyView(emptyContainer); } listView.setDivider(null); isListShown = true; listView.setOnItemClickListener(onItemClickListener); listView.setOnScrollListener(onScrollListener); if (adapter != null) { StickyListHeadersAdapter adapter = this.adapter; this.adapter = null; setListAdapter(adapter); } else { // We are starting without an adapter, so assume we won't have our data right away and start with the progress indicator. if (progressContainer != null) { setListShown(false, false); } } focusHandler.post(listViewFocusRunnable); } protected void showFab(boolean show) { ensureList(); if (show) { fabView.show(); } else { fabView.hide(); } } @OnClick(R.id.fab) protected void onFabClicked() { // convenience for overriding } protected boolean shouldPadForFab() { return false; } @OnClick(R.id.empty_button) void onSyncClick() { SyncService.clearCollection(getActivity()); SyncService.sync(getActivity(), SyncService.FLAG_SYNC_COLLECTION); } protected void onScrollUp() { } protected void onScrollDown() { } private boolean isSameRow(int firstVisibleItem) { return firstVisibleItem == previousFirstVisibleItem; } private int getTopItemScrollY() { StickyListHeadersListView listView = getListView(); if (listView == null || listView.getChildAt(0) == null) return 0; View topChild = listView.getChildAt(0); return topChild.getTop(); } }