ThreePaneLayout.java revision 9f976e29a0406a51dd7e15778625060d3e5e7031
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
17package com.android.email.activity;
18
19import com.android.email.R;
20import com.android.emailcommon.Logging;
21
22import android.animation.Animator;
23import android.animation.ObjectAnimator;
24import android.animation.PropertyValuesHolder;
25import android.animation.TimeInterpolator;
26import android.content.Context;
27import android.content.res.Resources;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.animation.DecelerateInterpolator;
35import android.widget.LinearLayout;
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 */
45public class ThreePaneLayout extends LinearLayout implements View.OnClickListener {
46    private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true
47
48    private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 80;
49    private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.5f);
50
51    /** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */
52    private static final int STATE_UNINITIALIZED = -1;
53
54    /** Mailbox list + message list both visible. */
55    private static final int STATE_LEFT_VISIBLE = 0;
56
57    /**
58     * A view where the MessageView is visible. The MessageList is visible if
59     * {@link #isPaneCollapsible} is false, but is otherwise collapsed and hidden.
60     */
61    private static final int STATE_RIGHT_VISIBLE = 1;
62
63    /**
64     * A view where the MessageView is partially visible and a collapsible MessageList on the left
65     * has been expanded to be in view. {@link #isPaneCollapsible} must return true for this
66     * state to be active.
67     */
68    private static final int STATE_MIDDLE_EXPANDED = 2;
69
70    // Flags for getVisiblePanes()
71    public static final int PANE_LEFT = 1 << 2;
72    public static final int PANE_MIDDLE = 1 << 1;
73    public static final int PANE_RIGHT = 1 << 0;
74
75    /** Current pane state.  See {@link #changePaneState} */
76    private int mPaneState = STATE_UNINITIALIZED;
77
78    /** See {@link #changePaneState} and {@link #onFirstSizeChanged} */
79    private int mInitialPaneState = STATE_UNINITIALIZED;
80
81    private View mLeftPane;
82    private View mMiddlePane;
83    private View mRightPane;
84    private MessageCommandButtonView mMessageCommandButtons;
85
86    // Views used only when the left pane is collapsible.
87    private View mFoggedGlass;
88
89    private boolean mFirstSizeChangedDone;
90
91    /** Mailbox list width.  Comes from resources. */
92    private int mMailboxListWidth;
93    /**
94     * Message list width, on:
95     * - the message list + message view mode, when the left pane is not collapsible
96     * - the message view + expanded message list mode, when the left pane is collapsible
97     * Comes from resources.
98     */
99    private int mMessageListWidth;
100
101    /** Hold last animator to cancel. */
102    private Animator mLastAnimator;
103
104    /**
105     * Hold last animator listener to cancel.  See {@link #startLayoutAnimation} for why
106     * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener}
107     */
108    private AnimatorListener mLastAnimatorListener;
109
110    // 2nd index for {@link #changePaneState}
111    private final int INDEX_VISIBLE = 0;
112    private final int INDEX_INVISIBLE = 1;
113    private final int INDEX_GONE = 2;
114
115    // Arrays used in {@link #changePaneState}
116    // First index: STATE_*
117    // Second index: INDEX_*
118    private View[][][] mShowHideViews;
119
120    private Callback mCallback = EmptyCallback.INSTANCE;
121
122    public interface Callback {
123        /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */
124        public void onVisiblePanesChanged(int previousVisiblePanes);
125    }
126
127    private static final class EmptyCallback implements Callback {
128        public static final Callback INSTANCE = new EmptyCallback();
129
130        @Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
131    }
132
133    public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
134        super(context, attrs, defStyle);
135        initView();
136    }
137
138    public ThreePaneLayout(Context context, AttributeSet attrs) {
139        super(context, attrs);
140        initView();
141    }
142
143    public ThreePaneLayout(Context context) {
144        super(context);
145        initView();
146    }
147
148    /** Perform basic initialization */
149    private void initView() {
150        setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
151    }
152
153    @Override
154    protected void onFinishInflate() {
155        super.onFinishInflate();
156
157        mLeftPane = findViewById(R.id.left_pane);
158        mMiddlePane = findViewById(R.id.middle_pane);
159        mMessageCommandButtons =
160                (MessageCommandButtonView) findViewById(R.id.message_command_buttons);
161
162        mFoggedGlass = findViewById(R.id.fogged_glass);
163        if (mFoggedGlass != null) {
164            mRightPane = findViewById(R.id.right_pane_with_fog);
165            mFoggedGlass.setOnClickListener(this);
166        } else {
167            mRightPane = findViewById(R.id.right_pane);
168        }
169
170        if (!isPaneCollapsible()) {
171            mShowHideViews = new View[][][] {
172                    // STATE_LEFT_VISIBLE
173                    {
174                        {mLeftPane, mMiddlePane}, // Visible
175                        {mRightPane}, // Invisible
176                        {mMessageCommandButtons}, // Gone
177                    },
178                    // STATE_RIGHT_VISIBLE
179                    {
180                        {mMiddlePane, mMessageCommandButtons, mRightPane}, // Visible
181                        {mLeftPane}, // Invisible
182                        {}, // Gone
183                    },
184                    // STATE_MIDDLE_EXPANDED
185                    {
186                        {}, // Visible
187                        {}, // Invisible
188                        {}, // Gone
189                    },
190            };
191        } else {
192            mShowHideViews = new View[][][] {
193                    // STATE_LEFT_VISIBLE
194                    {
195                        {mLeftPane, mMiddlePane}, // Visible
196                        {mRightPane, mFoggedGlass}, // Invisible
197                        {mMessageCommandButtons}, // Gone
198                    },
199                    // STATE_RIGHT_VISIBLE
200                    {
201                        {mRightPane, mMessageCommandButtons}, // Visible
202                        {mLeftPane, mMiddlePane, mFoggedGlass}, // Invisible
203                        {}, // Gone
204                    },
205                    // STATE_MIDDLE_EXPANDED
206                    {
207                        {mMiddlePane, mRightPane, mMessageCommandButtons, mFoggedGlass}, // Visible
208                        {mLeftPane}, // Invisible
209                        {}, // Gone
210                    },
211            };
212        }
213
214        mInitialPaneState = STATE_LEFT_VISIBLE;
215
216        final Resources resources = getResources();
217        mMailboxListWidth = getResources().getDimensionPixelSize(
218                R.dimen.mailbox_list_width);
219        mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width);
220    }
221
222
223    public void setCallback(Callback callback) {
224        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
225    }
226
227    /**
228     * Return whether or not the left pane should be collapsible.
229     */
230    private boolean isPaneCollapsible() {
231        return mFoggedGlass != null;
232    }
233
234    public MessageCommandButtonView getMessageCommandButtons() {
235        return mMessageCommandButtons;
236    }
237
238    @Override
239    protected Parcelable onSaveInstanceState() {
240        SavedState ss = new SavedState(super.onSaveInstanceState());
241        ss.mPaneState = mPaneState;
242        return ss;
243    }
244
245    @Override
246    protected void onRestoreInstanceState(Parcelable state) {
247        // Called after onFinishInflate()
248        SavedState ss = (SavedState) state;
249        super.onRestoreInstanceState(ss.getSuperState());
250        mInitialPaneState = ss.mPaneState;
251    }
252
253    @Override
254    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
255        super.onSizeChanged(w, h, oldw, oldh);
256        if (!mFirstSizeChangedDone) {
257            mFirstSizeChangedDone = true;
258            onFirstSizeChanged();
259        }
260    }
261
262    /**
263     * @return bit flags for visible panes.  Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE}
264     * and {@link #PANE_RIGHT},
265     */
266    public int getVisiblePanes() {
267        int ret = 0;
268        if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT;
269        if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE;
270        if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT;
271        return ret;
272    }
273
274    /**
275     * Handles the back event.
276     *
277     * @param isSystemBackKey set true if the system back key is pressed, rather than the home
278     * icon on action bar.
279     * @return true if the event is handled.
280     */
281    public boolean onBackPressed(boolean isSystemBackKey) {
282        if (!isPaneCollapsible()) {
283            switch (mPaneState) {
284            case STATE_RIGHT_VISIBLE:
285                changePaneState(STATE_LEFT_VISIBLE, true); // Close the right pane
286                return true;
287            }
288        } else {
289            switch (mPaneState) {
290                case STATE_RIGHT_VISIBLE:
291                    if (isSystemBackKey) {
292                        changePaneState(STATE_LEFT_VISIBLE, true);
293                    } else {
294                        changePaneState(STATE_MIDDLE_EXPANDED, true);
295                    }
296                    return true;
297                case STATE_MIDDLE_EXPANDED:
298                    changePaneState(STATE_LEFT_VISIBLE, true);
299                    return true;
300                }
301        }
302        return false;
303    }
304
305    /**
306     * Show the left most pane.  (i.e. mailbox list)
307     */
308    public void showLeftPane() {
309        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 void showRightPane() {
329        changePaneState(STATE_RIGHT_VISIBLE, true);
330    }
331
332    private void 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;
342        }
343        if (newState == mPaneState) {
344            return;
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    }
427
428    /**
429     * @return The ID of the view for the left pane fragment.  (i.e. mailbox list)
430     */
431    public int getLeftPaneId() {
432        return R.id.left_pane;
433    }
434
435    /**
436     * @return The ID of the view for the middle pane fragment.  (i.e. message list)
437     */
438    public int getMiddlePaneId() {
439        return R.id.middle_pane;
440    }
441
442    /**
443     * @return The ID of the view for the right pane fragment.  (i.e. message view)
444     */
445    public int getRightPaneId() {
446        return R.id.right_pane;
447    }
448
449    @Override
450    public void onClick(View v) {
451        switch (v.getId()) {
452            case R.id.fogged_glass:
453                if (!isPaneCollapsible()) {
454                    return; // Shouldn't happen
455                }
456                changePaneState(STATE_RIGHT_VISIBLE, true);
457                break;
458        }
459    }
460
461    private void setViewWidth(View v, int value) {
462        v.getLayoutParams().width = value;
463        requestLayout();
464    }
465
466    private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
467    private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
468
469    @SuppressWarnings("unused")
470    public void setMailboxListLeftAnim(int value) {
471        ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
472        requestLayout();
473    }
474
475    @SuppressWarnings("unused")
476    public void setMessageListWidthAnim(int value) {
477        setViewWidth(mMiddlePane, value);
478    }
479
480    private int getCurrentMailboxLeft() {
481        return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
482    }
483
484    private int getCurrentMessageListWidth() {
485        return mMiddlePane.getLayoutParams().width;
486    }
487
488    /**
489     * Helper method to start animation.
490     */
491    private void startLayoutAnimation(int duration, AnimatorListener listener,
492            PropertyValuesHolder... values) {
493        if (mLastAnimator != null) {
494            mLastAnimator.cancel();
495        }
496        if (mLastAnimatorListener != null) {
497            if (ANIMATION_DEBUG) {
498                Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
499            }
500            // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
501            // we end up cancelling the previous one *after* starting the next one.
502            // Directly tell the listener it's cancelled to avoid that.
503            mLastAnimatorListener.cancel();
504        }
505
506        final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
507                this, values).setDuration(duration);
508        animator.setInterpolator(INTERPOLATOR);
509        if (listener != null) {
510            animator.addListener(listener);
511        }
512        mLastAnimator = animator;
513        mLastAnimatorListener = listener;
514        animator.start();
515    }
516
517    /**
518     * Animation listener.
519     *
520     * Update the visibility of each pane before/after an animation.
521     */
522    private class AnimatorListener implements Animator.AnimatorListener {
523        private final String mLogLabel;
524        private final View[] mViewsVisible;
525        private final View[] mViewsInvisible;
526        private final View[] mViewsGone;
527        private final int mPreviousVisiblePanes;
528
529        private boolean mCancelled;
530
531        public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible,
532                View[] viewsGone, int previousVisiblePanes) {
533            mLogLabel = logLabel;
534            mViewsVisible = viewsVisible;
535            mViewsInvisible = viewsInvisible;
536            mViewsGone = viewsGone;
537            mPreviousVisiblePanes = previousVisiblePanes;
538        }
539
540        private void log(String message) {
541            if (ANIMATION_DEBUG) {
542                Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
543            }
544        }
545
546        public void cancel() {
547            log("cancel");
548            mCancelled = true;
549        }
550
551        /**
552         * Show the about-to-become-visible panes before an animation.
553         */
554        @Override
555        public void onAnimationStart(Animator animation) {
556            log("start");
557            for (View v : mViewsVisible) {
558                v.setVisibility(View.VISIBLE);
559            }
560            mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
561        }
562
563        @Override
564        public void onAnimationRepeat(Animator animation) {
565        }
566
567        @Override
568        public void onAnimationCancel(Animator animation) {
569        }
570
571        /**
572         * Hide the about-to-become-hidden panes after an animation.
573         */
574        @Override
575        public void onAnimationEnd(Animator animation) {
576            if (mCancelled) {
577                return; // But they shouldn't be hidden when cancelled.
578            }
579            log("end");
580            for (View v : mViewsInvisible) {
581                v.setVisibility(View.INVISIBLE);
582            }
583            for (View v : mViewsGone) {
584                v.setVisibility(View.GONE);
585            }
586            mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
587        }
588    }
589
590    private static class SavedState extends BaseSavedState {
591        int mPaneState;
592
593        /**
594         * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
595         */
596        SavedState(Parcelable superState) {
597            super(superState);
598        }
599
600        /**
601         * Constructor called from {@link #CREATOR}
602         */
603        private SavedState(Parcel in) {
604            super(in);
605            mPaneState = in.readInt();
606        }
607
608        @Override
609        public void writeToParcel(Parcel out, int flags) {
610            super.writeToParcel(out, flags);
611            out.writeInt(mPaneState);
612        }
613
614        public static final Parcelable.Creator<SavedState> CREATOR
615                = new Parcelable.Creator<SavedState>() {
616            public SavedState createFromParcel(Parcel in) {
617                return new SavedState(in);
618            }
619
620            public SavedState[] newArray(int size) {
621                return new SavedState[size];
622            }
623        };
624    }
625}
626