ThreePaneLayout.java revision 570a2f75be21af378601abc0ae829bfaa71c2674
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 android.animation.Animator;
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.animation.TimeInterpolator;
23import android.content.Context;
24import android.content.res.Resources;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.animation.DecelerateInterpolator;
32import android.widget.LinearLayout;
33
34import com.android.email.R;
35import 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 */
47public 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