Home | History | Annotate | Download | only in activity
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.activity;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ObjectAnimator;
     21 import android.animation.PropertyValuesHolder;
     22 import android.animation.TimeInterpolator;
     23 import android.content.Context;
     24 import android.content.res.Resources;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.animation.DecelerateInterpolator;
     32 import android.widget.LinearLayout;
     33 
     34 import com.android.email.R;
     35 import com.android.emailcommon.Logging;
     36 
     37 /**
     38  * The "three pane" layout used on tablet.
     39  *
     40  * This layout can show up to two panes at any given time, and operates in two different modes.
     41  * See {@link #isPaneCollapsible()} for details on the two modes.
     42  *
     43  * TODO Unit tests, when UX is settled.
     44  *
     45  * TODO onVisiblePanesChanged() should be called *AFTER* the animation, not before.
     46  */
     47 public class ThreePaneLayout extends LinearLayout {
     48     private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true
     49 
     50     private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 150;
     51     private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.75f);
     52 
     53     /** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */
     54     private static final int STATE_UNINITIALIZED = -1;
     55 
     56     /** Mailbox list + message list both visible. */
     57     public static final int STATE_LEFT_VISIBLE = 0;
     58 
     59     /**
     60      * A view where the MessageView is visible. The MessageList is visible if
     61      * {@link #isPaneCollapsible} is false, but is otherwise collapsed and hidden.
     62      */
     63     public static final int STATE_RIGHT_VISIBLE = 1;
     64 
     65     /**
     66      * A view where the MessageView is partially visible and a collapsible MessageList on the left
     67      * has been expanded to be in view. {@link #isPaneCollapsible} must return true for this
     68      * state to be active.
     69      */
     70     public static final int STATE_MIDDLE_EXPANDED = 2;
     71 
     72     // Flags for getVisiblePanes()
     73     public static final int PANE_LEFT = 1 << 2;
     74     public static final int PANE_MIDDLE = 1 << 1;
     75     public static final int PANE_RIGHT = 1 << 0;
     76 
     77     /** Current pane state.  See {@link #changePaneState} */
     78     private int mPaneState = STATE_UNINITIALIZED;
     79 
     80     /** See {@link #changePaneState} and {@link #onFirstSizeChanged} */
     81     private int mInitialPaneState = STATE_UNINITIALIZED;
     82 
     83     private View mLeftPane;
     84     private View mMiddlePane;
     85     private View mRightPane;
     86     private MessageCommandButtonView mMessageCommandButtons;
     87     private MessageCommandButtonView mInMessageCommandButtons;
     88     private boolean mConvViewExpandList;
     89 
     90     private boolean mFirstSizeChangedDone;
     91 
     92     /** Mailbox list width.  Comes from resources. */
     93     private int mMailboxListWidth;
     94     /**
     95      * Message list width, on:
     96      * - the message list + message view mode, when the left pane is not collapsible
     97      * - the message view + expanded message list mode, when the left pane is collapsible
     98      * Comes from resources.
     99      */
    100     private int mMessageListWidth;
    101 
    102     /** Hold last animator to cancel. */
    103     private Animator mLastAnimator;
    104 
    105     /**
    106      * Hold last animator listener to cancel.  See {@link #startLayoutAnimation} for why
    107      * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener}
    108      */
    109     private AnimatorListener mLastAnimatorListener;
    110 
    111     // 2nd index for {@link #changePaneState}
    112     private static final int INDEX_VISIBLE = 0;
    113     private static final int INDEX_INVISIBLE = 1;
    114     private static final int INDEX_GONE = 2;
    115 
    116     // Arrays used in {@link #changePaneState}
    117     // First index: STATE_*
    118     // Second index: INDEX_*
    119     private View[][][] mShowHideViews;
    120 
    121     private Callback mCallback = EmptyCallback.INSTANCE;
    122 
    123     private boolean mIsSearchResult = false;
    124 
    125     public interface Callback {
    126         /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */
    127         public void onVisiblePanesChanged(int previousVisiblePanes);
    128     }
    129 
    130     private static final class EmptyCallback implements Callback {
    131         public static final Callback INSTANCE = new EmptyCallback();
    132 
    133         @Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
    134     }
    135 
    136     public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
    137         super(context, attrs, defStyle);
    138         initView();
    139     }
    140 
    141     public ThreePaneLayout(Context context, AttributeSet attrs) {
    142         super(context, attrs);
    143         initView();
    144     }
    145 
    146     public ThreePaneLayout(Context context) {
    147         super(context);
    148         initView();
    149     }
    150 
    151     /** Perform basic initialization */
    152     private void initView() {
    153         setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
    154     }
    155 
    156     @Override
    157     protected Parcelable onSaveInstanceState() {
    158         SavedState ss = new SavedState(super.onSaveInstanceState());
    159         ss.mPaneState = mPaneState;
    160         return ss;
    161     }
    162 
    163     @Override
    164     protected void onRestoreInstanceState(Parcelable state) {
    165         // Called after onFinishInflate()
    166         SavedState ss = (SavedState) state;
    167         super.onRestoreInstanceState(ss.getSuperState());
    168         if (mIsSearchResult && UiUtilities.showTwoPaneSearchResults(getContext())) {
    169             mInitialPaneState = STATE_RIGHT_VISIBLE;
    170         } else {
    171             mInitialPaneState = ss.mPaneState;
    172         }
    173     }
    174 
    175     @Override
    176     protected void onFinishInflate() {
    177         super.onFinishInflate();
    178 
    179         mLeftPane = findViewById(R.id.left_pane);
    180         mMiddlePane = findViewById(R.id.middle_pane);
    181         mMessageCommandButtons = (MessageCommandButtonView)
    182                 findViewById(R.id.message_command_buttons);
    183         mInMessageCommandButtons = (MessageCommandButtonView)
    184                 findViewById(R.id.inmessage_command_buttons);
    185 
    186         mRightPane = findViewById(R.id.right_pane);
    187         mConvViewExpandList = getContext().getResources().getBoolean(R.bool.expand_middle_view);
    188         View[][] stateRightVisible = new View[][] {
    189                 {
    190                     mMiddlePane, mMessageCommandButtons, mRightPane
    191                 }, // Visible
    192                 {
    193                     mLeftPane
    194                 }, // Invisible
    195                 {
    196                     mInMessageCommandButtons
    197                 }, // Gone;
    198         };
    199         View[][] stateRightVisibleHideConvList = new View[][] {
    200                 {
    201                         mRightPane, mInMessageCommandButtons
    202                 }, // Visible
    203                 {
    204                         mMiddlePane, mMessageCommandButtons, mLeftPane
    205                 }, // Invisible
    206                 {}, // Gone;
    207         };
    208         mShowHideViews = new View[][][] {
    209                 // STATE_LEFT_VISIBLE
    210                 {
    211                         {
    212                            mLeftPane, mMiddlePane
    213                         }, // Visible
    214                         {
    215                             mRightPane
    216                         }, // Invisible
    217                         {
    218                             mMessageCommandButtons, mInMessageCommandButtons
    219                         }, // Gone
    220                 },
    221                 // STATE_RIGHT_VISIBLE
    222                 mConvViewExpandList ? stateRightVisible : stateRightVisibleHideConvList,
    223                 // STATE_MIDDLE_EXPANDED
    224                 {
    225                         {}, // Visible
    226                         {}, // Invisible
    227                         {}, // Gone
    228                 },
    229         };
    230 
    231         mInitialPaneState = STATE_LEFT_VISIBLE;
    232 
    233         final Resources resources = getResources();
    234         mMailboxListWidth = getResources().getDimensionPixelSize(
    235                 R.dimen.mailbox_list_width);
    236         mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width);
    237     }
    238 
    239     public void setIsSearch(boolean isSearch) {
    240         mIsSearchResult = isSearch;
    241         if (mIsSearchResult && UiUtilities.showTwoPaneSearchResults(getContext())) {
    242             mInitialPaneState = STATE_RIGHT_VISIBLE;
    243             if (mPaneState != STATE_RIGHT_VISIBLE) {
    244                 changePaneState(STATE_RIGHT_VISIBLE, false);
    245             }
    246         }
    247     }
    248 
    249     private boolean shouldShowMailboxList() {
    250         return !mIsSearchResult || UiUtilities.showTwoPaneSearchResults(getContext());
    251     }
    252 
    253     public void setCallback(Callback callback) {
    254         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
    255     }
    256 
    257     /**
    258      * Return whether or not the left pane should be collapsible.
    259      */
    260     public boolean isPaneCollapsible() {
    261         return false;
    262     }
    263 
    264     public MessageCommandButtonView getMessageCommandButtons() {
    265         return mMessageCommandButtons;
    266     }
    267 
    268     public MessageCommandButtonView getInMessageCommandButtons() {
    269         return mInMessageCommandButtons;
    270     }
    271 
    272     @Override
    273     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    274         super.onSizeChanged(w, h, oldw, oldh);
    275         if (!mFirstSizeChangedDone) {
    276             mFirstSizeChangedDone = true;
    277             onFirstSizeChanged();
    278         }
    279     }
    280 
    281     /**
    282      * @return bit flags for visible panes.  Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE}
    283      * and {@link #PANE_RIGHT},
    284      */
    285     public int getVisiblePanes() {
    286         int ret = 0;
    287         if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT;
    288         if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE;
    289         if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT;
    290         return ret;
    291     }
    292 
    293     public boolean isLeftPaneVisible() {
    294         return mLeftPane.getVisibility() == View.VISIBLE;
    295     }
    296     public boolean isMiddlePaneVisible() {
    297         return mMiddlePane.getVisibility() == View.VISIBLE;
    298     }
    299     public boolean isRightPaneVisible() {
    300         return mRightPane.getVisibility() == View.VISIBLE;
    301     }
    302 
    303     /**
    304      * Show the left most pane.  (i.e. mailbox list)
    305      */
    306     public boolean showLeftPane() {
    307         return changePaneState(STATE_LEFT_VISIBLE, true);
    308     }
    309 
    310     /**
    311      * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
    312      * can't layout properly.  We just remember all the requests to {@link #changePaneState}
    313      * until the first {@link #onSizeChanged}, at which point we actually change to the last
    314      * requested state.
    315      */
    316     private void onFirstSizeChanged() {
    317         if (mInitialPaneState != STATE_UNINITIALIZED) {
    318             changePaneState(mInitialPaneState, false);
    319             mInitialPaneState = STATE_UNINITIALIZED;
    320         }
    321     }
    322 
    323     /**
    324      * Show the right most pane.  (i.e. message view)
    325      */
    326     public boolean showRightPane() {
    327         return changePaneState(STATE_RIGHT_VISIBLE, true);
    328     }
    329 
    330     private int getMailboxListWidth() {
    331         if (!shouldShowMailboxList()) {
    332             return 0;
    333         }
    334         return mMailboxListWidth;
    335     }
    336 
    337     private boolean changePaneState(int newState, boolean animate) {
    338         if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) {
    339             newState = STATE_RIGHT_VISIBLE;
    340         }
    341         if (!mFirstSizeChangedDone) {
    342             // Before first onSizeChanged(), we don't know the width of the view, so we can't
    343             // layout properly.
    344             // Just remember the new state and return.
    345             mInitialPaneState = newState;
    346             return false;
    347         }
    348         if (newState == mPaneState) {
    349             return false;
    350         }
    351         // Just make sure the first transition doesn't animate.
    352         if (mPaneState == STATE_UNINITIALIZED) {
    353             animate = false;
    354         }
    355 
    356         final int previousVisiblePanes = getVisiblePanes();
    357         mPaneState = newState;
    358 
    359         // Animate to the new state.
    360         // (We still use animator even if animate == false; we just use 0 duration.)
    361         final int totalWidth = getMeasuredWidth();
    362 
    363         final int expectedMailboxLeft;
    364         final int expectedMessageListWidth;
    365 
    366         final String animatorLabel; // for debug purpose
    367 
    368         setViewWidth(mLeftPane, getMailboxListWidth());
    369         setViewWidth(mRightPane, totalWidth - getMessageListWidth());
    370 
    371         switch (mPaneState) {
    372             case STATE_LEFT_VISIBLE:
    373                 // mailbox + message list
    374                 animatorLabel = "moving to [mailbox list + message list]";
    375                 expectedMailboxLeft = 0;
    376                 expectedMessageListWidth = totalWidth - getMailboxListWidth();
    377                 break;
    378             case STATE_RIGHT_VISIBLE:
    379                 // message list + message view
    380                 animatorLabel = "moving to [message list + message view]";
    381                 expectedMailboxLeft = -getMailboxListWidth();
    382                 expectedMessageListWidth = getMessageListWidth();
    383                 break;
    384             default:
    385                 throw new IllegalStateException();
    386         }
    387         setViewWidth(mMiddlePane, expectedMessageListWidth);
    388         final View[][] showHideViews = mShowHideViews[mPaneState];
    389         final AnimatorListener listener = new AnimatorListener(animatorLabel,
    390                 showHideViews[INDEX_VISIBLE],
    391                 showHideViews[INDEX_INVISIBLE],
    392                 showHideViews[INDEX_GONE],
    393                 previousVisiblePanes);
    394 
    395         // Animation properties -- mailbox list left and message list width, at the same time.
    396         startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
    397                 PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
    398                         getCurrentMailboxLeft(), expectedMailboxLeft),
    399                 PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
    400                         getCurrentMessageListWidth(), expectedMessageListWidth)
    401                 );
    402         return true;
    403     }
    404 
    405     private int getMessageListWidth() {
    406         if (!mConvViewExpandList && mPaneState == STATE_RIGHT_VISIBLE) {
    407             return 0;
    408         }
    409         return mMessageListWidth;
    410     }
    411     /**
    412      * @return The ID of the view for the left pane fragment.  (i.e. mailbox list)
    413      */
    414     public int getLeftPaneId() {
    415         return R.id.left_pane;
    416     }
    417 
    418     /**
    419      * @return The ID of the view for the middle pane fragment.  (i.e. message list)
    420      */
    421     public int getMiddlePaneId() {
    422         return R.id.middle_pane;
    423     }
    424 
    425     /**
    426      * @return The ID of the view for the right pane fragment.  (i.e. message view)
    427      */
    428     public int getRightPaneId() {
    429         return R.id.right_pane;
    430     }
    431 
    432     private void setViewWidth(View v, int value) {
    433         v.getLayoutParams().width = value;
    434         requestLayout();
    435     }
    436 
    437     private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
    438     private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
    439 
    440     public void setMailboxListLeftAnim(int value) {
    441         ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
    442         requestLayout();
    443     }
    444 
    445     public void setMessageListWidthAnim(int value) {
    446         setViewWidth(mMiddlePane, value);
    447     }
    448 
    449     private int getCurrentMailboxLeft() {
    450         return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
    451     }
    452 
    453     private int getCurrentMessageListWidth() {
    454         return mMiddlePane.getLayoutParams().width;
    455     }
    456 
    457     /**
    458      * Helper method to start animation.
    459      */
    460     private void startLayoutAnimation(int duration, AnimatorListener listener,
    461             PropertyValuesHolder... values) {
    462         if (mLastAnimator != null) {
    463             mLastAnimator.cancel();
    464         }
    465         if (mLastAnimatorListener != null) {
    466             if (ANIMATION_DEBUG) {
    467                 Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
    468             }
    469             // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
    470             // we end up cancelling the previous one *after* starting the next one.
    471             // Directly tell the listener it's cancelled to avoid that.
    472             mLastAnimatorListener.cancel();
    473         }
    474 
    475         final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
    476                 this, values).setDuration(duration);
    477         animator.setInterpolator(INTERPOLATOR);
    478         if (listener != null) {
    479             animator.addListener(listener);
    480         }
    481         mLastAnimator = animator;
    482         mLastAnimatorListener = listener;
    483         animator.start();
    484     }
    485 
    486     /**
    487      * Get the state of the view. Returns ones of: STATE_UNINITIALIZED,
    488      * STATE_LEFT_VISIBLE, STATE_MIDDLE_EXPANDED, STATE_RIGHT_VISIBLE
    489      */
    490     public int getPaneState() {
    491         return mPaneState;
    492     }
    493     /**
    494      * Animation listener.
    495      *
    496      * Update the visibility of each pane before/after an animation.
    497      */
    498     private class AnimatorListener implements Animator.AnimatorListener {
    499         private final String mLogLabel;
    500         private final View[] mViewsVisible;
    501         private final View[] mViewsInvisible;
    502         private final View[] mViewsGone;
    503         private final int mPreviousVisiblePanes;
    504 
    505         private boolean mCancelled;
    506 
    507         public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible,
    508                 View[] viewsGone, int previousVisiblePanes) {
    509             mLogLabel = logLabel;
    510             mViewsVisible = viewsVisible;
    511             mViewsInvisible = viewsInvisible;
    512             mViewsGone = viewsGone;
    513             mPreviousVisiblePanes = previousVisiblePanes;
    514         }
    515 
    516         private void log(String message) {
    517             if (ANIMATION_DEBUG) {
    518                 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
    519             }
    520         }
    521 
    522         public void cancel() {
    523             log("cancel");
    524             mCancelled = true;
    525         }
    526 
    527         /**
    528          * Show the about-to-become-visible panes before an animation.
    529          */
    530         @Override
    531         public void onAnimationStart(Animator animation) {
    532             log("start");
    533             for (View v : mViewsVisible) {
    534                 v.setVisibility(View.VISIBLE);
    535             }
    536 
    537             // TODO These things, making invisible views and calling the visible pane changed
    538             // callback, should really be done in onAnimationEnd.
    539             // However, because we may want to initiate a fragment transaction in the callback but
    540             // by the time animation is done, the activity may be stopped (by user's HOME press),
    541             // it's not easy to get right.  For now, we just do this before the animation.
    542             for (View v : mViewsInvisible) {
    543                 v.setVisibility(View.INVISIBLE);
    544             }
    545             for (View v : mViewsGone) {
    546                 v.setVisibility(View.GONE);
    547             }
    548             mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
    549         }
    550 
    551         @Override
    552         public void onAnimationRepeat(Animator animation) {
    553         }
    554 
    555         @Override
    556         public void onAnimationCancel(Animator animation) {
    557         }
    558 
    559         /**
    560          * Hide the about-to-become-hidden panes after an animation.
    561          */
    562         @Override
    563         public void onAnimationEnd(Animator animation) {
    564             if (mCancelled) {
    565                 return; // But they shouldn't be hidden when cancelled.
    566             }
    567             log("end");
    568         }
    569     }
    570 
    571     private static class SavedState extends BaseSavedState {
    572         int mPaneState;
    573 
    574         /**
    575          * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
    576          */
    577         SavedState(Parcelable superState) {
    578             super(superState);
    579         }
    580 
    581         /**
    582          * Constructor called from {@link #CREATOR}
    583          */
    584         private SavedState(Parcel in) {
    585             super(in);
    586             mPaneState = in.readInt();
    587         }
    588 
    589         @Override
    590         public void writeToParcel(Parcel out, int flags) {
    591             super.writeToParcel(out, flags);
    592             out.writeInt(mPaneState);
    593         }
    594 
    595         public static final Parcelable.Creator<SavedState> CREATOR
    596                 = new Parcelable.Creator<SavedState>() {
    597             public SavedState createFromParcel(Parcel in) {
    598                 return new SavedState(in);
    599             }
    600 
    601             public SavedState[] newArray(int size) {
    602                 return new SavedState[size];
    603             }
    604         };
    605     }
    606 }
    607