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