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