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.ui; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.TimeInterpolator; 23 import android.app.Activity; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.util.AttributeSet; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.animation.AnimationUtils; 30 import android.widget.FrameLayout; 31 32 import com.android.mail.R; 33 import com.android.mail.ui.ViewMode.ModeChangeListener; 34 import com.android.mail.utils.LogUtils; 35 import com.android.mail.utils.Utils; 36 import com.android.mail.utils.ViewUtils; 37 import com.google.common.annotations.VisibleForTesting; 38 39 /** 40 * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet) 41 * activity, and the transitions between them. 42 * 43 * This is not intended to be a generic layout; it is specific to the {@code Fragment}s 44 * available in {@link MailActivity} and assumes their existence. It merely configures them 45 * according to the specific <i>modes</i> the {@link Activity} can be in. 46 * 47 * Currently, the layout differs in three dimensions: orientation, two aspects of view modes. 48 * This results in essentially three states: One where the folders are on the left and conversation 49 * list is on the right, and two states where the conversation list is on the left: one in which 50 * it's collapsed and another where it is not. 51 * 52 * In folder or conversation list view, conversations are hidden and folders and conversation lists 53 * are visible. This is the case in both portrait and landscape 54 * 55 * In Conversation List or Conversation View, folders are hidden, and conversation lists and 56 * conversation view is visible. This is the case in both portrait and landscape. 57 * 58 * In the Gmail source code, this was called TriStateSplitLayout 59 */ 60 final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { 61 62 private static final String LOG_TAG = "TwoPaneLayout"; 63 private static final long SLIDE_DURATION_MS = 300; 64 65 private final int mDrawerWidthMini; 66 private final int mDrawerWidthOpen; 67 private final double mConversationListWeight; 68 private final TimeInterpolator mSlideInterpolator; 69 /** 70 * If true, this layout group will treat the thread list and conversation view as full-width 71 * panes to switch between.<br> 72 * <br> 73 * If false, always show a conversation view right next to the conversation list. This view will 74 * also be populated (preview / "peek" mode) with a default conversation if none is selected by 75 * the user. 76 */ 77 private final boolean mListCollapsible; 78 79 /** 80 * The current mode that the tablet layout is in. This is a constant integer that holds values 81 * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}. 82 */ 83 private int mCurrentMode = ViewMode.UNKNOWN; 84 /** 85 * This mode represents the current positions of the three panes. This is split out from the 86 * current mode to give context to state transitions. 87 */ 88 private int mPositionedMode = ViewMode.UNKNOWN; 89 90 private TwoPaneController mController; 91 private LayoutListener mListener; 92 private boolean mIsSearchResult; 93 94 private View mMiscellaneousView; 95 private View mConversationView; 96 private View mFoldersView; 97 private View mListView; 98 99 public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane; 100 101 private final Runnable mTransitionCompleteRunnable = new Runnable() { 102 @Override 103 public void run() { 104 onTransitionComplete(); 105 } 106 }; 107 108 public TwoPaneLayout(Context context) { 109 this(context, null); 110 } 111 112 public TwoPaneLayout(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 115 final Resources res = getResources(); 116 117 // The conversation list might be visible now, depending on the layout: in portrait we 118 // don't show the conversation list, but in landscape we do. This information is stored 119 // in the constants 120 mListCollapsible = res.getBoolean(R.bool.list_collapsible); 121 122 mDrawerWidthMini = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_mini); 123 mDrawerWidthOpen = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_open); 124 125 mSlideInterpolator = AnimationUtils.loadInterpolator(context, 126 android.R.interpolator.decelerate_cubic); 127 128 final int convListWeight = res.getInteger(R.integer.conversation_list_weight); 129 final int convViewWeight = res.getInteger(R.integer.conversation_view_weight); 130 mConversationListWeight = (double) convListWeight 131 / (convListWeight + convViewWeight); 132 } 133 134 @Override 135 protected void onFinishInflate() { 136 super.onFinishInflate(); 137 138 mFoldersView = findViewById(R.id.drawer); 139 mListView = findViewById(R.id.conversation_list_pane); 140 mConversationView = findViewById(R.id.conversation_pane); 141 mMiscellaneousView = findViewById(MISCELLANEOUS_VIEW_ID); 142 143 // all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes 144 mCurrentMode = ViewMode.UNKNOWN; 145 mFoldersView.setVisibility(GONE); 146 mListView.setVisibility(GONE); 147 mConversationView.setVisibility(GONE); 148 mMiscellaneousView.setVisibility(GONE); 149 } 150 151 @VisibleForTesting 152 public void setController(TwoPaneController controller, boolean isSearchResult) { 153 mController = controller; 154 mListener = controller; 155 mIsSearchResult = isSearchResult; 156 157 ((ConversationViewFrame) mConversationView).setDownEventListener(mController); 158 } 159 160 @Override 161 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 162 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this); 163 setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec)); 164 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 165 } 166 167 @Override 168 protected void onLayout(boolean changed, int l, int t, int r, int b) { 169 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this); 170 positionPanes(getMeasuredWidth()); 171 super.onLayout(changed, l, t, r, b); 172 } 173 174 /** 175 * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes 176 * have the correct widths set for the current overall size and view mode. 177 * 178 * @param parentWidth this view's new width 179 */ 180 private void setupPaneWidths(int parentWidth) { 181 // only adjust the pane widths when my width changes 182 if (parentWidth != getMeasuredWidth()) { 183 final int convWidth = computeConversationWidth(parentWidth); 184 setPaneWidth(mMiscellaneousView, convWidth); 185 setPaneWidth(mConversationView, convWidth); 186 setPaneWidth(mListView, computeConversationListWidth(parentWidth)); 187 } 188 } 189 190 /** 191 * Positions the three sliding panes at the correct X offset (using {@link View#setX(float)}). 192 * When switching from list->conversation mode or vice versa, animate the change in X. 193 * 194 * @param width 195 */ 196 private void positionPanes(int width) { 197 final int convX, listX, foldersX; 198 final boolean isRtl = ViewUtils.isViewRtl(this); 199 200 final int foldersW = isDrawerOpen() ? mDrawerWidthOpen : mDrawerWidthMini; 201 final int listW = getPaneWidth(mListView); 202 203 boolean cvOnScreen = true; 204 if (!mListCollapsible) { 205 if (isRtl) { 206 foldersX = width - mDrawerWidthOpen; 207 listX = width - foldersW - listW; 208 convX = listX - getPaneWidth(mConversationView); 209 } else { 210 foldersX = 0; 211 listX = foldersW; 212 convX = listX + listW; 213 } 214 } else { 215 if (mController.getCurrentConversation() != null 216 && !mController.isCurrentConversationJustPeeking()) { 217 // CV mode 218 if (isRtl) { 219 convX = 0; 220 listX = getPaneWidth(mConversationView); 221 foldersX = listX + width - mDrawerWidthOpen; 222 } else { 223 convX = 0; 224 listX = -listW; 225 foldersX = listX - foldersW; 226 } 227 } else { 228 // TL mode 229 cvOnScreen = false; 230 if (isRtl) { 231 foldersX = width - mDrawerWidthOpen; 232 listX = width - foldersW - listW; 233 convX = listX - getPaneWidth(mConversationView); 234 } else { 235 foldersX = 0; 236 listX = foldersW; 237 convX = listX + listW; 238 } 239 } 240 } 241 242 animatePanes(foldersX, listX, convX); 243 244 // For views that are not on the screen, let's set their visibility for accessibility. 245 mFoldersView.setVisibility(foldersX >= 0 ? VISIBLE : INVISIBLE); 246 mListView.setVisibility(listX >= 0 ? VISIBLE : INVISIBLE); 247 mConversationView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); 248 mMiscellaneousView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); 249 250 mPositionedMode = mCurrentMode; 251 } 252 253 private final AnimatorListenerAdapter mPaneAnimationListener = new AnimatorListenerAdapter() { 254 @Override 255 public void onAnimationEnd(Animator animation) { 256 useHardwareLayer(false); 257 onTransitionComplete(); 258 } 259 @Override 260 public void onAnimationCancel(Animator animation) { 261 useHardwareLayer(false); 262 } 263 }; 264 265 private void animatePanes(int foldersX, int listX, int convX) { 266 // If positioning has not yet happened, we don't need to animate panes into place. 267 // This happens on first layout, rotate, and when jumping straight to a conversation from 268 // a view intent. 269 if (mPositionedMode == ViewMode.UNKNOWN) { 270 mConversationView.setX(convX); 271 mMiscellaneousView.setX(convX); 272 mListView.setX(listX); 273 mFoldersView.setX(foldersX); 274 275 // listeners need to know that the "transition" is complete, even if one is not run. 276 // defer notifying listeners because we're in a layout pass, and they might do layout. 277 post(mTransitionCompleteRunnable); 278 return; 279 } 280 281 useHardwareLayer(true); 282 283 if (ViewMode.isAdMode(mCurrentMode)) { 284 mMiscellaneousView.animate().x(convX); 285 } else { 286 mConversationView.animate().x(convX); 287 } 288 289 mFoldersView.animate().x(foldersX); 290 mListView.animate() 291 .x(listX) 292 .setListener(mPaneAnimationListener); 293 configureAnimations(mConversationView, mFoldersView, mListView, mMiscellaneousView); 294 } 295 296 private void configureAnimations(View... views) { 297 for (View v : views) { 298 v.animate() 299 .setInterpolator(mSlideInterpolator) 300 .setDuration(SLIDE_DURATION_MS); 301 } 302 } 303 304 private void useHardwareLayer(boolean useHardware) { 305 final int layerType = useHardware ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE; 306 mFoldersView.setLayerType(layerType, null); 307 mListView.setLayerType(layerType, null); 308 mConversationView.setLayerType(layerType, null); 309 mMiscellaneousView.setLayerType(layerType, null); 310 if (useHardware) { 311 // these buildLayer calls are safe because layout is the only way we get here 312 // (i.e. these views must already be attached) 313 mFoldersView.buildLayer(); 314 mListView.buildLayer(); 315 mConversationView.buildLayer(); 316 mMiscellaneousView.buildLayer(); 317 } 318 } 319 320 private void onTransitionComplete() { 321 if (mController.isDestroyed()) { 322 // quit early if the hosting activity was destroyed before the animation finished 323 LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early"); 324 return; 325 } 326 327 switch (mCurrentMode) { 328 case ViewMode.CONVERSATION: 329 case ViewMode.SEARCH_RESULTS_CONVERSATION: 330 dispatchConversationVisibilityChanged(true); 331 dispatchConversationListVisibilityChange(!isConversationListCollapsed()); 332 333 break; 334 case ViewMode.CONVERSATION_LIST: 335 case ViewMode.SEARCH_RESULTS_LIST: 336 dispatchConversationVisibilityChanged(false); 337 dispatchConversationListVisibilityChange(true); 338 339 break; 340 case ViewMode.AD: 341 dispatchConversationVisibilityChanged(false); 342 dispatchConversationListVisibilityChange(!isConversationListCollapsed()); 343 344 break; 345 default: 346 break; 347 } 348 } 349 350 /** 351 * Computes the width of the conversation list in stable state of the current mode. 352 */ 353 public int computeConversationListWidth() { 354 return computeConversationListWidth(getMeasuredWidth()); 355 } 356 357 /** 358 * Computes the width of the conversation list in stable state of the current mode. 359 */ 360 private int computeConversationListWidth(int parentWidth) { 361 final int availWidth = parentWidth - mDrawerWidthMini; 362 return mListCollapsible ? availWidth : (int) (availWidth * mConversationListWeight); 363 } 364 365 public int computeConversationWidth() { 366 return computeConversationWidth(getMeasuredWidth()); 367 } 368 369 /** 370 * Computes the width of the conversation pane in stable state of the 371 * current mode. 372 */ 373 private int computeConversationWidth(int parentWidth) { 374 return mListCollapsible ? parentWidth : 375 parentWidth - computeConversationListWidth(parentWidth) - mDrawerWidthMini; 376 } 377 378 private void dispatchConversationListVisibilityChange(boolean visible) { 379 if (mListener != null) { 380 mListener.onConversationListVisibilityChanged(visible); 381 } 382 } 383 384 private void dispatchConversationVisibilityChanged(boolean visible) { 385 if (mListener != null) { 386 mListener.onConversationVisibilityChanged(visible); 387 } 388 } 389 390 // does not apply to drawer children. will return zero for those. 391 private int getPaneWidth(View pane) { 392 return pane.getLayoutParams().width; 393 } 394 395 private boolean isDrawerOpen() { 396 return mController != null && mController.isDrawerOpen(); 397 } 398 399 /** 400 * @return Whether or not the conversation list is visible on screen. 401 */ 402 @Deprecated 403 public boolean isConversationListCollapsed() { 404 return !ViewMode.isListMode(mCurrentMode) && mListCollapsible; 405 } 406 407 @Override 408 public void onViewModeChanged(int newMode) { 409 // make all initially GONE panes visible only when the view mode is first determined 410 if (mCurrentMode == ViewMode.UNKNOWN) { 411 mFoldersView.setVisibility(VISIBLE); 412 mListView.setVisibility(VISIBLE); 413 } 414 415 if (ViewMode.isAdMode(newMode)) { 416 mMiscellaneousView.setVisibility(VISIBLE); 417 mConversationView.setVisibility(GONE); 418 } else { 419 mConversationView.setVisibility(VISIBLE); 420 mMiscellaneousView.setVisibility(GONE); 421 } 422 423 // detach the pager immediately from its data source (to prevent processing updates) 424 if (ViewMode.isConversationMode(mCurrentMode)) { 425 mController.disablePagerUpdates(); 426 } 427 428 mCurrentMode = newMode; 429 LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode); 430 431 // do all the real work in onMeasure/onLayout, when panes are sized and positioned for the 432 // current width/height anyway 433 requestLayout(); 434 } 435 436 public boolean isModeChangePending() { 437 return mPositionedMode != mCurrentMode; 438 } 439 440 private void setPaneWidth(View pane, int w) { 441 final ViewGroup.LayoutParams lp = pane.getLayoutParams(); 442 if (lp.width == w) { 443 return; 444 } 445 lp.width = w; 446 pane.setLayoutParams(lp); 447 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 448 final String s; 449 if (pane == mFoldersView) { 450 s = "folders"; 451 } else if (pane == mListView) { 452 s = "conv-list"; 453 } else if (pane == mConversationView) { 454 s = "conv-view"; 455 } else if (pane == mMiscellaneousView) { 456 s = "misc-view"; 457 } else { 458 s = "???:" + pane; 459 } 460 LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s); 461 } 462 } 463 464 public boolean shouldShowPreviewPanel() { 465 return !mListCollapsible; 466 } 467 } 468