• 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.util.AttributeSet;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.animation.AnimationUtils;
30 import android.widget.FrameLayout;
31 
32 import com.android.mail.R;
33 import com.android.mail.ui.ViewMode.ModeChangeListener;
34 import com.android.mail.utils.LogUtils;
35 import com.android.mail.utils.Utils;
36 import com.android.mail.utils.ViewUtils;
37 import com.google.common.annotations.VisibleForTesting;
38 
39 /**
40  * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet)
41  * activity, and the transitions between them.
42  *
43  * This is not intended to be a generic layout; it is specific to the {@code Fragment}s
44  * available in {@link MailActivity} and assumes their existence. It merely configures them
45  * according to the specific <i>modes</i> the {@link Activity} can be in.
46  *
47  * Currently, the layout differs in three dimensions: orientation, two aspects of view modes.
48  * This results in essentially three states: One where the folders are on the left and conversation
49  * list is on the right, and two states where the conversation list is on the left: one in which
50  * it's collapsed and another where it is not.
51  *
52  * In folder or conversation list view, conversations are hidden and folders and conversation lists
53  * are visible. This is the case in both portrait and landscape
54  *
55  * In Conversation List or Conversation View, folders are hidden, and conversation lists and
56  * conversation view is visible. This is the case in both portrait and landscape.
57  *
58  * In the Gmail source code, this was called TriStateSplitLayout
59  */
60 final class TwoPaneLayout extends FrameLayout implements ModeChangeListener {
61 
62     private static final String LOG_TAG = "TwoPaneLayout";
63     private static final long SLIDE_DURATION_MS = 300;
64 
65     private final int mDrawerWidthMini;
66     private final int mDrawerWidthOpen;
67     private final double mConversationListWeight;
68     private final TimeInterpolator mSlideInterpolator;
69     /**
70      * If true, this layout group will treat the thread list and conversation view as full-width
71      * panes to switch between.<br>
72      * <br>
73      * If false, always show a conversation view right next to the conversation list. This view will
74      * also be populated (preview / "peek" mode) with a default conversation if none is selected by
75      * the user.
76      */
77     private final boolean mListCollapsible;
78 
79     /**
80      * The current mode that the tablet layout is in. This is a constant integer that holds values
81      * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}.
82      */
83     private int mCurrentMode = ViewMode.UNKNOWN;
84     /**
85      * This mode represents the current positions of the three panes. This is split out from the
86      * current mode to give context to state transitions.
87      */
88     private int mPositionedMode = ViewMode.UNKNOWN;
89 
90     private TwoPaneController mController;
91     private LayoutListener mListener;
92     private boolean mIsSearchResult;
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 
TwoPaneLayout(Context context)108     public TwoPaneLayout(Context context) {
109         this(context, null);
110     }
111 
TwoPaneLayout(Context context, AttributeSet attrs)112     public TwoPaneLayout(Context context, AttributeSet attrs) {
113         super(context, attrs);
114 
115         final Resources res = getResources();
116 
117         // The conversation list might be visible now, depending on the layout: in portrait we
118         // don't show the conversation list, but in landscape we do.  This information is stored
119         // in the constants
120         mListCollapsible = res.getBoolean(R.bool.list_collapsible);
121 
122         mDrawerWidthMini = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_mini);
123         mDrawerWidthOpen = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_open);
124 
125         mSlideInterpolator = AnimationUtils.loadInterpolator(context,
126                 android.R.interpolator.decelerate_cubic);
127 
128         final int convListWeight = res.getInteger(R.integer.conversation_list_weight);
129         final int convViewWeight = res.getInteger(R.integer.conversation_view_weight);
130         mConversationListWeight = (double) convListWeight
131                 / (convListWeight + convViewWeight);
132     }
133 
134     @Override
onFinishInflate()135     protected void onFinishInflate() {
136         super.onFinishInflate();
137 
138         mFoldersView = findViewById(R.id.drawer);
139         mListView = findViewById(R.id.conversation_list_pane);
140         mConversationView = findViewById(R.id.conversation_pane);
141         mMiscellaneousView = findViewById(MISCELLANEOUS_VIEW_ID);
142 
143         // all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes
144         mCurrentMode = ViewMode.UNKNOWN;
145         mFoldersView.setVisibility(GONE);
146         mListView.setVisibility(GONE);
147         mConversationView.setVisibility(GONE);
148         mMiscellaneousView.setVisibility(GONE);
149     }
150 
151     @VisibleForTesting
setController(TwoPaneController controller, boolean isSearchResult)152     public void setController(TwoPaneController controller, boolean isSearchResult) {
153         mController = controller;
154         mListener = controller;
155         mIsSearchResult = isSearchResult;
156 
157         ((ConversationViewFrame) mConversationView).setDownEventListener(mController);
158     }
159 
160     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)161     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
162         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this);
163         setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec));
164         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
165     }
166 
167     @Override
onLayout(boolean changed, int l, int t, int r, int b)168     protected void onLayout(boolean changed, int l, int t, int r, int b) {
169         LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this);
170         positionPanes(getMeasuredWidth());
171         super.onLayout(changed, l, t, r, b);
172     }
173 
174     /**
175      * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes
176      * have the correct widths set for the current overall size and view mode.
177      *
178      * @param parentWidth this view's new width
179      */
setupPaneWidths(int parentWidth)180     private void setupPaneWidths(int parentWidth) {
181         // only adjust the pane widths when my width changes
182         if (parentWidth != getMeasuredWidth()) {
183             final int convWidth = computeConversationWidth(parentWidth);
184             setPaneWidth(mMiscellaneousView, convWidth);
185             setPaneWidth(mConversationView, convWidth);
186             setPaneWidth(mListView, computeConversationListWidth(parentWidth));
187         }
188     }
189 
190     /**
191      * Positions the three sliding panes at the correct X offset (using {@link View#setX(float)}).
192      * When switching from list->conversation mode or vice versa, animate the change in X.
193      *
194      * @param width
195      */
positionPanes(int width)196     private void positionPanes(int width) {
197         final int convX, listX, foldersX;
198         final boolean isRtl = ViewUtils.isViewRtl(this);
199 
200         final int foldersW = isDrawerOpen() ? mDrawerWidthOpen : mDrawerWidthMini;
201         final int listW = getPaneWidth(mListView);
202 
203         boolean cvOnScreen = true;
204         if (!mListCollapsible) {
205             if (isRtl) {
206                 foldersX = width - mDrawerWidthOpen;
207                 listX = width - foldersW - listW;
208                 convX = listX - getPaneWidth(mConversationView);
209             } else {
210                 foldersX = 0;
211                 listX = foldersW;
212                 convX = listX + listW;
213             }
214         } else {
215             if (mController.getCurrentConversation() != null
216                     && !mController.isCurrentConversationJustPeeking()) {
217                 // CV mode
218                 if (isRtl) {
219                     convX = 0;
220                     listX = getPaneWidth(mConversationView);
221                     foldersX = listX + width - mDrawerWidthOpen;
222                 } else {
223                     convX = 0;
224                     listX = -listW;
225                     foldersX = listX - foldersW;
226                 }
227             } else {
228                 // TL mode
229                 cvOnScreen = false;
230                 if (isRtl) {
231                     foldersX = width - mDrawerWidthOpen;
232                     listX = width - foldersW - listW;
233                     convX = listX - getPaneWidth(mConversationView);
234                 } else {
235                     foldersX = 0;
236                     listX = foldersW;
237                     convX = listX + listW;
238                 }
239             }
240         }
241 
242         animatePanes(foldersX, listX, convX);
243 
244         // For views that are not on the screen, let's set their visibility for accessibility.
245         mFoldersView.setVisibility(foldersX >= 0 ? VISIBLE : INVISIBLE);
246         mListView.setVisibility(listX >= 0 ? VISIBLE : INVISIBLE);
247         mConversationView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE);
248         mMiscellaneousView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE);
249 
250         mPositionedMode = mCurrentMode;
251     }
252 
253     private final AnimatorListenerAdapter mPaneAnimationListener = new AnimatorListenerAdapter() {
254         @Override
255         public void onAnimationEnd(Animator animation) {
256             useHardwareLayer(false);
257             onTransitionComplete();
258         }
259         @Override
260         public void onAnimationCancel(Animator animation) {
261             useHardwareLayer(false);
262         }
263     };
264 
animatePanes(int foldersX, int listX, int convX)265     private void animatePanes(int foldersX, int listX, int convX) {
266         // If positioning has not yet happened, we don't need to animate panes into place.
267         // This happens on first layout, rotate, and when jumping straight to a conversation from
268         // a view intent.
269         if (mPositionedMode == ViewMode.UNKNOWN) {
270             mConversationView.setX(convX);
271             mMiscellaneousView.setX(convX);
272             mListView.setX(listX);
273             mFoldersView.setX(foldersX);
274 
275             // listeners need to know that the "transition" is complete, even if one is not run.
276             // defer notifying listeners because we're in a layout pass, and they might do layout.
277             post(mTransitionCompleteRunnable);
278             return;
279         }
280 
281         useHardwareLayer(true);
282 
283         if (ViewMode.isAdMode(mCurrentMode)) {
284             mMiscellaneousView.animate().x(convX);
285         } else {
286             mConversationView.animate().x(convX);
287         }
288 
289         mFoldersView.animate().x(foldersX);
290         mListView.animate()
291             .x(listX)
292             .setListener(mPaneAnimationListener);
293         configureAnimations(mConversationView, mFoldersView, mListView, mMiscellaneousView);
294     }
295 
configureAnimations(View... views)296     private void configureAnimations(View... views) {
297         for (View v : views) {
298             v.animate()
299                 .setInterpolator(mSlideInterpolator)
300                 .setDuration(SLIDE_DURATION_MS);
301         }
302     }
303 
useHardwareLayer(boolean useHardware)304     private void useHardwareLayer(boolean useHardware) {
305         final int layerType = useHardware ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
306         mFoldersView.setLayerType(layerType, null);
307         mListView.setLayerType(layerType, null);
308         mConversationView.setLayerType(layerType, null);
309         mMiscellaneousView.setLayerType(layerType, null);
310         if (useHardware) {
311             // these buildLayer calls are safe because layout is the only way we get here
312             // (i.e. these views must already be attached)
313             mFoldersView.buildLayer();
314             mListView.buildLayer();
315             mConversationView.buildLayer();
316             mMiscellaneousView.buildLayer();
317         }
318     }
319 
onTransitionComplete()320     private void onTransitionComplete() {
321         if (mController.isDestroyed()) {
322             // quit early if the hosting activity was destroyed before the animation finished
323             LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early");
324             return;
325         }
326 
327         switch (mCurrentMode) {
328             case ViewMode.CONVERSATION:
329             case ViewMode.SEARCH_RESULTS_CONVERSATION:
330                 dispatchConversationVisibilityChanged(true);
331                 dispatchConversationListVisibilityChange(!isConversationListCollapsed());
332 
333                 break;
334             case ViewMode.CONVERSATION_LIST:
335             case ViewMode.SEARCH_RESULTS_LIST:
336                 dispatchConversationVisibilityChanged(false);
337                 dispatchConversationListVisibilityChange(true);
338 
339                 break;
340             case ViewMode.AD:
341                 dispatchConversationVisibilityChanged(false);
342                 dispatchConversationListVisibilityChange(!isConversationListCollapsed());
343 
344                 break;
345             default:
346                 break;
347         }
348     }
349 
350     /**
351      * Computes the width of the conversation list in stable state of the current mode.
352      */
computeConversationListWidth()353     public int computeConversationListWidth() {
354         return computeConversationListWidth(getMeasuredWidth());
355     }
356 
357     /**
358      * Computes the width of the conversation list in stable state of the current mode.
359      */
computeConversationListWidth(int parentWidth)360     private int computeConversationListWidth(int parentWidth) {
361         final int availWidth = parentWidth - mDrawerWidthMini;
362         return mListCollapsible ? availWidth : (int) (availWidth * mConversationListWeight);
363     }
364 
computeConversationWidth()365     public int computeConversationWidth() {
366         return computeConversationWidth(getMeasuredWidth());
367     }
368 
369     /**
370      * Computes the width of the conversation pane in stable state of the
371      * current mode.
372      */
computeConversationWidth(int parentWidth)373     private int computeConversationWidth(int parentWidth) {
374         return mListCollapsible ? parentWidth :
375                 parentWidth - computeConversationListWidth(parentWidth) - mDrawerWidthMini;
376     }
377 
dispatchConversationListVisibilityChange(boolean visible)378     private void dispatchConversationListVisibilityChange(boolean visible) {
379         if (mListener != null) {
380             mListener.onConversationListVisibilityChanged(visible);
381         }
382     }
383 
dispatchConversationVisibilityChanged(boolean visible)384     private void dispatchConversationVisibilityChanged(boolean visible) {
385         if (mListener != null) {
386             mListener.onConversationVisibilityChanged(visible);
387         }
388     }
389 
390     // does not apply to drawer children. will return zero for those.
getPaneWidth(View pane)391     private int getPaneWidth(View pane) {
392         return pane.getLayoutParams().width;
393     }
394 
isDrawerOpen()395     private boolean isDrawerOpen() {
396         return mController != null && mController.isDrawerOpen();
397     }
398 
399     /**
400      * @return Whether or not the conversation list is visible on screen.
401      */
402     @Deprecated
isConversationListCollapsed()403     public boolean isConversationListCollapsed() {
404         return !ViewMode.isListMode(mCurrentMode) && mListCollapsible;
405     }
406 
407     @Override
onViewModeChanged(int newMode)408     public void onViewModeChanged(int newMode) {
409         // make all initially GONE panes visible only when the view mode is first determined
410         if (mCurrentMode == ViewMode.UNKNOWN) {
411             mFoldersView.setVisibility(VISIBLE);
412             mListView.setVisibility(VISIBLE);
413         }
414 
415         if (ViewMode.isAdMode(newMode)) {
416             mMiscellaneousView.setVisibility(VISIBLE);
417             mConversationView.setVisibility(GONE);
418         } else {
419             mConversationView.setVisibility(VISIBLE);
420             mMiscellaneousView.setVisibility(GONE);
421         }
422 
423         // detach the pager immediately from its data source (to prevent processing updates)
424         if (ViewMode.isConversationMode(mCurrentMode)) {
425             mController.disablePagerUpdates();
426         }
427 
428         mCurrentMode = newMode;
429         LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode);
430 
431         // do all the real work in onMeasure/onLayout, when panes are sized and positioned for the
432         // current width/height anyway
433         requestLayout();
434     }
435 
isModeChangePending()436     public boolean isModeChangePending() {
437         return mPositionedMode != mCurrentMode;
438     }
439 
setPaneWidth(View pane, int w)440     private void setPaneWidth(View pane, int w) {
441         final ViewGroup.LayoutParams lp = pane.getLayoutParams();
442         if (lp.width == w) {
443             return;
444         }
445         lp.width = w;
446         pane.setLayoutParams(lp);
447         if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
448             final String s;
449             if (pane == mFoldersView) {
450                 s = "folders";
451             } else if (pane == mListView) {
452                 s = "conv-list";
453             } else if (pane == mConversationView) {
454                 s = "conv-view";
455             } else if (pane == mMiscellaneousView) {
456                 s = "misc-view";
457             } else {
458                 s = "???:" + pane;
459             }
460             LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s);
461         }
462     }
463 
shouldShowPreviewPanel()464     public boolean shouldShowPreviewPanel() {
465         return !mListCollapsible;
466     }
467 }
468