package cgeo.geocaching.activity; import cgeo.geocaching.R; import cgeo.geocaching.utils.Log; import android.app.Activity; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import butterknife.ButterKnife; import com.viewpagerindicator.TitlePageIndicator; import org.apache.commons.lang3.tuple.Pair; /** * Abstract activity with the ability to manage pages in a view pager. * * @param <Page> * Enum listing all available pages of this activity. The pages available at a certain point of time are * defined by overriding {@link #getOrderedPages()}. */ public abstract class AbstractViewPagerActivity<Page extends Enum<Page>> extends AbstractActionBarActivity { /** * A {@link List} of all available pages. * * TODO Move to adapter */ private final List<Page> pageOrder = new ArrayList<>(); /** * Instances of all {@link PageViewCreator}. */ private final Map<Page, PageViewCreator> viewCreators = new HashMap<>(); /** * Store the states of the page views to be able to persist them when destroyed and reinstantiated again */ private final Map<Page, Bundle> viewStates = new HashMap<>(); /** * The {@link ViewPager} for this activity. */ private ViewPager viewPager; /** * The {@link ViewPagerAdapter} for this activity. */ private ViewPagerAdapter viewPagerAdapter; /** * The {@link TitlePageIndicator} for this activity. */ private TitlePageIndicator titleIndicator; public interface PageViewCreator { /** * Returns a validated view. * */ View getDispatchedView(final ViewGroup parentView); /** * Returns a (maybe cached) view. * */ View getView(final ViewGroup parentView); /** * Handles changed data-sets. */ void notifyDataSetChanged(); /** * Gets state of the view */ @Nullable Bundle getViewState(); /** * Set the state of the view */ void setViewState(@NonNull Bundle state); } /** * Page selection interface for the view pager. * */ protected interface OnPageSelectedListener { void onPageSelected(int position); } /** * The ViewPagerAdapter for scrolling through pages of the CacheDetailActivity. */ private class ViewPagerAdapter extends PagerAdapter { @Override public void destroyItem(final ViewGroup container, final int position, final Object object) { if (position >= pageOrder.size()) { return; } final Page page = pageOrder.get(position); // Store the state of the view if the page supports it final PageViewCreator creator = viewCreators.get(page); if (creator != null) { final Bundle state = creator.getViewState(); if (state != null) { viewStates.put(page, state); } } container.removeView((View) object); } @Override public void finishUpdate(final ViewGroup container) { // empty } @Override public int getCount() { return pageOrder.size(); } @Override public Object instantiateItem(final ViewGroup container, final int position) { final Page page = pageOrder.get(position); PageViewCreator creator = viewCreators.get(page); if (creator == null && page != null) { creator = AbstractViewPagerActivity.this.createViewCreator(page); viewCreators.put(page, creator); viewStates.put(page, new Bundle()); } View view = null; try { if (creator != null) { // Result from getView() is maybe cached, but it should be valid because the // creator should be informed about data-changes with notifyDataSetChanged() view = creator.getView(container); // Restore the state of the view if the page supports it final Bundle state = viewStates.get(page); if (state != null) { creator.setViewState(state); } container.addView(view, 0); } } catch (final Exception e) { Log.e("ViewPagerAdapter.instantiateItem ", e); } return view; } @Override public boolean isViewFromObject(final View view, final Object object) { return view == object; } @Override public void restoreState(final Parcelable arg0, final ClassLoader arg1) { // empty } @Override public Parcelable saveState() { return null; } @Override public void startUpdate(final ViewGroup arg0) { // empty } @Override public int getItemPosition(final Object object) { // We are doing the caching. So pretend that the view is gone. // The ViewPager will get it back in instantiateItem() return POSITION_NONE; } @Override public CharSequence getPageTitle(final int position) { final Page page = pageOrder.get(position); if (page == null) { return ""; } return AbstractViewPagerActivity.this.getTitle(page); } } /** * Create the view pager. Call this from the {@link Activity#onCreate} implementation. * * @param startPageIndex * index of the page shown first * @param pageSelectedListener * page selection listener or {@code null} */ protected final void createViewPager(final int startPageIndex, final OnPageSelectedListener pageSelectedListener) { // initialize ViewPager viewPager = ButterKnife.findById(this, R.id.viewpager); viewPagerAdapter = new ViewPagerAdapter(); viewPager.setAdapter(viewPagerAdapter); titleIndicator = (TitlePageIndicator) findViewById(R.id.pager_indicator); titleIndicator.setViewPager(viewPager); if (pageSelectedListener != null) { titleIndicator.setOnPageChangeListener(new OnPageChangeListener() { @Override public void onPageSelected(final int position) { pageSelectedListener.onPageSelected(position); } @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { // empty } @Override public void onPageScrollStateChanged(final int state) { // empty } }); } // switch to entry page (last used or 2) if (viewPagerAdapter.getCount() < startPageIndex) { for (int i = 0; i <= startPageIndex; i++) { // we can't switch to a page that is out of bounds, so we add null-pages pageOrder.add(null); } } viewPagerAdapter.notifyDataSetChanged(); viewPager.setCurrentItem(startPageIndex, false); } /** * create the view creator for the given page * * @return new view creator */ protected abstract PageViewCreator createViewCreator(Page page); /** * get the title for the given page */ protected abstract String getTitle(Page page); protected final void reinitializeViewPager() { // notify all creators that the data has changed for (final PageViewCreator creator : viewCreators.values()) { creator.notifyDataSetChanged(); } // reset the stored view states of all pages for (final Bundle state : viewStates.values()) { state.clear(); } pageOrder.clear(); final Pair<List<? extends Page>, Integer> pagesAndIndex = getOrderedPages(); pageOrder.addAll(pagesAndIndex.getLeft()); // Since we just added pages notifyDataSetChanged needs to be called before we possibly setCurrentItem below. // But, calling it will reset current item and we won't be able to tell if we would have been out of bounds final int currentItem = getCurrentItem(); // notify the adapter that the data has changed viewPagerAdapter.notifyDataSetChanged(); // switch to details page, if we're out of bounds final int defaultPage = pagesAndIndex.getRight(); if (currentItem < 0 || currentItem >= viewPagerAdapter.getCount()) { viewPager.setCurrentItem(defaultPage, false); } // notify the indicator that the data has changed titleIndicator.notifyDataSetChanged(); } /** * @return the currently available list of ordered pages, together with the index of the default page */ protected abstract Pair<List<? extends Page>, Integer> getOrderedPages(); public final Page getPage(final int position) { return pageOrder.get(position); } protected final int getPageIndex(final Page page) { return pageOrder.indexOf(page); } protected final PageViewCreator getViewCreator(final Page page) { return viewCreators.get(page); } protected final boolean isCurrentPage(final Page page) { return getCurrentItem() == getPageIndex(page); } protected int getCurrentItem() { return viewPager.getCurrentItem(); } }