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 java.util.List; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.TimeInterpolator; 25 import android.animation.ValueAnimator; 26 import android.app.Activity; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.Canvas; 30 import android.graphics.drawable.Drawable; 31 import androidx.annotation.NonNull; 32 import android.util.AttributeSet; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewPropertyAnimator; 37 import android.view.animation.AnimationUtils; 38 import android.widget.FrameLayout; 39 40 import com.android.mail.R; 41 import com.android.mail.ui.ViewMode.ModeChangeListener; 42 import com.android.mail.utils.LogUtils; 43 import com.android.mail.utils.Utils; 44 import com.android.mail.utils.ViewUtils; 45 import com.google.common.annotations.VisibleForTesting; 46 import com.google.common.collect.Lists; 47 48 /** 49 * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet) 50 * activity, and the transitions between them. 51 * 52 * This is not intended to be a generic layout; it is specific to the {@code Fragment}s 53 * available in {@link MailActivity} and assumes their existence. It merely configures them 54 * according to the specific <i>modes</i> the {@link Activity} can be in. 55 * 56 * Currently, the layout differs in three dimensions: orientation, two aspects of view modes. 57 * This results in essentially three states: One where the folders are on the left and conversation 58 * list is on the right, and two states where the conversation list is on the left: one in which 59 * it's collapsed and another where it is not. 60 * 61 * In folder or conversation list view, conversations are hidden and folders and conversation lists 62 * are visible. This is the case in both portrait and landscape 63 * 64 * In Conversation List or Conversation View, folders are hidden, and conversation lists and 65 * conversation view is visible. This is the case in both portrait and landscape. 66 * 67 * In the Gmail source code, this was called TriStateSplitLayout 68 */ 69 final class TwoPaneLayout extends FrameLayout implements ModeChangeListener, 70 GmailDragHelper.GmailDragHelperCallback { 71 public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane; 72 public static final long SLIDE_DURATION_MS = 300; 73 74 private static final String LOG_TAG = "TwoPaneLayout"; 75 76 private final int mDrawerWidthMini; 77 private final int mDrawerWidthOpen; 78 private final int mDrawerWidthDelta; 79 private final double mConversationListWeight; 80 private final TimeInterpolator mSlideInterpolator; 81 /** 82 * If true, always show a conversation view right next to the conversation list. This view will 83 * also be populated (preview / "peek" mode) with a default conversation if none is selected by 84 * the user.<br> 85 * <br> 86 * If false, this layout group will treat the thread list and conversation view as full-width 87 * panes to switch between. 88 */ 89 private final boolean mShouldShowPreviewPanel; 90 91 /** 92 * The current mode that the tablet layout is in. This is a constant integer that holds values 93 * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}. 94 */ 95 private int mCurrentMode = ViewMode.UNKNOWN; 96 /** 97 * This is a copy of {@link #mCurrentMode} that layout/positioning/animating code uses to 98 * compare to the 'new' current mode, to avoid unnecessarily calculation. 99 */ 100 private int mTranslatedMode = ViewMode.UNKNOWN; 101 102 private TwoPaneController mController; 103 private LayoutListener mListener; 104 // Drag helper for capturing drag over the list pane 105 private final GmailDragHelper mDragHelper; 106 private int mCurrentDragMode; 107 // mXThreshold is only used for dragging the mini-drawer out. This optional parameter allows for 108 // the drag to only initiate once it hits the edge of the mini-drawer so that the edge follows 109 // the drag. 110 private Float mXThreshold; 111 112 private View mFoldersView; 113 private View mListView; 114 // content view encompasses both conversation and ad view. 115 private View mConversationFrame; 116 117 // These two views get switched in/out depending on the view mode. 118 private View mConversationView; 119 private View mMiscellaneousView; 120 121 private boolean mIsRtl; 122 123 // These are computed when the base layout changes. 124 private int mFoldersLeft; 125 private int mFoldersRight; 126 private int mListLeft; 127 private int mListRight; 128 private int mConvLeft; 129 private int mConvRight; 130 131 private final Drawable mShadowDrawable; 132 private final int mShadowMinWidth; 133 134 private final List<Runnable> mTransitionCompleteJobs = Lists.newArrayList(); 135 private final PaneAnimationListener mPaneAnimationListener = new PaneAnimationListener(); 136 137 // Keep track if we are tracking the current touch events 138 private boolean mShouldInterceptCurrentTouch; 139 140 public interface ConversationListLayoutListener { 141 /** 142 * Used for two-pane landscape layout positioning when other views need to align themselves 143 * to the list view. Should be called only in tablet landscape mode! 144 * @param xEnd the ending x coordinate of the list view 145 * @param drawerOpen 146 */ onConversationListLayout(int xEnd, boolean drawerOpen)147 void onConversationListLayout(int xEnd, boolean drawerOpen); 148 } 149 150 // Responsible for invalidating the shadow region only to minimize drawing overhead (and jank) 151 // Coordinated with ListView animation to ensure shadow and list slide together. 152 private final ValueAnimator.AnimatorUpdateListener mListViewAnimationListener = 153 new ValueAnimator.AnimatorUpdateListener() { 154 @Override 155 public void onAnimationUpdate(ValueAnimator valueAnimator) { 156 if (mIsRtl) { 157 // Get the right edge of list and use as left edge coord for shadow 158 final int leftEdgeCoord = (int) mListView.getX() + mListView.getWidth(); 159 invalidate(leftEdgeCoord, 0, leftEdgeCoord + mShadowMinWidth, 160 getBottom()); 161 } else { 162 // Get the left edge of list and use as right edge coord for shadow 163 final int rightEdgeCoord = (int) mListView.getX(); 164 invalidate(rightEdgeCoord - mShadowMinWidth, 0, rightEdgeCoord, 165 getBottom()); 166 } 167 } 168 }; 169 TwoPaneLayout(Context context)170 public TwoPaneLayout(Context context) { 171 this(context, null); 172 } 173 TwoPaneLayout(Context context, AttributeSet attrs)174 public TwoPaneLayout(Context context, AttributeSet attrs) { 175 super(context, attrs); 176 177 final Resources res = getResources(); 178 179 // The conversation list might be visible now, depending on the layout: in portrait we 180 // don't show the conversation list, but in landscape we do. This information is stored 181 // in the constants 182 mShouldShowPreviewPanel = res.getBoolean(R.bool.is_tablet_landscape); 183 184 mDrawerWidthMini = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_mini); 185 mDrawerWidthOpen = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_open); 186 mDrawerWidthDelta = mDrawerWidthOpen - mDrawerWidthMini; 187 188 mSlideInterpolator = AnimationUtils.loadInterpolator(context, 189 android.R.interpolator.decelerate_cubic); 190 191 final int convListWeight = res.getInteger(R.integer.conversation_list_weight); 192 final int convViewWeight = res.getInteger(R.integer.conversation_view_weight); 193 mConversationListWeight = (double) convListWeight 194 / (convListWeight + convViewWeight); 195 196 mShadowDrawable = getResources().getDrawable(R.drawable.ic_vertical_shadow_start_4dp); 197 mShadowMinWidth = mShadowDrawable.getMinimumWidth(); 198 199 mDragHelper = new GmailDragHelper(context, this); 200 } 201 202 @Override toString()203 public String toString() { 204 final StringBuilder sb = new StringBuilder(super.toString()); 205 sb.append("{mTranslatedMode="); 206 sb.append(mTranslatedMode); 207 sb.append(" mCurrDragMode="); 208 sb.append(mCurrentDragMode); 209 sb.append(" mShouldInterceptCurrentTouch="); 210 sb.append(mShouldInterceptCurrentTouch); 211 sb.append(" mTransitionCompleteJobs="); 212 sb.append(mTransitionCompleteJobs); 213 sb.append("}"); 214 return sb.toString(); 215 } 216 217 @Override dispatchDraw(@onNull Canvas canvas)218 protected void dispatchDraw(@NonNull Canvas canvas) { 219 // Draw children/update the canvas first. 220 super.dispatchDraw(canvas); 221 222 if (ViewUtils.isViewRtl(this)) { 223 // Get the right edge of list and use as left edge coord for shadow 224 final int leftEdgeCoord = (int) mListView.getX() + mListView.getWidth(); 225 mShadowDrawable.setBounds(leftEdgeCoord, 0, leftEdgeCoord + mShadowMinWidth, 226 mListView.getBottom()); 227 } else { 228 // Get the left edge of list and use as right edge coord for shadow 229 final int rightEdgeCoord = (int) mListView.getX(); 230 mShadowDrawable.setBounds(rightEdgeCoord - mShadowMinWidth, 0, rightEdgeCoord, 231 mListView.getBottom()); 232 } 233 234 mShadowDrawable.draw(canvas); 235 } 236 237 @Override onFinishInflate()238 protected void onFinishInflate() { 239 super.onFinishInflate(); 240 241 mFoldersView = findViewById(R.id.drawer); 242 mListView = findViewById(R.id.conversation_list_pane); 243 mConversationFrame = findViewById(R.id.conversation_frame); 244 245 mConversationView = mConversationFrame.findViewById(R.id.conversation_pane); 246 mMiscellaneousView = mConversationFrame.findViewById(MISCELLANEOUS_VIEW_ID); 247 248 // all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes 249 mCurrentMode = ViewMode.UNKNOWN; 250 mFoldersView.setVisibility(GONE); 251 mListView.setVisibility(GONE); 252 mConversationView.setVisibility(GONE); 253 mMiscellaneousView.setVisibility(GONE); 254 } 255 256 @VisibleForTesting setController(TwoPaneController controller)257 public void setController(TwoPaneController controller) { 258 mController = controller; 259 mListener = controller; 260 261 ((ConversationViewFrame) mConversationFrame).setDownEventListener(mController); 262 } 263 264 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)265 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 266 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this); 267 setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec)); 268 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 269 } 270 271 @Override onLayout(boolean changed, int l, int t, int r, int b)272 protected void onLayout(boolean changed, int l, int t, int r, int b) { 273 LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this); 274 super.onLayout(changed, l, t, r, b); 275 mIsRtl = ViewUtils.isViewRtl(this); 276 277 // Layout only positions the children views at their default locations, and any pane 278 // movement is done via translation rather than layout. 279 // Thus, we should only re-compute the overall layout on changed. 280 if (changed) { 281 final int width = getMeasuredWidth(); 282 computePanePositions(width); 283 284 // If the view mode is different from positions and we are computing pane position, then 285 // set the default translation for portrait mode. 286 // This is necessary because on rotation we get onViewModeChanged() call before 287 // onMeasure actually happens, so we often do not know the width to translate to. This 288 // call ensures that the default translation values always correspond to the view mode. 289 if (mTranslatedMode != mCurrentMode && !mShouldShowPreviewPanel) { 290 translateDueToViewMode(width, false /* animate */); 291 } else { 292 onTransitionComplete(); 293 } 294 } 295 296 // Layout the children views 297 final int bottom = getMeasuredHeight(); 298 mFoldersView.layout(mFoldersLeft, 0, mFoldersRight, bottom); 299 mListView.layout(mListLeft, 0, mListRight, bottom); 300 mConversationFrame.layout(mConvLeft, 0, mConvRight, bottom); 301 } 302 303 /** 304 * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes 305 * have the correct widths set for the current overall size and view mode. 306 * 307 * @param parentWidth this view's new width 308 */ setupPaneWidths(int parentWidth)309 private void setupPaneWidths(int parentWidth) { 310 // only adjust the pane widths when my width changes 311 if (parentWidth != getMeasuredWidth()) { 312 final int convWidth = computeConversationWidth(parentWidth); 313 setPaneWidth(mConversationFrame, convWidth); 314 setPaneWidth(mListView, computeConversationListWidth(parentWidth)); 315 } 316 } 317 318 /** 319 * Compute the default base location of each pane and save it in their corresponding 320 * instance variables. onLayout will then layout each child accordingly. 321 * @param width the available width to layout the children panes 322 */ computePanePositions(int width)323 private void computePanePositions(int width) { 324 // Always compute the base value as closed drawer 325 final int foldersW = mDrawerWidthMini; 326 final int listW = getPaneWidth(mListView); 327 final int convW = getPaneWidth(mConversationFrame); 328 329 // Compute default pane positions 330 if (mIsRtl) { 331 mFoldersLeft = width - mDrawerWidthOpen; 332 mListLeft = width - foldersW- listW; 333 mConvLeft = mListLeft - convW; 334 } else { 335 mFoldersLeft = 0; 336 mListLeft = foldersW; 337 mConvLeft = mListLeft + listW; 338 } 339 mFoldersRight = mFoldersLeft + mDrawerWidthOpen; 340 mListRight = mListLeft + listW; 341 mConvRight = mConvLeft + convW; 342 } 343 344 /** 345 * Animate the drawer to the provided state. 346 */ animateDrawer(boolean minimized)347 public void animateDrawer(boolean minimized) { 348 // In rtl the drawer opens in the negative direction. 349 final int openDrawerDelta = mIsRtl ? -mDrawerWidthDelta : mDrawerWidthDelta; 350 translatePanes(minimized ? 0 : openDrawerDelta, 0 /* drawerDeltaX */, true /* animate */); 351 } 352 353 /** 354 * Translate the panes to their ending positions, can choose to either animate the translation 355 * or let it be instantaneous. 356 * @param deltaX The ending translationX to translate all of the panes except for drawer. 357 * @param drawerDeltaX the ending translationX to translate the drawer. This is necessary 358 * because in landscape mode the drawer doesn't actually move and rest of the panes simply 359 * move to cover/uncover the drawer. The drawer only moves in portrait from TL -> CV. 360 * @param animate whether to animate the translation or not. 361 */ translatePanes(float deltaX, float drawerDeltaX, boolean animate)362 private void translatePanes(float deltaX, float drawerDeltaX, boolean animate) { 363 if (animate) { 364 animatePanes(deltaX, drawerDeltaX); 365 } else { 366 mFoldersView.setTranslationX(drawerDeltaX); 367 mListView.setTranslationX(deltaX); 368 mConversationFrame.setTranslationX(deltaX); 369 } 370 } 371 372 /** 373 * Animate the panes' translationX to their corresponding deltas. Refer to 374 * {@link TwoPaneLayout#translatePanes(float, float, boolean)} for explanation on deltas. 375 */ animatePanes(float deltaX, float drawerDeltaX)376 private void animatePanes(float deltaX, float drawerDeltaX) { 377 mConversationFrame.animate().translationX(deltaX); 378 379 final ViewPropertyAnimator listAnimation = mListView.animate() 380 .translationX(deltaX) 381 .setListener(mPaneAnimationListener); 382 383 mFoldersView.animate().translationX(drawerDeltaX); 384 385 // If we're running K+, we can use the update listener to transition the list's left shadow 386 // and set different update listeners based on rtl to avoid doing a check on every frame 387 if (Utils.isRunningKitkatOrLater()) { 388 listAnimation.setUpdateListener(mListViewAnimationListener); 389 } 390 391 configureAnimations(mFoldersView, mListView, mConversationFrame); 392 } 393 configureAnimations(View... views)394 private void configureAnimations(View... views) { 395 for (View v : views) { 396 v.animate() 397 .setInterpolator(mSlideInterpolator) 398 .setDuration(SLIDE_DURATION_MS); 399 } 400 } 401 402 /** 403 * Adjusts the visibility of each pane before and after a transition. After the transition, 404 * any invisible panes should be marked invisible. But visible panes should not wait for the 405 * transition to finish-- they should be marked visible immediately. 406 */ adjustPaneVisibility(final boolean folderVisible, final boolean listVisible, final boolean cvVisible)407 private void adjustPaneVisibility(final boolean folderVisible, final boolean listVisible, 408 final boolean cvVisible) { 409 applyPaneVisibility(VISIBLE, folderVisible, listVisible, cvVisible); 410 mTransitionCompleteJobs.add(new Runnable() { 411 @Override 412 public void run() { 413 applyPaneVisibility(INVISIBLE, !folderVisible, !listVisible, !cvVisible); 414 } 415 }); 416 } 417 applyPaneVisibility(int visibility, boolean applyToFolders, boolean applyToList, boolean applyToCV)418 private void applyPaneVisibility(int visibility, boolean applyToFolders, boolean applyToList, 419 boolean applyToCV) { 420 if (applyToFolders) { 421 mFoldersView.setVisibility(visibility); 422 } 423 if (applyToList) { 424 mListView.setVisibility(visibility); 425 } 426 if (applyToCV) { 427 if (mConversationView.getVisibility() != GONE) { 428 mConversationView.setVisibility(visibility); 429 } 430 if (mMiscellaneousView.getVisibility() != GONE) { 431 mMiscellaneousView.setVisibility(visibility); 432 } 433 } 434 } 435 onTransitionComplete()436 private void onTransitionComplete() { 437 if (mController.isDestroyed()) { 438 // quit early if the hosting activity was destroyed before the animation finished 439 LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early"); 440 return; 441 } 442 443 for (Runnable job : mTransitionCompleteJobs) { 444 job.run(); 445 } 446 mTransitionCompleteJobs.clear(); 447 448 // We finished transitioning into the new mode. 449 mTranslatedMode = mCurrentMode; 450 451 // Notify conversation list layout listeners of position change. 452 final int xEnd = mIsRtl ? mListLeft : mListRight; 453 if (mShouldShowPreviewPanel && xEnd != 0) { 454 final List<ConversationListLayoutListener> layoutListeners = 455 mController.getConversationListLayoutListeners(); 456 for (ConversationListLayoutListener listener : layoutListeners) { 457 listener.onConversationListLayout(xEnd, isDrawerOpen()); 458 } 459 } 460 461 dispatchVisibilityChanged(); 462 } 463 dispatchVisibilityChanged()464 private void dispatchVisibilityChanged() { 465 switch (mCurrentMode) { 466 case ViewMode.CONVERSATION: 467 case ViewMode.SEARCH_RESULTS_CONVERSATION: 468 dispatchConversationVisibilityChanged(true); 469 dispatchConversationListVisibilityChange(!isConversationListCollapsed()); 470 471 break; 472 case ViewMode.CONVERSATION_LIST: 473 case ViewMode.SEARCH_RESULTS_LIST: 474 case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION: 475 dispatchConversationVisibilityChanged(false); 476 dispatchConversationListVisibilityChange(true); 477 478 break; 479 case ViewMode.AD: 480 dispatchConversationVisibilityChanged(false); 481 dispatchConversationListVisibilityChange(!isConversationListCollapsed()); 482 483 break; 484 default: 485 break; 486 } 487 } 488 489 @Override onDragStarted()490 public void onDragStarted() { 491 mController.onDrawerDragStarted(); 492 } 493 494 @Override onDrag(float deltaX)495 public void onDrag(float deltaX) { 496 // We use percentDragged here because deltaX is relative to the current drag and not 497 // relative to the start/end positions of the drawer. 498 final float percentDragged = computeDragPercentage(deltaX); 499 // Again, in RTL the drawer opens in the negative direction, so need to inverse the delta. 500 final float translationX = percentDragged * 501 (mIsRtl ? -mDrawerWidthDelta : mDrawerWidthDelta); 502 translatePanes(translationX, 0 /* drawerDeltaX */, false /* animate */); 503 mController.onDrawerDrag(percentDragged); 504 // Invalidate the entire drawers region to ensure that we don't get the "ghosts" of the 505 // fake shadow for pre-L. 506 if (mIsRtl) { 507 invalidate((int) mListView.getX() + mListView.getWidth(), 0, 508 (int) mFoldersView.getX() + mFoldersView.getWidth(), getBottom()); 509 } else { 510 invalidate((int) mFoldersView.getX(), 0, (int) mListView.getX(), getBottom()); 511 } 512 } 513 514 @Override onDragEnded(float deltaX, float velocityX, boolean isFling)515 public void onDragEnded(float deltaX, float velocityX, boolean isFling) { 516 if (isFling) { 517 // Drawer is minimized if velocity is toward the left or it's rtl. 518 if (mIsRtl) { 519 mController.onDrawerDragEnded(velocityX >= 0); 520 } else { 521 mController.onDrawerDragEnded(velocityX < 0); 522 } 523 } else { 524 // If we got past the half-way mark, animate it rest of the way. 525 mController.onDrawerDragEnded(computeDragPercentage(deltaX) < 0.5f); 526 } 527 } 528 529 /** 530 * Given the delta that user moved, return a percentage that signifies the drag progress. 531 * @param deltaX the distance dragged. 532 * @return percent dragged (values range from 0 to 1). 533 * 0 means a fully closed drawer, and 1 means a fully open drawer. 534 */ 535 private float computeDragPercentage(float deltaX) { 536 final float percent; 537 if (mIsRtl) { 538 if (mCurrentDragMode == GmailDragHelper.CAPTURE_LEFT_TO_RIGHT) { 539 percent = (mDrawerWidthDelta - deltaX) / mDrawerWidthDelta; 540 } else { 541 percent = -deltaX / mDrawerWidthDelta; 542 } 543 } else { 544 if (mCurrentDragMode == GmailDragHelper.CAPTURE_LEFT_TO_RIGHT) { 545 percent = deltaX / mDrawerWidthDelta; 546 } else { 547 percent = (mDrawerWidthDelta + deltaX) / mDrawerWidthDelta; 548 } 549 } 550 551 return percent < 0 ? 0 : percent > 1 ? 1 : percent; 552 } 553 554 @Override 555 public boolean onInterceptTouchEvent(MotionEvent ev) { 556 if (isModeChangePending()) { 557 return false; 558 } 559 560 switch (ev.getAction()) { 561 case MotionEvent.ACTION_DOWN: 562 final float x = ev.getX(); 563 final boolean drawerOpen = isDrawerOpen(); 564 if (drawerOpen) { 565 // Only start intercepting if the down event is inside the list pane or in 566 // landscape conv pane 567 final float left; 568 final float right; 569 if (mShouldShowPreviewPanel) { 570 final boolean isAdMode = ViewMode.isAdMode(mCurrentMode); 571 left = mIsRtl ? mConversationFrame.getX() : mListView.getX(); 572 right = mIsRtl ? mListView.getX() + mListView.getWidth() : 573 mConversationFrame.getX() + mConversationFrame.getWidth(); 574 } else { 575 left = mListView.getX(); 576 right = left + mListView.getWidth(); 577 } 578 579 // Set the potential start drag states 580 mShouldInterceptCurrentTouch = x >= left && x <= right; 581 mXThreshold = null; 582 if (mIsRtl) { 583 mCurrentDragMode = GmailDragHelper.CAPTURE_LEFT_TO_RIGHT; 584 } else { 585 mCurrentDragMode = GmailDragHelper.CAPTURE_RIGHT_TO_LEFT; 586 } 587 } else { 588 // Only capture within the mini drawer 589 final float foldersX1 = mIsRtl ? mFoldersView.getX() + mDrawerWidthDelta : 590 mFoldersView.getX(); 591 final float foldersX2 = foldersX1 + mDrawerWidthMini; 592 593 // Set the potential start drag states 594 mShouldInterceptCurrentTouch = x >= foldersX1 && x <= foldersX2; 595 if (mIsRtl) { 596 mCurrentDragMode = GmailDragHelper.CAPTURE_RIGHT_TO_LEFT; 597 mXThreshold = (float) mFoldersLeft + mDrawerWidthDelta; 598 } else { 599 mCurrentDragMode = GmailDragHelper.CAPTURE_LEFT_TO_RIGHT; 600 mXThreshold = (float) mFoldersLeft + mDrawerWidthMini; 601 } 602 } 603 break; 604 } 605 return mShouldInterceptCurrentTouch && 606 mDragHelper.processTouchEvent(ev, mCurrentDragMode, mXThreshold); 607 } 608 609 @Override 610 public boolean onTouchEvent(@NonNull MotionEvent ev) { 611 if (mShouldInterceptCurrentTouch) { 612 mDragHelper.processTouchEvent(ev, mCurrentDragMode, mXThreshold); 613 return true; 614 } 615 return super.onTouchEvent(ev); 616 } 617 618 /** 619 * Computes the width of the conversation list in stable state of the current mode. 620 */ 621 public int computeConversationListWidth() { 622 return computeConversationListWidth(getMeasuredWidth()); 623 } 624 625 /** 626 * Computes the width of the conversation list in stable state of the current mode. 627 */ 628 private int computeConversationListWidth(int parentWidth) { 629 final int availWidth = parentWidth - mDrawerWidthMini; 630 return mShouldShowPreviewPanel ? (int) (availWidth * mConversationListWeight) : availWidth; 631 } 632 633 public int computeConversationWidth() { 634 return computeConversationWidth(getMeasuredWidth()); 635 } 636 637 /** 638 * Computes the width of the conversation pane in stable state of the 639 * current mode. 640 */ 641 private int computeConversationWidth(int parentWidth) { 642 return mShouldShowPreviewPanel ? parentWidth - computeConversationListWidth(parentWidth) 643 - mDrawerWidthMini : parentWidth; 644 } 645 646 private void dispatchConversationListVisibilityChange(boolean visible) { 647 if (mListener != null) { 648 mListener.onConversationListVisibilityChanged(visible); 649 } 650 } 651 652 private void dispatchConversationVisibilityChanged(boolean visible) { 653 if (mListener != null) { 654 mListener.onConversationVisibilityChanged(visible); 655 } 656 } 657 658 // does not apply to drawer children. will return zero for those. 659 private int getPaneWidth(View pane) { 660 return pane.getLayoutParams().width; 661 } 662 663 private boolean isDrawerOpen() { 664 return mController != null && mController.isDrawerOpen(); 665 } 666 667 /** 668 * @return Whether or not the conversation list is visible on screen. 669 */ 670 @Deprecated 671 public boolean isConversationListCollapsed() { 672 return !ViewMode.isListMode(mCurrentMode) && !mShouldShowPreviewPanel; 673 } 674 675 @Override 676 public void onViewModeChanged(int newMode) { 677 // make all initially GONE panes visible only when the view mode is first determined 678 if (mCurrentMode == ViewMode.UNKNOWN) { 679 mFoldersView.setVisibility(VISIBLE); 680 mListView.setVisibility(VISIBLE); 681 } 682 683 if (ViewMode.isAdMode(newMode)) { 684 mMiscellaneousView.setVisibility(VISIBLE); 685 mConversationView.setVisibility(GONE); 686 } else { 687 mConversationView.setVisibility(VISIBLE); 688 mMiscellaneousView.setVisibility(GONE); 689 } 690 691 // detach the pager immediately from its data source (to prevent processing updates) 692 if (ViewMode.isConversationMode(mCurrentMode)) { 693 mController.disablePagerUpdates(); 694 } 695 696 // notify of list visibility change up-front when going to list mode 697 // (so the transition runs with the full TL in view) 698 if (newMode == ViewMode.CONVERSATION_LIST) { 699 dispatchConversationListVisibilityChange(true); 700 } 701 702 mCurrentMode = newMode; 703 LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode); 704 705 // If this is the first view mode change, we can't perform any translations yet because 706 // the view doesn't have any measurements. 707 final int width = getMeasuredWidth(); 708 if (width != 0) { 709 // On view mode changes, ensure that we animate the panes & notify visibility changes. 710 if (mShouldShowPreviewPanel) { 711 onTransitionComplete(); 712 } else { 713 translateDueToViewMode(width, true /* animate */); 714 } 715 } 716 } 717 718 /** 719 * This is only called in portrait mode since only view mode changes in portrait mode affect 720 * the pane positioning. This should be called after every view mode change to ensure that 721 * each pane are in their corresponding locations based on the view mode. 722 * @param width the available width to position the panes. 723 * @param animate whether to animate the translation or not. 724 */ 725 private void translateDueToViewMode(int width, boolean animate) { 726 // Need to translate for CV mode 727 if (ViewMode.isConversationMode(mCurrentMode) || ViewMode.isAdMode(mCurrentMode)) { 728 final int translateWidth = mIsRtl ? width : -width; 729 translatePanes(translateWidth, translateWidth, animate); 730 adjustPaneVisibility(false /* folder */, false /* list */, true /* cv */); 731 } else { 732 translatePanes(0, 0, animate); 733 adjustPaneVisibility(true /* folder */, true /* list */, false /* cv */); 734 } 735 // adjustPaneVisibility assumes onTransitionComplete will be called to finish setting the 736 // visibility of disappearing panes. 737 if (!animate) { 738 onTransitionComplete(); 739 } 740 } 741 742 public boolean isModeChangePending() { 743 return mTranslatedMode != mCurrentMode; 744 } 745 746 private void setPaneWidth(View pane, int w) { 747 final ViewGroup.LayoutParams lp = pane.getLayoutParams(); 748 if (lp.width == w) { 749 return; 750 } 751 lp.width = w; 752 pane.setLayoutParams(lp); 753 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 754 final String s; 755 if (pane == mFoldersView) { 756 s = "folders"; 757 } else if (pane == mListView) { 758 s = "conv-list"; 759 } else if (pane == mConversationView) { 760 s = "conv-view"; 761 } else if (pane == mMiscellaneousView) { 762 s = "misc-view"; 763 } else if (pane == mConversationFrame) { 764 s = "conv-misc-wrapper"; 765 } else { 766 s = "???:" + pane; 767 } 768 LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s); 769 } 770 } 771 772 public boolean shouldShowPreviewPanel() { 773 return mShouldShowPreviewPanel; 774 } 775 776 private class PaneAnimationListener extends AnimatorListenerAdapter implements Runnable { 777 778 @Override 779 public void run() { 780 onTransitionComplete(); 781 } 782 783 @Override 784 public void onAnimationStart(Animator animation) { 785 // If we're running pre-K, we don't have ViewPropertyAnimator's setUpdateListener. 786 // This is a hack to get around it and uses a dummy ValueAnimator to allow us 787 // to create an animation for the shadow along with the list view. 788 if (!Utils.isRunningKitkatOrLater()) { 789 final ValueAnimator shadowAnimator = ValueAnimator.ofFloat(0, 1); 790 shadowAnimator.setDuration(SLIDE_DURATION_MS) 791 .addUpdateListener(mListViewAnimationListener); 792 shadowAnimator.start(); 793 } 794 } 795 796 @Override 797 public void onAnimationEnd(Animator animation) { 798 onTransitionComplete(); 799 } 800 801 } 802 803 } 804