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 implements View.OnClickListener {
     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     private 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     private 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     private 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 
     88     // Views used only when the left pane is collapsible.
     89     private View mFoggedGlass;
     90 
     91     private boolean mFirstSizeChangedDone;
     92 
     93     /** Mailbox list width.  Comes from resources. */
     94     private int mMailboxListWidth;
     95     /**
     96      * Message list width, on:
     97      * - the message list + message view mode, when the left pane is not collapsible
     98      * - the message view + expanded message list mode, when the left pane is collapsible
     99      * Comes from resources.
    100      */
    101     private int mMessageListWidth;
    102 
    103     /** Hold last animator to cancel. */
    104     private Animator mLastAnimator;
    105 
    106     /**
    107      * Hold last animator listener to cancel.  See {@link #startLayoutAnimation} for why
    108      * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener}
    109      */
    110     private AnimatorListener mLastAnimatorListener;
    111 
    112     // 2nd index for {@link #changePaneState}
    113     private static final int INDEX_VISIBLE = 0;
    114     private static final int INDEX_INVISIBLE = 1;
    115     private static final int INDEX_GONE = 2;
    116 
    117     // Arrays used in {@link #changePaneState}
    118     // First index: STATE_*
    119     // Second index: INDEX_*
    120     private View[][][] mShowHideViews;
    121 
    122     private Callback mCallback = EmptyCallback.INSTANCE;
    123 
    124     public interface Callback {
    125         /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */
    126         public void onVisiblePanesChanged(int previousVisiblePanes);
    127     }
    128 
    129     private static final class EmptyCallback implements Callback {
    130         public static final Callback INSTANCE = new EmptyCallback();
    131 
    132         @Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
    133     }
    134 
    135     public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
    136         super(context, attrs, defStyle);
    137         initView();
    138     }
    139 
    140     public ThreePaneLayout(Context context, AttributeSet attrs) {
    141         super(context, attrs);
    142         initView();
    143     }
    144 
    145     public ThreePaneLayout(Context context) {
    146         super(context);
    147         initView();
    148     }
    149 
    150     /** Perform basic initialization */
    151     private void initView() {
    152         setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
    153     }
    154 
    155     @Override
    156     protected void onFinishInflate() {
    157         super.onFinishInflate();
    158 
    159         mLeftPane = findViewById(R.id.left_pane);
    160         mMiddlePane = findViewById(R.id.middle_pane);
    161         mMessageCommandButtons =
    162                 (MessageCommandButtonView) findViewById(R.id.message_command_buttons);
    163 
    164         mFoggedGlass = findViewById(R.id.fogged_glass);
    165         if (mFoggedGlass != null) {
    166             mRightPane = findViewById(R.id.right_pane_with_fog);
    167             mFoggedGlass.setOnClickListener(this);
    168         } else {
    169             mRightPane = findViewById(R.id.right_pane);
    170         }
    171 
    172         if (!isPaneCollapsible()) {
    173             mShowHideViews = new View[][][] {
    174                     // STATE_LEFT_VISIBLE
    175                     {
    176                         {mLeftPane, mMiddlePane}, // Visible
    177                         {mRightPane}, // Invisible
    178                         {mMessageCommandButtons}, // Gone
    179                     },
    180                     // STATE_RIGHT_VISIBLE
    181                     {
    182                         {mMiddlePane, mMessageCommandButtons, mRightPane}, // Visible
    183                         {mLeftPane}, // Invisible
    184                         {}, // Gone
    185                     },
    186                     // STATE_MIDDLE_EXPANDED
    187                     {
    188                         {}, // Visible
    189                         {}, // Invisible
    190                         {}, // Gone
    191                     },
    192             };
    193         } else {
    194             mShowHideViews = new View[][][] {
    195                     // STATE_LEFT_VISIBLE
    196                     {
    197                         {mLeftPane, mMiddlePane}, // Visible
    198                         {mRightPane, mFoggedGlass}, // Invisible
    199                         {mMessageCommandButtons}, // Gone
    200                     },
    201                     // STATE_RIGHT_VISIBLE
    202                     {
    203                         {mRightPane, mMessageCommandButtons}, // Visible
    204                         {mLeftPane, mMiddlePane, mFoggedGlass}, // Invisible
    205                         {}, // Gone
    206                     },
    207                     // STATE_MIDDLE_EXPANDED
    208                     {
    209                         {mMiddlePane, mRightPane, mMessageCommandButtons, mFoggedGlass}, // Visible
    210                         {mLeftPane}, // Invisible
    211                         {}, // Gone
    212                     },
    213             };
    214         }
    215 
    216         mInitialPaneState = STATE_LEFT_VISIBLE;
    217 
    218         final Resources resources = getResources();
    219         mMailboxListWidth = getResources().getDimensionPixelSize(
    220                 R.dimen.mailbox_list_width);
    221         mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width);
    222     }
    223 
    224 
    225     public void setCallback(Callback callback) {
    226         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
    227     }
    228 
    229     /**
    230      * Return whether or not the left pane should be collapsible.
    231      */
    232     public boolean isPaneCollapsible() {
    233         return mFoggedGlass != null;
    234     }
    235 
    236     public MessageCommandButtonView getMessageCommandButtons() {
    237         return mMessageCommandButtons;
    238     }
    239 
    240     @Override
    241     protected Parcelable onSaveInstanceState() {
    242         SavedState ss = new SavedState(super.onSaveInstanceState());
    243         ss.mPaneState = mPaneState;
    244         return ss;
    245     }
    246 
    247     @Override
    248     protected void onRestoreInstanceState(Parcelable state) {
    249         // Called after onFinishInflate()
    250         SavedState ss = (SavedState) state;
    251         super.onRestoreInstanceState(ss.getSuperState());
    252         mInitialPaneState = ss.mPaneState;
    253     }
    254 
    255     @Override
    256     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    257         super.onSizeChanged(w, h, oldw, oldh);
    258         if (!mFirstSizeChangedDone) {
    259             mFirstSizeChangedDone = true;
    260             onFirstSizeChanged();
    261         }
    262     }
    263 
    264     /**
    265      * @return bit flags for visible panes.  Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE}
    266      * and {@link #PANE_RIGHT},
    267      */
    268     public int getVisiblePanes() {
    269         int ret = 0;
    270         if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT;
    271         if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE;
    272         if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT;
    273         return ret;
    274     }
    275 
    276     public boolean isLeftPaneVisible() {
    277         return mLeftPane.getVisibility() == View.VISIBLE;
    278     }
    279     public boolean isMiddlePaneVisible() {
    280         return mMiddlePane.getVisibility() == View.VISIBLE;
    281     }
    282     public boolean isRightPaneVisible() {
    283         return mRightPane.getVisibility() == View.VISIBLE;
    284     }
    285 
    286     /**
    287      * Handles the back event.
    288      *
    289      */
    290     public boolean uncollapsePane() {
    291         if (!isPaneCollapsible()) {
    292             return false;
    293         }
    294 
    295         if (mPaneState == STATE_RIGHT_VISIBLE) {
    296             return changePaneState(STATE_MIDDLE_EXPANDED, true);
    297         } else if (mInitialPaneState == STATE_RIGHT_VISIBLE) {
    298             mInitialPaneState = STATE_MIDDLE_EXPANDED;
    299             return true;
    300         }
    301 
    302         return false;
    303     }
    304 
    305     /**
    306      * Show the left most pane.  (i.e. mailbox list)
    307      */
    308     public boolean showLeftPane() {
    309         return changePaneState(STATE_LEFT_VISIBLE, true);
    310     }
    311 
    312     /**
    313      * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
    314      * can't layout properly.  We just remember all the requests to {@link #changePaneState}
    315      * until the first {@link #onSizeChanged}, at which point we actually change to the last
    316      * requested state.
    317      */
    318     private void onFirstSizeChanged() {
    319         if (mInitialPaneState != STATE_UNINITIALIZED) {
    320             changePaneState(mInitialPaneState, false);
    321             mInitialPaneState = STATE_UNINITIALIZED;
    322         }
    323     }
    324 
    325     /**
    326      * Show the right most pane.  (i.e. message view)
    327      */
    328     public boolean showRightPane() {
    329         return changePaneState(STATE_RIGHT_VISIBLE, true);
    330     }
    331 
    332     private boolean changePaneState(int newState, boolean animate) {
    333         if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) {
    334             newState = STATE_RIGHT_VISIBLE;
    335         }
    336         if (!mFirstSizeChangedDone) {
    337             // Before first onSizeChanged(), we don't know the width of the view, so we can't
    338             // layout properly.
    339             // Just remember the new state and return.
    340             mInitialPaneState = newState;
    341             return false;
    342         }
    343         if (newState == mPaneState) {
    344             return false;
    345         }
    346         // Just make sure the first transition doesn't animate.
    347         if (mPaneState == STATE_UNINITIALIZED) {
    348             animate = false;
    349         }
    350 
    351         final int previousVisiblePanes = getVisiblePanes();
    352         mPaneState = newState;
    353 
    354         // Animate to the new state.
    355         // (We still use animator even if animate == false; we just use 0 duration.)
    356         final int totalWidth = getMeasuredWidth();
    357 
    358         final int expectedMailboxLeft;
    359         final int expectedMessageListWidth;
    360 
    361         final String animatorLabel; // for debug purpose
    362 
    363         if (!isPaneCollapsible()) {
    364             setViewWidth(mLeftPane, mMailboxListWidth);
    365             setViewWidth(mRightPane, totalWidth - mMessageListWidth);
    366 
    367             switch (mPaneState) {
    368                 case STATE_LEFT_VISIBLE:
    369                     // mailbox + message list
    370                     animatorLabel = "moving to [mailbox list + message list]";
    371                     expectedMailboxLeft = 0;
    372                     expectedMessageListWidth = totalWidth - mMailboxListWidth;
    373                     break;
    374                 case STATE_RIGHT_VISIBLE:
    375                     // message list + message view
    376                     animatorLabel = "moving to [message list + message view]";
    377                     expectedMailboxLeft = -mMailboxListWidth;
    378                     expectedMessageListWidth = mMessageListWidth;
    379                     break;
    380                 default:
    381                     throw new IllegalStateException();
    382             }
    383 
    384         } else {
    385             setViewWidth(mLeftPane, mMailboxListWidth);
    386             setViewWidth(mRightPane, totalWidth);
    387 
    388             switch (mPaneState) {
    389                 case STATE_LEFT_VISIBLE:
    390                     // message list + Message view -> mailbox + message list
    391                     animatorLabel = "moving to [mailbox list + message list]";
    392                     expectedMailboxLeft = 0;
    393                     expectedMessageListWidth = totalWidth - mMailboxListWidth;
    394                     break;
    395                 case STATE_MIDDLE_EXPANDED:
    396                     // mailbox + message list -> message list + message view
    397                     animatorLabel = "moving to [message list + message view]";
    398                     expectedMailboxLeft = -mMailboxListWidth;
    399                     expectedMessageListWidth = mMessageListWidth;
    400                     break;
    401                 case STATE_RIGHT_VISIBLE:
    402                     // message view only
    403                     animatorLabel = "moving to [message view]";
    404                     expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth);
    405                     expectedMessageListWidth = mMessageListWidth;
    406                     break;
    407                 default:
    408                     throw new IllegalStateException();
    409             }
    410         }
    411 
    412         final View[][] showHideViews = mShowHideViews[mPaneState];
    413         final AnimatorListener listener = new AnimatorListener(animatorLabel,
    414                 showHideViews[INDEX_VISIBLE],
    415                 showHideViews[INDEX_INVISIBLE],
    416                 showHideViews[INDEX_GONE],
    417                 previousVisiblePanes);
    418 
    419         // Animation properties -- mailbox list left and message list width, at the same time.
    420         startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
    421                 PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
    422                         getCurrentMailboxLeft(), expectedMailboxLeft),
    423                 PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
    424                         getCurrentMessageListWidth(), expectedMessageListWidth)
    425                 );
    426         return true;
    427     }
    428 
    429     /**
    430      * @return The ID of the view for the left pane fragment.  (i.e. mailbox list)
    431      */
    432     public int getLeftPaneId() {
    433         return R.id.left_pane;
    434     }
    435 
    436     /**
    437      * @return The ID of the view for the middle pane fragment.  (i.e. message list)
    438      */
    439     public int getMiddlePaneId() {
    440         return R.id.middle_pane;
    441     }
    442 
    443     /**
    444      * @return The ID of the view for the right pane fragment.  (i.e. message view)
    445      */
    446     public int getRightPaneId() {
    447         return R.id.right_pane;
    448     }
    449 
    450     @Override
    451     public void onClick(View v) {
    452         switch (v.getId()) {
    453             case R.id.fogged_glass:
    454                 if (!isPaneCollapsible()) {
    455                     return; // Shouldn't happen
    456                 }
    457                 changePaneState(STATE_RIGHT_VISIBLE, true);
    458                 break;
    459         }
    460     }
    461 
    462     private void setViewWidth(View v, int value) {
    463         v.getLayoutParams().width = value;
    464         requestLayout();
    465     }
    466 
    467     private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
    468     private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
    469 
    470     public void setMailboxListLeftAnim(int value) {
    471         ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
    472         requestLayout();
    473     }
    474 
    475     public void setMessageListWidthAnim(int value) {
    476         setViewWidth(mMiddlePane, value);
    477     }
    478 
    479     private int getCurrentMailboxLeft() {
    480         return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
    481     }
    482 
    483     private int getCurrentMessageListWidth() {
    484         return mMiddlePane.getLayoutParams().width;
    485     }
    486 
    487     /**
    488      * Helper method to start animation.
    489      */
    490     private void startLayoutAnimation(int duration, AnimatorListener listener,
    491             PropertyValuesHolder... values) {
    492         if (mLastAnimator != null) {
    493             mLastAnimator.cancel();
    494         }
    495         if (mLastAnimatorListener != null) {
    496             if (ANIMATION_DEBUG) {
    497                 Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
    498             }
    499             // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
    500             // we end up cancelling the previous one *after* starting the next one.
    501             // Directly tell the listener it's cancelled to avoid that.
    502             mLastAnimatorListener.cancel();
    503         }
    504 
    505         final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
    506                 this, values).setDuration(duration);
    507         animator.setInterpolator(INTERPOLATOR);
    508         if (listener != null) {
    509             animator.addListener(listener);
    510         }
    511         mLastAnimator = animator;
    512         mLastAnimatorListener = listener;
    513         animator.start();
    514     }
    515 
    516     /**
    517      * Animation listener.
    518      *
    519      * Update the visibility of each pane before/after an animation.
    520      */
    521     private class AnimatorListener implements Animator.AnimatorListener {
    522         private final String mLogLabel;
    523         private final View[] mViewsVisible;
    524         private final View[] mViewsInvisible;
    525         private final View[] mViewsGone;
    526         private final int mPreviousVisiblePanes;
    527 
    528         private boolean mCancelled;
    529 
    530         public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible,
    531                 View[] viewsGone, int previousVisiblePanes) {
    532             mLogLabel = logLabel;
    533             mViewsVisible = viewsVisible;
    534             mViewsInvisible = viewsInvisible;
    535             mViewsGone = viewsGone;
    536             mPreviousVisiblePanes = previousVisiblePanes;
    537         }
    538 
    539         private void log(String message) {
    540             if (ANIMATION_DEBUG) {
    541                 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
    542             }
    543         }
    544 
    545         public void cancel() {
    546             log("cancel");
    547             mCancelled = true;
    548         }
    549 
    550         /**
    551          * Show the about-to-become-visible panes before an animation.
    552          */
    553         @Override
    554         public void onAnimationStart(Animator animation) {
    555             log("start");
    556             for (View v : mViewsVisible) {
    557                 v.setVisibility(View.VISIBLE);
    558             }
    559 
    560             // TODO These things, making invisible views and calling the visible pane changed
    561             // callback, should really be done in onAnimationEnd.
    562             // However, because we may want to initiate a fragment transaction in the callback but
    563             // by the time animation is done, the activity may be stopped (by user's HOME press),
    564             // it's not easy to get right.  For now, we just do this before the animation.
    565             for (View v : mViewsInvisible) {
    566                 v.setVisibility(View.INVISIBLE);
    567             }
    568             for (View v : mViewsGone) {
    569                 v.setVisibility(View.GONE);
    570             }
    571             mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
    572         }
    573 
    574         @Override
    575         public void onAnimationRepeat(Animator animation) {
    576         }
    577 
    578         @Override
    579         public void onAnimationCancel(Animator animation) {
    580         }
    581 
    582         /**
    583          * Hide the about-to-become-hidden panes after an animation.
    584          */
    585         @Override
    586         public void onAnimationEnd(Animator animation) {
    587             if (mCancelled) {
    588                 return; // But they shouldn't be hidden when cancelled.
    589             }
    590             log("end");
    591         }
    592     }
    593 
    594     private static class SavedState extends BaseSavedState {
    595         int mPaneState;
    596 
    597         /**
    598          * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
    599          */
    600         SavedState(Parcelable superState) {
    601             super(superState);
    602         }
    603 
    604         /**
    605          * Constructor called from {@link #CREATOR}
    606          */
    607         private SavedState(Parcel in) {
    608             super(in);
    609             mPaneState = in.readInt();
    610         }
    611 
    612         @Override
    613         public void writeToParcel(Parcel out, int flags) {
    614             super.writeToParcel(out, flags);
    615             out.writeInt(mPaneState);
    616         }
    617 
    618         public static final Parcelable.Creator<SavedState> CREATOR
    619                 = new Parcelable.Creator<SavedState>() {
    620             public SavedState createFromParcel(Parcel in) {
    621                 return new SavedState(in);
    622             }
    623 
    624             public SavedState[] newArray(int size) {
    625                 return new SavedState[size];
    626             }
    627         };
    628     }
    629 }
    630