Home | History | Annotate | Download | only in ui
      1 /*******************************************************************************
      2  *      Copyright (C) 2012 Google Inc.
      3  *      Licensed to The Android Open Source Project.
      4  *
      5  *      Licensed under the Apache License, Version 2.0 (the "License");
      6  *      you may not use this file except in compliance with the License.
      7  *      You may obtain a copy of the License at
      8  *
      9  *           http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *      Unless required by applicable law or agreed to in writing, software
     12  *      distributed under the License is distributed on an "AS IS" BASIS,
     13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *      See the License for the specific language governing permissions and
     15  *      limitations under the License.
     16  *******************************************************************************/
     17 
     18 package com.android.mail.ui;
     19 
     20 import 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 
    108     public TwoPaneLayout(Context context) {
    109         this(context, null);
    110     }
    111 
    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
    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
    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
    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
    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      */
    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      */
    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 
    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 
    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 
    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 
    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      */
    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      */
    360     private int computeConversationListWidth(int parentWidth) {
    361         final int availWidth = parentWidth - mDrawerWidthMini;
    362         return mListCollapsible ? availWidth : (int) (availWidth * mConversationListWeight);
    363     }
    364 
    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      */
    373     private int computeConversationWidth(int parentWidth) {
    374         return mListCollapsible ? parentWidth :
    375                 parentWidth - computeConversationListWidth(parentWidth) - mDrawerWidthMini;
    376     }
    377 
    378     private void dispatchConversationListVisibilityChange(boolean visible) {
    379         if (mListener != null) {
    380             mListener.onConversationListVisibilityChanged(visible);
    381         }
    382     }
    383 
    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.
    391     private int getPaneWidth(View pane) {
    392         return pane.getLayoutParams().width;
    393     }
    394 
    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
    403     public boolean isConversationListCollapsed() {
    404         return !ViewMode.isListMode(mCurrentMode) && mListCollapsible;
    405     }
    406 
    407     @Override
    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 
    436     public boolean isModeChangePending() {
    437         return mPositionedMode != mCurrentMode;
    438     }
    439 
    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 
    464     public boolean shouldShowPreviewPanel() {
    465         return !mListCollapsible;
    466     }
    467 }
    468