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