Home | History | Annotate | Download | only in ui
      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 android.support.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          */
    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 
    170     public TwoPaneLayout(Context context) {
    171         this(context, null);
    172     }
    173 
    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
    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
    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
    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
    257     public void setController(TwoPaneController controller) {
    258         mController = controller;
    259         mListener = controller;
    260 
    261         ((ConversationViewFrame) mConversationFrame).setDownEventListener(mController);
    262     }
    263 
    264     @Override
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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 
    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 
    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 
    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
    490     public void onDragStarted() {
    491         mController.onDrawerDragStarted();
    492     }
    493 
    494     @Override
    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
    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