Home | History | Annotate | Download | only in browse
      1 /*
      2  * Copyright (C) 2012 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.browse;
     19 
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.app.FragmentManager;
     22 import android.content.Context;
     23 import android.content.res.TypedArray;
     24 import android.database.DataSetObservable;
     25 import android.database.DataSetObserver;
     26 import android.graphics.drawable.Drawable;
     27 import android.support.v4.view.ViewPager;
     28 import android.view.View;
     29 import android.view.ViewPropertyAnimator;
     30 
     31 import com.android.mail.R;
     32 import com.android.mail.graphics.PageMarginDrawable;
     33 import com.android.mail.providers.Account;
     34 import com.android.mail.providers.Conversation;
     35 import com.android.mail.providers.Folder;
     36 import com.android.mail.ui.AbstractActivityController;
     37 import com.android.mail.ui.ActivityController;
     38 import com.android.mail.ui.RestrictedActivity;
     39 import com.android.mail.utils.LogUtils;
     40 import com.android.mail.utils.Utils;
     41 
     42 /**
     43  * A simple controller for a {@link ViewPager} of conversations.
     44  * <p>
     45  * Instead of placing a ViewPager in a Fragment that replaces the other app views, we leave a
     46  * ViewPager in the activity's view hierarchy at all times and have this controller manage it.
     47  * This allows the ViewPager to safely instantiate inner conversation fragments since it is not
     48  * itself contained in a Fragment (no nested fragments!).
     49  * <p>
     50  * This arrangement has pros and cons...<br>
     51  * pros: FragmentManager manages restoring conversation fragments, each conversation gets its own
     52  * LoaderManager<br>
     53  * cons: the activity's Controller has to specially handle show/hide conversation view,
     54  * conversation fragment transitions must be done manually
     55  * <p>
     56  * This controller is a small delegate of {@link AbstractActivityController} and shares its
     57  * lifetime.
     58  *
     59  */
     60 public class ConversationPagerController {
     61 
     62     private ViewPager mPager;
     63     private ConversationPagerAdapter mPagerAdapter;
     64     private FragmentManager mFragmentManager;
     65     private ActivityController mActivityController;
     66     private boolean mShown;
     67     /**
     68      * True when the initial conversation passed to show() is busy loading. We assume that the
     69      * first {@link #onConversationSeen()} callback is triggered by that initial
     70      * conversation, and unset this flag when first signaled. Side-to-side paging will not re-enable
     71      * this flag, since it's only needed for initial conversation load.
     72      */
     73     private boolean mInitialConversationLoading;
     74     private final DataSetObservable mLoadedObservable = new DataSetObservable();
     75 
     76     public static final String LOG_TAG = "ConvPager";
     77 
     78     /**
     79      * Enables an optimization to the PagerAdapter that causes ViewPager to initially load just the
     80      * target conversation, then when the conversation view signals that the conversation is loaded
     81      * and visible (via onConversationSeen), we switch to paged mode to load the left/right
     82      * adjacent conversations.
     83      * <p>
     84      * Should improve load times. It also works around an issue in ViewPager that always loads item
     85      * zero (with the fragment visibility hint ON) when the adapter is initially set.
     86      */
     87     private static final boolean ENABLE_SINGLETON_INITIAL_LOAD = false;
     88 
     89     /** Duration of pager.show(...)'s animation */
     90     private static final int SHOW_ANIMATION_DURATION = 300;
     91 
     92     public ConversationPagerController(RestrictedActivity activity,
     93             ActivityController controller) {
     94         mFragmentManager = activity.getFragmentManager();
     95         mPager = (ViewPager) activity.findViewById(R.id.conversation_pager);
     96         mActivityController = controller;
     97         setupPageMargin(activity.getActivityContext());
     98     }
     99 
    100     /**
    101      * Show the conversation pager for the given conversation and animate in if specified along
    102      * with given animation listener.
    103      * @param account current account
    104      * @param folder current folder
    105      * @param initialConversation conversation to display initially in pager
    106      * @param changeVisibility true if we need to make the pager appear
    107      * @param pagerAnimationListener animation listener for pager fade-in, null indicates no
    108      *                               animation should take place
    109      */
    110     public void show(Account account, Folder folder, Conversation initialConversation,
    111             boolean changeVisibility, AnimatorListenerAdapter pagerAnimationListener) {
    112         mInitialConversationLoading = true;
    113 
    114         if (mShown) {
    115             LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
    116             // optimize for the case where account+folder are the same, when we can just shift
    117             // the existing pager to show the new conversation
    118             // If in detached mode, don't do this optimization
    119             if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)
    120                     && !mPagerAdapter.isDetached()) {
    121                 final int pos = mPagerAdapter.getConversationPosition(initialConversation);
    122                 if (pos >= 0) {
    123                     setCurrentItem(pos);
    124                     return;
    125                 }
    126             }
    127             // unable to shift, destroy existing state and fall through to normal startup
    128             cleanup();
    129         }
    130 
    131         if (changeVisibility) {
    132             // If we have a pagerAnimationListener, go ahead and animate
    133             if (pagerAnimationListener != null) {
    134                 // Reset alpha to 0 before animating/making it visible
    135                 mPager.setAlpha(0f);
    136                 mPager.setVisibility(View.VISIBLE);
    137 
    138                 // Fade in pager to full visibility - this can be cancelled mid-animation
    139                 mPager.animate().alpha(1f)
    140                         .setDuration(SHOW_ANIMATION_DURATION).setListener(pagerAnimationListener);
    141 
    142             // Otherwise, make the pager appear without animation
    143             } else {
    144                 // In case pager animation was cancelled and alpha value was not reset,
    145                 // ensure that the pager is completely visible for a non-animated pager.show
    146                 mPager.setAlpha(1f);
    147                 mPager.setVisibility(View.VISIBLE);
    148             }
    149         }
    150 
    151         mPagerAdapter = new ConversationPagerAdapter(mPager.getContext(), mFragmentManager,
    152                 account, folder, initialConversation);
    153         mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
    154         mPagerAdapter.setActivityController(mActivityController);
    155         mPagerAdapter.setPager(mPager);
    156         LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
    157 
    158         Utils.sConvLoadTimer.mark("pager init");
    159         LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s adapter=%s",
    160                 mPagerAdapter.getCount(), initialConversation, mPagerAdapter);
    161         mPager.setAdapter(mPagerAdapter);
    162 
    163         if (!ENABLE_SINGLETON_INITIAL_LOAD) {
    164             // FIXME: unnecessary to do this on restore. setAdapter will restore current position
    165             final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
    166             if (initialPos >= 0) {
    167                 LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
    168                 setCurrentItem(initialPos);
    169             }
    170         }
    171         Utils.sConvLoadTimer.mark("pager setAdapter");
    172 
    173         mShown = true;
    174     }
    175 
    176     /**
    177      * Hide the pager and cancel any running/pending animation
    178      * @param changeVisibility true if we need to make the pager disappear
    179      */
    180     public void hide(boolean changeVisibility) {
    181         if (!mShown) {
    182             LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
    183             return;
    184         }
    185         mShown = false;
    186 
    187         // Cancel any potential animations to avoid listener methods running when they shouldn't
    188         mPager.animate().cancel();
    189 
    190         if (changeVisibility) {
    191             mPager.setVisibility(View.GONE);
    192         }
    193 
    194         LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
    195         mPager.setAdapter(null);
    196         cleanup();
    197     }
    198 
    199     /**
    200      * Part of a delicate dance to kill fragments on restore after rotation if
    201      * the device configuration no longer calls for them. You must call
    202      * {@link #show(Account, Folder, Conversation, boolean, boolean)} first, and you probably want
    203      * to call {@link #hide(boolean)} afterwards to finish the cleanup. See go/xqaxk. Sorry...
    204      *
    205      */
    206     public void killRestoredFragments() {
    207         mPagerAdapter.killRestoredFragments();
    208     }
    209 
    210     // Explicitly set the focus to the conversation pager, specifically the conv overlay.
    211     public void focusPager() {
    212         mPager.requestFocus();
    213     }
    214 
    215     private void setCurrentItem(int pos) {
    216         // disable onPageSelected notifications during this operation. that listener is only there
    217         // to update the rest of the app when the user swipes to another page.
    218         mPagerAdapter.enablePageChangeListener(false);
    219         mPager.setCurrentItem(pos);
    220         mPagerAdapter.enablePageChangeListener(true);
    221     }
    222 
    223     public boolean isInitialConversationLoading() {
    224         return mInitialConversationLoading;
    225     }
    226 
    227     public void onDestroy() {
    228         // need to release resources before a configuration change kills the activity and controller
    229         cleanup();
    230     }
    231 
    232     private void cleanup() {
    233         if (mPagerAdapter != null) {
    234             // stop observing the conversation list
    235             mPagerAdapter.setActivityController(null);
    236             mPagerAdapter.setPager(null);
    237             mPagerAdapter = null;
    238         }
    239     }
    240 
    241     public void onConversationSeen() {
    242         if (mPagerAdapter == null) {
    243             return;
    244         }
    245 
    246         // take the adapter out of singleton mode to begin loading the
    247         // other non-visible conversations
    248         if (mPagerAdapter.isSingletonMode()) {
    249             LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
    250                     " switching to cursor mode to load other conversations");
    251             mPagerAdapter.setSingletonMode(false);
    252         }
    253 
    254         if (mInitialConversationLoading) {
    255             mInitialConversationLoading = false;
    256             mLoadedObservable.notifyChanged();
    257         }
    258     }
    259 
    260     public void registerConversationLoadedObserver(DataSetObserver observer) {
    261         mLoadedObservable.registerObserver(observer);
    262     }
    263 
    264     public void unregisterConversationLoadedObserver(DataSetObserver observer) {
    265         mLoadedObservable.unregisterObserver(observer);
    266     }
    267 
    268     /**
    269      * Stops listening to changes to the adapter. This must be followed immediately by
    270      * {@link #hide(boolean)}.
    271      */
    272     public void stopListening() {
    273         if (mPagerAdapter != null) {
    274             mPagerAdapter.stopListening();
    275         }
    276     }
    277 
    278     private void setupPageMargin(Context c) {
    279         final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
    280         final Drawable divider = a.getDrawable(0);
    281         a.recycle();
    282         final int padding = c.getResources().getDimensionPixelOffset(
    283                 R.dimen.conversation_page_gutter);
    284         final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0,
    285                 c.getResources().getColor(R.color.conversation_view_background_color));
    286         mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
    287         mPager.setPageMarginDrawable(gutterDrawable);
    288     }
    289 
    290 }
    291