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 ConversationPagerController(RestrictedActivity activity, ActivityController controller)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 */ show(Account account, Folder folder, Conversation initialConversation, boolean changeVisibility, AnimatorListenerAdapter pagerAnimationListener)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 */ hide(boolean changeVisibility)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 */ killRestoredFragments()206 public void killRestoredFragments() { 207 mPagerAdapter.killRestoredFragments(); 208 } 209 210 // Explicitly set the focus to the conversation pager, specifically the conv overlay. focusPager()211 public void focusPager() { 212 mPager.requestFocus(); 213 } 214 setCurrentItem(int pos)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 isInitialConversationLoading()223 public boolean isInitialConversationLoading() { 224 return mInitialConversationLoading; 225 } 226 onDestroy()227 public void onDestroy() { 228 // need to release resources before a configuration change kills the activity and controller 229 cleanup(); 230 } 231 cleanup()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 onConversationSeen()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 registerConversationLoadedObserver(DataSetObserver observer)260 public void registerConversationLoadedObserver(DataSetObserver observer) { 261 mLoadedObservable.registerObserver(observer); 262 } 263 unregisterConversationLoadedObserver(DataSetObserver observer)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 */ stopListening()272 public void stopListening() { 273 if (mPagerAdapter != null) { 274 mPagerAdapter.stopListening(); 275 } 276 } 277 setupPageMargin(Context c)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