• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  *      Copyright (C) 2012 Google Inc.
3  *      Licensed to The Android Open Source Project.
4  *
5  *      Licensed under the Apache License, Version 2.0 (the "License");
6  *      you may not use this file except in compliance with the License.
7  *      You may obtain a copy of the License at
8  *
9  *           http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *      Unless required by applicable law or agreed to in writing, software
12  *      distributed under the License is distributed on an "AS IS" BASIS,
13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *      See the License for the specific language governing permissions and
15  *      limitations under the License.
16  *******************************************************************************/
17 
18 package com.android.mail.ui;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.TimeInterpolator;
23 import android.app.Activity;
24 import android.content.Context;
25 import android.content.res.Resources;
26 import android.support.v4.widget.DrawerLayout;
27 import android.util.AttributeSet;
28 import android.view.Gravity;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewParent;
32 import android.view.animation.AnimationUtils;
33 import android.widget.FrameLayout;
34 
35 import com.android.mail.R;
36 import com.android.mail.ui.ViewMode.ModeChangeListener;
37 import com.android.mail.utils.LogUtils;
38 import com.android.mail.utils.Utils;
39 import com.google.common.annotations.VisibleForTesting;
40 
41 /**
42  * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet)
43  * activity, and the transitions between them.
44  *
45  * This is not intended to be a generic layout; it is specific to the {@code Fragment}s
46  * available in {@link MailActivity} and assumes their existence. It merely configures them
47  * according to the specific <i>modes</i> the {@link Activity} can be in.
48  *
49  * Currently, the layout differs in three dimensions: orientation, two aspects of view modes.
50  * This results in essentially three states: One where the folders are on the left and conversation
51  * list is on the right, and two states where the conversation list is on the left: one in which
52  * it's collapsed and another where it is not.
53  *
54  * In folder or conversation list view, conversations are hidden and folders and conversation lists
55  * are visible. This is the case in both portrait and landscape
56  *
57  * In Conversation List or Conversation View, folders are hidden, and conversation lists and
58  * conversation view is visible. This is the case in both portrait and landscape.
59  *
60  * In the Gmail source code, this was called TriStateSplitLayout
61  */
62 final class TwoPaneLayout extends FrameLayout implements ModeChangeListener {
63 
64     private static final String LOG_TAG = "TwoPaneLayout";
65     private static final long SLIDE_DURATION_MS = 300;
66 
67     private final double mConversationListWeight;
68     private final double mFolderListWeight;
69     private final TimeInterpolator mSlideInterpolator;
70     /**
71      * True if and only if the conversation list is collapsible in the current device configuration.
72      * See {@link #isConversationListCollapsed()} to see whether it is currently collapsed
73      * (based on the current view mode).
74      */
75     private final boolean mListCollapsible;
76 
77     /**
78      * The current mode that the tablet layout is in. This is a constant integer that holds values
79      * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}.
80      */
81     private int mCurrentMode = ViewMode.UNKNOWN;
82     /**
83      * This mode represents the current positions of the three panes. This is split out from the
84      * current mode to give context to state transitions.
85      */
86     private int mPositionedMode = ViewMode.UNKNOWN;
87 
88     private AbstractActivityController mController;
89     private LayoutListener mListener;
90     private boolean mIsSearchResult;
91 
92     private DrawerLayout mDrawerLayout;
93 
94     private View mMiscellaneousView;
95     private View mConversationView;
96     private View mFoldersView;
97     private View mListView;
98 
99     public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane;
100 
101     private final Runnable mTransitionCompleteRunnable = new Runnable() {
102         @Override
103         public void run() {
104             onTransitionComplete();
105         }
106     };
107     /**
108      * A special view used during animation of the conversation list.
109      * <p>
110      * The conversation list changes width when switching view modes, so to visually smooth out
111      * the transition, we cross-fade the old and new widths. During the transition, a bitmap of the
112      * old conversation list is kept here, and this view moves in tandem with the real list view,
113      * but its opacity gradually fades out to give way to the new width.
114      */
115     private ConversationListCopy mListCopyView;
116 
117     /**
118      * During a mode transition, this value is the final width for {@link #mListCopyView}. We want
119      * to avoid changing its width during the animation, as it should match the initial width of
120      * {@link #mListView}.
121      */
122     private Integer mListCopyWidthOnComplete;
123 
124     private final boolean mIsExpansiveLayout;
125     private boolean mDrawerInitialSetupComplete;
126 
TwoPaneLayout(Context context)127     public TwoPaneLayout(Context context) {
128         this(context, null);
129     }
130 
TwoPaneLayout(Context context, AttributeSet attrs)131     public TwoPaneLayout(Context context, AttributeSet attrs) {
132         super(context, attrs);
133 
134         final Resources res = getResources();
135 
136         // The conversation list might be visible now, depending on the layout: in portrait we
137         // don't show the conversation list, but in landscape we do.  This information is stored
138         // in the constants
139         mListCollapsible = res.getBoolean(R.bool.list_collapsible);
140 
141         mSlideInterpolator = AnimationUtils.loadInterpolator(context,
142                 android.R.interpolator.decelerate_cubic);
143 
144         final int folderListWeight = res.getInteger(R.integer.folder_list_weight);
145         final int convListWeight = res.getInteger(R.integer.conversation_list_weight);
146         final int convViewWeight = res.getInteger(R.integer.conversation_view_weight);
147         mFolderListWeight = (double) folderListWeight
148                 / (folderListWeight + convListWeight);
149         mConversationListWeight = (double) convListWeight
150                 / (convListWeight + convViewWeight);
151 
152         mIsExpansiveLayout = res.getBoolean(R.bool.use_expansive_tablet_ui);
153         mDrawerInitialSetupComplete = false;
154     }
155 
156     @Override
onFinishInflate()157     protected void onFinishInflate() {
158         super.onFinishInflate();
159 
160         mFoldersView = findViewById(R.id.content_pane);
161         mListView = findViewById(R.id.conversation_list_pane);
162         mListCopyView = (ConversationListCopy) findViewById(R.id.conversation_list_copy);
163         mConversationView = findViewById(R.id.conversation_pane);
164         mMiscellaneousView = findViewById(MISCELLANEOUS_VIEW_ID);
165 
166         // all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes
167         mCurrentMode = ViewMode.UNKNOWN;
168         mFoldersView.setVisibility(GONE);
169         mListView.setVisibility(GONE);
170         mListCopyView.setVisibility(GONE);
171         mConversationView.setVisibility(GONE);
172         mMiscellaneousView.setVisibility(GONE);
173     }
174 
175     @VisibleForTesting
setController(AbstractActivityController controller, boolean isSearchResult)176     public void setController(AbstractActivityController controller, boolean isSearchResult) {
177         mController = controller;
178         mListener = controller;
179         mIsSearchResult = isSearchResult;
180     }
181 
setDrawerLayout(DrawerLayout drawerLayout)182     public void setDrawerLayout(DrawerLayout drawerLayout) {
183         mDrawerLayout = drawerLayout;
184     }
185 
186     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)187     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
188         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this);
189         setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec));
190         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
191     }
192 
193     @Override
onLayout(boolean changed, int l, int t, int r, int b)194     protected void onLayout(boolean changed, int l, int t, int r, int b) {
195         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this);
196         if (changed || mCurrentMode != mPositionedMode) {
197             positionPanes(getMeasuredWidth());
198         }
199         super.onLayout(changed, l, t, r, b);
200     }
201 
202     /**
203      * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes
204      * have the correct widths set for the current overall size and view mode.
205      *
206      * @param parentWidth this view's new width
207      */
setupPaneWidths(int parentWidth)208     private void setupPaneWidths(int parentWidth) {
209         final int foldersWidth = computeFolderListWidth(parentWidth);
210         final int foldersFragmentWidth;
211         if (isDrawerView(mFoldersView)) {
212             foldersFragmentWidth = getResources().getDimensionPixelSize(R.dimen.drawer_width);
213         } else {
214             foldersFragmentWidth = foldersWidth;
215         }
216         final int convWidth = computeConversationWidth(parentWidth);
217 
218         setPaneWidth(mFoldersView, foldersFragmentWidth);
219 
220         // only adjust the fixed conversation view width when my width changes
221         if (parentWidth != getMeasuredWidth()) {
222             LogUtils.i(LOG_TAG, "setting up new TPL, w=%d fw=%d cv=%d", parentWidth,
223                     foldersWidth, convWidth);
224 
225             setPaneWidth(mMiscellaneousView, convWidth);
226             setPaneWidth(mConversationView, convWidth);
227         }
228 
229         final int currListWidth = getPaneWidth(mListView);
230         int listWidth = currListWidth;
231         switch (mCurrentMode) {
232             case ViewMode.AD:
233             case ViewMode.CONVERSATION:
234             case ViewMode.SEARCH_RESULTS_CONVERSATION:
235                 if (!mListCollapsible) {
236                     listWidth = parentWidth - convWidth;
237                 }
238                 break;
239             case ViewMode.CONVERSATION_LIST:
240             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
241             case ViewMode.SEARCH_RESULTS_LIST:
242                 listWidth = parentWidth - foldersWidth;
243                 break;
244             default:
245                 break;
246         }
247         LogUtils.d(LOG_TAG, "conversation list width change, w=%d", listWidth);
248         setPaneWidth(mListView, listWidth);
249 
250         if ((mCurrentMode != mPositionedMode && mPositionedMode != ViewMode.UNKNOWN)
251                 || mListCopyWidthOnComplete != null) {
252             mListCopyWidthOnComplete = listWidth;
253         } else {
254             setPaneWidth(mListCopyView, listWidth);
255         }
256     }
257 
258     /**
259      * Positions the three sliding panes at the correct X offset (using {@link View#setX(float)}).
260      * When switching from list->conversation mode or vice versa, animate the change in X.
261      *
262      * @param width
263      */
positionPanes(int width)264     private void positionPanes(int width) {
265         if (mPositionedMode == mCurrentMode) {
266             return;
267         }
268 
269         boolean hasPositions = false;
270         int convX = 0, listX = 0, foldersX = 0;
271 
272         switch (mCurrentMode) {
273             case ViewMode.AD:
274             case ViewMode.CONVERSATION:
275             case ViewMode.SEARCH_RESULTS_CONVERSATION: {
276                 final int foldersW = getPaneWidth(mFoldersView);
277                 final int listW;
278                 listW = getPaneWidth(mListView);
279 
280                 if (mListCollapsible) {
281                     convX = 0;
282                     listX = -listW;
283                     foldersX = listX - foldersW;
284                 } else {
285                     convX = listW;
286                     listX = 0;
287                     foldersX = -foldersW;
288                 }
289                 hasPositions = true;
290                 LogUtils.i(LOG_TAG, "conversation mode layout, x=%d/%d/%d", foldersX, listX, convX);
291                 break;
292             }
293             case ViewMode.CONVERSATION_LIST:
294             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
295             case ViewMode.SEARCH_RESULTS_LIST: {
296                 convX = width;
297                 listX = getPaneWidth(mFoldersView);
298                 foldersX = 0;
299 
300                 hasPositions = true;
301                 LogUtils.i(LOG_TAG, "conv-list mode layout, x=%d/%d/%d", foldersX, listX, convX);
302                 break;
303             }
304             default:
305                 break;
306         }
307 
308         if (hasPositions) {
309             animatePanes(foldersX, listX, convX);
310         }
311 
312         mPositionedMode = mCurrentMode;
313     }
314 
315     private final AnimatorListenerAdapter mPaneAnimationListener = new AnimatorListenerAdapter() {
316         @Override
317         public void onAnimationEnd(Animator animation) {
318             mListCopyView.unbind();
319             useHardwareLayer(false);
320             fixupListCopyWidth();
321             onTransitionComplete();
322         }
323         @Override
324         public void onAnimationCancel(Animator animation) {
325             mListCopyView.unbind();
326             useHardwareLayer(false);
327         }
328     };
329 
330     /**
331      * @param foldersX
332      * @param listX
333      * @param convX
334      */
animatePanes(int foldersX, int listX, int convX)335     private void animatePanes(int foldersX, int listX, int convX) {
336         // If positioning has not yet happened, we don't need to animate panes into place.
337         // This happens on first layout, rotate, and when jumping straight to a conversation from
338         // a view intent.
339         if (mPositionedMode == ViewMode.UNKNOWN) {
340             mConversationView.setX(convX);
341             mMiscellaneousView.setX(convX);
342             mListView.setX(listX);
343             if (!isDrawerView(mFoldersView)) {
344                 mFoldersView.setX(foldersX);
345             }
346 
347             // listeners need to know that the "transition" is complete, even if one is not run.
348             // defer notifying listeners because we're in a layout pass, and they might do layout.
349             post(mTransitionCompleteRunnable);
350             return;
351         }
352 
353         final boolean useListCopy = getPaneWidth(mListView) != getPaneWidth(mListCopyView);
354 
355         if (useListCopy) {
356             // freeze the current list view before it gets redrawn
357             mListCopyView.bind(mListView);
358             mListCopyView.setX(mListView.getX());
359 
360             mListCopyView.setAlpha(1.0f);
361             mListView.setAlpha(0.0f);
362         }
363 
364         useHardwareLayer(true);
365 
366         if (ViewMode.isAdMode(mCurrentMode)) {
367             mMiscellaneousView.animate().x(convX);
368         } else {
369             mConversationView.animate().x(convX);
370         }
371 
372         if (!isDrawerView(mFoldersView)) {
373             mFoldersView.animate().x(foldersX);
374         }
375         if (useListCopy) {
376             mListCopyView.animate().x(listX).alpha(0.0f);
377         }
378         mListView.animate()
379             .x(listX)
380             .alpha(1.0f)
381             .setListener(mPaneAnimationListener);
382         configureAnimations(mConversationView, mFoldersView, mListView, mListCopyView,
383                 mMiscellaneousView);
384     }
385 
configureAnimations(View... views)386     private void configureAnimations(View... views) {
387         for (View v : views) {
388             if (isDrawerView(v)) {
389                 continue;
390             }
391             v.animate()
392                 .setInterpolator(mSlideInterpolator)
393                 .setDuration(SLIDE_DURATION_MS);
394         }
395     }
396 
useHardwareLayer(boolean useHardware)397     private void useHardwareLayer(boolean useHardware) {
398         final int layerType = useHardware ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
399         if (!isDrawerView(mFoldersView)) {
400             mFoldersView.setLayerType(layerType, null);
401         }
402         mListView.setLayerType(layerType, null);
403         mListCopyView.setLayerType(layerType, null);
404         mConversationView.setLayerType(layerType, null);
405         mMiscellaneousView.setLayerType(layerType, null);
406         if (useHardware) {
407             // these buildLayer calls are safe because layout is the only way we get here
408             // (i.e. these views must already be attached)
409             if (!isDrawerView(mFoldersView)) {
410                 mFoldersView.buildLayer();
411             }
412             mListView.buildLayer();
413             mListCopyView.buildLayer();
414             mConversationView.buildLayer();
415             mMiscellaneousView.buildLayer();
416         }
417     }
418 
fixupListCopyWidth()419     private void fixupListCopyWidth() {
420         if (mListCopyWidthOnComplete == null ||
421                 getPaneWidth(mListCopyView) == mListCopyWidthOnComplete) {
422             mListCopyWidthOnComplete = null;
423             return;
424         }
425         LogUtils.i(LOG_TAG, "onAnimationEnd of list view, setting copy width to %d",
426                 mListCopyWidthOnComplete);
427         setPaneWidth(mListCopyView, mListCopyWidthOnComplete);
428         mListCopyWidthOnComplete = null;
429     }
430 
onTransitionComplete()431     private void onTransitionComplete() {
432         if (mController.isDestroyed()) {
433             // quit early if the hosting activity was destroyed before the animation finished
434             LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early");
435             return;
436         }
437 
438         switch (mCurrentMode) {
439             case ViewMode.CONVERSATION:
440             case ViewMode.SEARCH_RESULTS_CONVERSATION:
441                 dispatchConversationVisibilityChanged(true);
442                 dispatchConversationListVisibilityChange(!isConversationListCollapsed());
443 
444                 break;
445             case ViewMode.CONVERSATION_LIST:
446             case ViewMode.SEARCH_RESULTS_LIST:
447                 dispatchConversationVisibilityChanged(false);
448                 dispatchConversationListVisibilityChange(true);
449 
450                 break;
451             case ViewMode.AD:
452                 dispatchConversationVisibilityChanged(false);
453                 dispatchConversationListVisibilityChange(!isConversationListCollapsed());
454 
455                 break;
456             default:
457                 break;
458         }
459     }
460 
461     /**
462      * Computes the width of the conversation list in stable state of the current mode.
463      */
computeConversationListWidth()464     public int computeConversationListWidth() {
465         return computeConversationListWidth(getMeasuredWidth());
466     }
467 
468     /**
469      * Computes the width of the conversation list in stable state of the current mode.
470      */
computeConversationListWidth(int totalWidth)471     private int computeConversationListWidth(int totalWidth) {
472         switch (mCurrentMode) {
473             case ViewMode.CONVERSATION_LIST:
474             case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
475             case ViewMode.SEARCH_RESULTS_LIST:
476                 return totalWidth - computeFolderListWidth(totalWidth);
477             case ViewMode.AD:
478             case ViewMode.CONVERSATION:
479             case ViewMode.SEARCH_RESULTS_CONVERSATION:
480                 return (int) (totalWidth * mConversationListWeight);
481         }
482         return 0;
483     }
484 
computeConversationWidth()485     public int computeConversationWidth() {
486         return computeConversationWidth(getMeasuredWidth());
487     }
488 
489     /**
490      * Computes the width of the conversation pane in stable state of the
491      * current mode.
492      */
computeConversationWidth(int totalWidth)493     private int computeConversationWidth(int totalWidth) {
494         if (mListCollapsible) {
495             return totalWidth;
496         } else {
497             return totalWidth - (int) (totalWidth * mConversationListWeight);
498         }
499     }
500 
501     /**
502      * Computes the width of the folder list in stable state of the current mode.
503      */
computeFolderListWidth(int parentWidth)504     private int computeFolderListWidth(int parentWidth) {
505         if (mIsSearchResult) {
506             return 0;
507         } else if (isDrawerView(mFoldersView)) {
508             return 0;
509         } else {
510             return (int) (parentWidth * mFolderListWeight);
511         }
512     }
513 
dispatchConversationListVisibilityChange(boolean visible)514     private void dispatchConversationListVisibilityChange(boolean visible) {
515         if (mListener != null) {
516             mListener.onConversationListVisibilityChanged(visible);
517         }
518     }
519 
dispatchConversationVisibilityChanged(boolean visible)520     private void dispatchConversationVisibilityChanged(boolean visible) {
521         if (mListener != null) {
522             mListener.onConversationVisibilityChanged(visible);
523         }
524     }
525 
526     // does not apply to drawer children. will return zero for those.
getPaneWidth(View pane)527     private int getPaneWidth(View pane) {
528         return isDrawerView(pane) ? 0 : pane.getLayoutParams().width;
529     }
530 
isDrawerView(View child)531     private boolean isDrawerView(View child) {
532         return child != null && child.getParent() == mDrawerLayout;
533     }
534 
535     /**
536      * @return Whether or not the conversation list is visible on screen.
537      */
isConversationListCollapsed()538     public boolean isConversationListCollapsed() {
539         return !ViewMode.isListMode(mCurrentMode) && mListCollapsible;
540     }
541 
542     @Override
onViewModeChanged(int newMode)543     public void onViewModeChanged(int newMode) {
544         // make all initially GONE panes visible only when the view mode is first determined
545         if (mCurrentMode == ViewMode.UNKNOWN) {
546             mFoldersView.setVisibility(VISIBLE);
547             mListView.setVisibility(VISIBLE);
548             mListCopyView.setVisibility(VISIBLE);
549         }
550 
551         if (ViewMode.isAdMode(newMode)) {
552             mMiscellaneousView.setVisibility(VISIBLE);
553             mConversationView.setVisibility(GONE);
554         } else {
555             mConversationView.setVisibility(VISIBLE);
556             mMiscellaneousView.setVisibility(GONE);
557         }
558 
559         // set up the drawer as appropriate for the configuration
560         final ViewParent foldersParent = mFoldersView.getParent();
561         if (mIsExpansiveLayout && foldersParent != this) {
562             if (foldersParent != mDrawerLayout) {
563                 throw new IllegalStateException("invalid Folders fragment parent: " +
564                         foldersParent);
565             }
566             mDrawerLayout.removeView(mFoldersView);
567             addView(mFoldersView, 0);
568             mFoldersView.findViewById(R.id.folders_pane_edge).setVisibility(VISIBLE);
569             mFoldersView.setBackgroundDrawable(null);
570         } else if (!mIsExpansiveLayout && foldersParent == this) {
571             removeView(mFoldersView);
572             mDrawerLayout.addView(mFoldersView);
573             final DrawerLayout.LayoutParams lp =
574                     (DrawerLayout.LayoutParams) mFoldersView.getLayoutParams();
575             lp.gravity = Gravity.START;
576             mFoldersView.setLayoutParams(lp);
577             mFoldersView.findViewById(R.id.folders_pane_edge).setVisibility(GONE);
578             mFoldersView.setBackgroundResource(R.color.list_background_color);
579         }
580 
581         // detach the pager immediately from its data source (to prevent processing updates)
582         if (ViewMode.isConversationMode(mCurrentMode)) {
583             mController.disablePagerUpdates();
584         }
585 
586         mDrawerInitialSetupComplete = true;
587         mCurrentMode = newMode;
588         LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode);
589 
590         // do all the real work in onMeasure/onLayout, when panes are sized and positioned for the
591         // current width/height anyway
592         requestLayout();
593     }
594 
isModeChangePending()595     public boolean isModeChangePending() {
596         return mPositionedMode != mCurrentMode;
597     }
598 
setPaneWidth(View pane, int w)599     private void setPaneWidth(View pane, int w) {
600         final ViewGroup.LayoutParams lp = pane.getLayoutParams();
601         if (lp.width == w) {
602             return;
603         }
604         lp.width = w;
605         pane.setLayoutParams(lp);
606         if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
607             final String s;
608             if (pane == mFoldersView) {
609                 s = "folders";
610             } else if (pane == mListView) {
611                 s = "conv-list";
612             } else if (pane == mConversationView) {
613                 s = "conv-view";
614             } else if (pane == mMiscellaneousView) {
615                 s = "misc-view";
616             } else {
617                 s = "???:" + pane;
618             }
619             LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s);
620         }
621     }
622 
isDrawerEnabled()623     public boolean isDrawerEnabled() {
624         return !mIsExpansiveLayout && mDrawerInitialSetupComplete;
625     }
626 
isExpansiveLayout()627     public boolean isExpansiveLayout() {
628         return mIsExpansiveLayout;
629     }
630 }
631