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          */
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