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
18package com.android.mail.ui;
19
20import java.util.List;
21
22import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.TimeInterpolator;
25import android.animation.ValueAnimator;
26import android.app.Activity;
27import android.content.Context;
28import android.content.res.Resources;
29import android.graphics.Canvas;
30import android.graphics.drawable.Drawable;
31import android.support.annotation.NonNull;
32import android.util.AttributeSet;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.ViewPropertyAnimator;
37import android.view.animation.AnimationUtils;
38import android.widget.FrameLayout;
39
40import com.android.mail.R;
41import com.android.mail.ui.ViewMode.ModeChangeListener;
42import com.android.mail.utils.LogUtils;
43import com.android.mail.utils.Utils;
44import com.android.mail.utils.ViewUtils;
45import com.google.common.annotations.VisibleForTesting;
46import 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 */
69final 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