ConversationPagerController.java revision f59080ee7ddbb986a295a14578b55f17a10cff4a
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
18package com.android.mail.browse;
19
20import android.animation.AnimatorListenerAdapter;
21import android.app.FragmentManager;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.database.DataSetObservable;
25import android.database.DataSetObserver;
26import android.graphics.drawable.Drawable;
27import android.support.v4.view.ViewPager;
28import android.view.View;
29import android.view.ViewPropertyAnimator;
30
31import com.android.mail.R;
32import com.android.mail.graphics.PageMarginDrawable;
33import com.android.mail.providers.Account;
34import com.android.mail.providers.Conversation;
35import com.android.mail.providers.Folder;
36import com.android.mail.ui.AbstractActivityController;
37import com.android.mail.ui.ActivityController;
38import com.android.mail.ui.RestrictedActivity;
39import com.android.mail.utils.LogUtils;
40import 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 */
60public 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 animation
108     */
109    public void show(Account account, Folder folder, Conversation initialConversation,
110            boolean changeVisibility, AnimatorListenerAdapter pagerAnimationListener) {
111        mInitialConversationLoading = true;
112
113        if (mShown) {
114            LogUtils.d(LOG_TAG, "IN CPC.show, but already shown");
115            // optimize for the case where account+folder are the same, when we can just shift
116            // the existing pager to show the new conversation
117            // If in detached mode, don't do this optimization
118            if (mPagerAdapter != null && mPagerAdapter.matches(account, folder)
119                    && !mPagerAdapter.isDetached()) {
120                final int pos = mPagerAdapter.getConversationPosition(initialConversation);
121                if (pos >= 0) {
122                    mPager.setCurrentItem(pos);
123                    return;
124                }
125            }
126            // unable to shift, destroy existing state and fall through to normal startup
127            cleanup();
128        }
129
130        if (changeVisibility) {
131            // Reset alpha to 0 before animating/making it visible
132            mPager.setAlpha(0f);
133            mPager.setVisibility(View.VISIBLE);
134
135            final ViewPropertyAnimator pagerAnimator = mPager.animate().alpha(1f)
136                    .setDuration(SHOW_ANIMATION_DURATION);
137
138            // If we have any thing that listens in on pager show (see OnePaneController's
139            // showConversation(..) for an example), tack it on
140            if (pagerAnimationListener != null) {
141                pagerAnimator.setListener(pagerAnimationListener);
142            }
143        }
144
145        mPagerAdapter = new ConversationPagerAdapter(mPager.getContext(), mFragmentManager,
146                account, folder, initialConversation);
147        mPagerAdapter.setSingletonMode(ENABLE_SINGLETON_INITIAL_LOAD);
148        mPagerAdapter.setActivityController(mActivityController);
149        mPagerAdapter.setPager(mPager);
150        LogUtils.d(LOG_TAG, "IN CPC.show, adapter=%s", mPagerAdapter);
151
152        Utils.sConvLoadTimer.mark("pager init");
153        LogUtils.d(LOG_TAG, "init pager adapter, count=%d initialConv=%s adapter=%s",
154                mPagerAdapter.getCount(), initialConversation, mPagerAdapter);
155        mPager.setAdapter(mPagerAdapter);
156
157        if (!ENABLE_SINGLETON_INITIAL_LOAD) {
158            // FIXME: unnecessary to do this on restore. setAdapter will restore current position
159            final int initialPos = mPagerAdapter.getConversationPosition(initialConversation);
160            if (initialPos >= 0) {
161                LogUtils.d(LOG_TAG, "*** pager fragment init pos=%d", initialPos);
162                mPager.setCurrentItem(initialPos);
163            }
164        }
165        Utils.sConvLoadTimer.mark("pager setAdapter");
166
167        mShown = true;
168    }
169
170    /**
171     * Hide the pager and cancel any running/pending animation
172     * @param changeVisibility true if we need to make the pager disappear
173     */
174    public void hide(boolean changeVisibility) {
175        if (!mShown) {
176            LogUtils.d(LOG_TAG, "IN CPC.hide, but already hidden");
177            return;
178        }
179        mShown = false;
180
181        // Cancel any potential animations to avoid listener methods running when they shouldn't
182        mPager.animate().cancel();
183
184        if (changeVisibility) {
185            mPager.setVisibility(View.GONE);
186        }
187
188        LogUtils.d(LOG_TAG, "IN CPC.hide, clearing adapter and unregistering list observer");
189        mPager.setAdapter(null);
190        cleanup();
191    }
192
193    // Explicitly set the focus to the conversation pager, specifically the conv overlay.
194    public void focusPager() {
195        mPager.requestFocus();
196    }
197
198    public boolean isInitialConversationLoading() {
199        return mInitialConversationLoading;
200    }
201
202    public void onDestroy() {
203        // need to release resources before a configuration change kills the activity and controller
204        cleanup();
205    }
206
207    private void cleanup() {
208        if (mPagerAdapter != null) {
209            // stop observing the conversation list
210            mPagerAdapter.setActivityController(null);
211            mPagerAdapter.setPager(null);
212            mPagerAdapter = null;
213        }
214    }
215
216    public void onConversationSeen() {
217        if (mPagerAdapter == null) {
218            return;
219        }
220
221        // take the adapter out of singleton mode to begin loading the
222        // other non-visible conversations
223        if (mPagerAdapter.isSingletonMode()) {
224            LogUtils.i(LOG_TAG, "IN pager adapter, finished loading primary conversation," +
225                    " switching to cursor mode to load other conversations");
226            mPagerAdapter.setSingletonMode(false);
227        }
228
229        if (mInitialConversationLoading) {
230            mInitialConversationLoading = false;
231            mLoadedObservable.notifyChanged();
232        }
233    }
234
235    public void registerConversationLoadedObserver(DataSetObserver observer) {
236        mLoadedObservable.registerObserver(observer);
237    }
238
239    public void unregisterConversationLoadedObserver(DataSetObserver observer) {
240        mLoadedObservable.unregisterObserver(observer);
241    }
242
243    /**
244     * Stops listening to changes to the adapter. This must be followed immediately by
245     * {@link #hide(boolean)}.
246     */
247    public void stopListening() {
248        if (mPagerAdapter != null) {
249            mPagerAdapter.stopListening();
250        }
251    }
252
253    private void setupPageMargin(Context c) {
254        final TypedArray a = c.obtainStyledAttributes(new int[] {android.R.attr.listDivider});
255        final Drawable divider = a.getDrawable(0);
256        a.recycle();
257        final int padding = c.getResources().getDimensionPixelOffset(
258                R.dimen.conversation_page_gutter);
259        final Drawable gutterDrawable = new PageMarginDrawable(divider, padding, 0, padding, 0,
260                c.getResources().getColor(R.color.conversation_view_background_color));
261        mPager.setPageMargin(gutterDrawable.getIntrinsicWidth() + 2 * padding);
262        mPager.setPageMarginDrawable(gutterDrawable);
263    }
264
265}
266