ThreePaneLayout.java revision d2dac0fd6c7f8e9f6741e3b2d4cc333231a22082
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    public boolean onBackPressed() {
216        if (isLandscape()) {
217            switch (mPaneState) {
218            case STATE_RIGHT_VISIBLE:
219                changePaneState(STATE_LEFT_VISIBLE, true); // Close the right pane
220                return true;
221            }
222        } else {
223            switch (mPaneState) {
224                case STATE_RIGHT_VISIBLE:
225                    changePaneState(STATE_PORTRAIT_MIDDLE_EXPANDED, true);
226                    return true;
227                case STATE_PORTRAIT_MIDDLE_EXPANDED:
228                    changePaneState(STATE_LEFT_VISIBLE, true);
229                    return true;
230                }
231        }
232        return false;
233    }
234
235    /**
236     * Show the left most pane.  (i.e. mailbox list)
237     */
238    public void showLeftPane() {
239        changePaneState(STATE_LEFT_VISIBLE, true);
240    }
241
242    /**
243     * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
244     * can't layout properly.  We just remember all the requests to {@link #changePaneState}
245     * until the first {@link #onSizeChanged}, at which point we actually change to the last
246     * requested state.
247     */
248    private void onFirstSizeChanged() {
249        if (mInitialPaneState != STATE_UNINITIALIZED) {
250            changePaneState(mInitialPaneState, false);
251            mInitialPaneState = STATE_UNINITIALIZED;
252        }
253    }
254
255    /**
256     * Show the right most pane.  (i.e. message view)
257     */
258    public void showRightPane() {
259        changePaneState(STATE_RIGHT_VISIBLE, true);
260    }
261
262    private void changePaneState(int newState, boolean animate) {
263        if (isLandscape() && (newState == STATE_PORTRAIT_MIDDLE_EXPANDED)) {
264            newState = STATE_RIGHT_VISIBLE;
265        }
266        if (!mFirstSizeChangedDone) {
267            // Before first onSizeChanged(), we don't know the width of the view, so we can't
268            // layout properly.
269            // Just remember the new state and return.
270            mInitialPaneState = newState;
271            return;
272        }
273        if (newState == mPaneState) {
274            return;
275        }
276        // Just make sure the first transition doesn't animate.
277        if (mPaneState == STATE_UNINITIALIZED) {
278            animate = false;
279        }
280
281        final int previousVisiblePanes = getVisiblePanes();
282        mPaneState = newState;
283
284        // Animate to the new state.
285        // (We still use animator even if animate == false; we just use 0 duration.)
286        final int totalWidth = getMeasuredWidth();
287
288        final int expectedMailboxLeft;
289        final int expectedMessageListWidth;
290
291        final String animatorLabel; // for debug purpose
292
293        final View[] viewsToShow;
294        final View[] viewsToHide;
295
296        if (isLandscape()) { // Landscape
297            setViewWidth(mLeftPane, mMailboxListWidth);
298            setViewWidth(mRightPane, totalWidth - mMessageListWidth);
299
300            switch (mPaneState) {
301                case STATE_LEFT_VISIBLE:
302                    // mailbox + message list
303                    animatorLabel = "moving to [mailbox list + message list]";
304                    expectedMailboxLeft = 0;
305                    expectedMessageListWidth = totalWidth - mMailboxListWidth;
306                    viewsToShow = mViewsLeft;
307                    viewsToHide = mViewsRight;
308                    break;
309                case STATE_RIGHT_VISIBLE:
310                    // message list + message view
311                    animatorLabel = "moving to [message list + message view]";
312                    expectedMailboxLeft = -mMailboxListWidth;
313                    expectedMessageListWidth = mMessageListWidth;
314                    viewsToShow = mViewsRight;
315                    viewsToHide = mViewsLeft;
316                    break;
317                default:
318                    throw new IllegalStateException();
319            }
320
321        } else { // Portrait
322            setViewWidth(mLeftPane, mMailboxListWidth);
323            setViewWidth(mRightPane, totalWidth);
324
325            switch (mPaneState) {
326                case STATE_LEFT_VISIBLE:
327                    // message list + Message view -> mailbox + message list
328                    animatorLabel = "moving to [mailbox list + message list]";
329                    expectedMailboxLeft = 0;
330                    expectedMessageListWidth = totalWidth - mMailboxListWidth;
331                    viewsToShow = mViewsLeftMiddle;
332                    viewsToHide = mViewsRight;
333                    break;
334                case STATE_PORTRAIT_MIDDLE_EXPANDED:
335                    // mailbox + message list -> message list + message view
336                    animatorLabel = "moving to [message list + message view]";
337                    expectedMailboxLeft = -mMailboxListWidth;
338                    expectedMessageListWidth = mMessageListWidth;
339                    viewsToShow = mViewsMiddleRightFogged;
340                    viewsToHide = mViewsLeft;
341                    break;
342                case STATE_RIGHT_VISIBLE:
343                    // message view only
344                    animatorLabel = "moving to [message view]";
345                    expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth);
346                    expectedMessageListWidth = mMessageListWidth;
347                    viewsToShow = mViewsRight;
348                    viewsToHide = mViewsLeftMiddleFogged;
349                    break;
350                default:
351                    throw new IllegalStateException();
352            }
353        }
354
355        final AnimatorListener listener = new AnimatorListener(animatorLabel, viewsToShow,
356                viewsToHide, previousVisiblePanes) ;
357
358        // Animation properties -- mailbox list left and message list width, at the same time.
359        startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
360                PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
361                        getCurrentMailboxLeft(), expectedMailboxLeft),
362                PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
363                        getCurrentMessageListWidth(), expectedMessageListWidth)
364                );
365    }
366
367    /**
368     * @return The ID of the view for the left pane fragment.  (i.e. mailbox list)
369     */
370    public int getLeftPaneId() {
371        return R.id.left_pane;
372    }
373
374    /**
375     * @return The ID of the view for the middle pane fragment.  (i.e. message list)
376     */
377    public int getMiddlePaneId() {
378        return R.id.middle_pane;
379    }
380
381    /**
382     * @return The ID of the view for the right pane fragment.  (i.e. message view)
383     */
384    public int getRightPaneId() {
385        return R.id.right_pane;
386    }
387
388    @Override
389    public void onClick(View v) {
390        switch (v.getId()) {
391            case R.id.fogged_glass:
392                if (isLandscape()) {
393                    return; // Shouldn't happen
394                }
395                changePaneState(STATE_RIGHT_VISIBLE, true);
396                break;
397        }
398    }
399
400    private void setViewWidth(View v, int value) {
401        v.getLayoutParams().width = value;
402        requestLayout();
403    }
404
405    private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
406    private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
407
408    @SuppressWarnings("unused")
409    public void setMailboxListLeftAnim(int value) {
410        ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
411        requestLayout();
412    }
413
414    @SuppressWarnings("unused")
415    public void setMessageListWidthAnim(int value) {
416        setViewWidth(mMiddlePane, value);
417    }
418
419    private int getCurrentMailboxLeft() {
420        return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
421    }
422
423    private int getCurrentMessageListWidth() {
424        return mMiddlePane.getLayoutParams().width;
425    }
426
427    /**
428     * Helper method to start animation.
429     */
430    private void startLayoutAnimation(int duration, AnimatorListener listener,
431            PropertyValuesHolder... values) {
432        if (mLastAnimator != null) {
433            mLastAnimator.cancel();
434        }
435        if (mLastAnimatorListener != null) {
436            if (ANIMATION_DEBUG) {
437                Log.w(Email.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
438            }
439            // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
440            // we end up cancelling the previous one *after* starting the next one.
441            // Directly tell the listener it's cancelled to avoid that.
442            mLastAnimatorListener.cancel();
443        }
444
445        final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
446                this, values).setDuration(duration);
447        animator.setInterpolator(INTERPOLATOR);
448        if (listener != null) {
449            animator.addListener(listener);
450        }
451        mLastAnimator = animator;
452        mLastAnimatorListener = listener;
453        animator.start();
454    }
455
456    /**
457     * Animation listener.
458     *
459     * Update the visibility of each pane before/after an animation.
460     */
461    private class AnimatorListener implements Animator.AnimatorListener {
462        private final String mLogLabel;
463        private final View[] mViewsToShow;
464        private final View[] mViewsToHide;
465        private final int mPreviousVisiblePanes;
466
467        private boolean mCancelled;
468
469        public AnimatorListener(String logLabel, View[] viewsToShow, View[] viewsToHide,
470                int previousVisiblePanes) {
471            mLogLabel = logLabel;
472            mViewsToShow = viewsToShow;
473            mViewsToHide = viewsToHide;
474            mPreviousVisiblePanes = previousVisiblePanes;
475        }
476
477        private void log(String message) {
478            if (ANIMATION_DEBUG) {
479                Log.w(Email.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
480            }
481        }
482
483        public void cancel() {
484            log("cancel");
485            mCancelled = true;
486        }
487
488        /**
489         * Show the about-to-become-visible panes before an animation.
490         */
491        @Override
492        public void onAnimationStart(Animator animation) {
493            log("start");
494            for (View v : mViewsToShow) {
495                v.setVisibility(View.VISIBLE);
496            }
497            mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
498        }
499
500        @Override
501        public void onAnimationRepeat(Animator animation) {
502        }
503
504        @Override
505        public void onAnimationCancel(Animator animation) {
506        }
507
508        /**
509         * Hide the about-to-become-hidden panes after an animation.
510         */
511        @Override
512        public void onAnimationEnd(Animator animation) {
513            if (mCancelled) {
514                return; // But they shouldn't be hidden when cancelled.
515            }
516            log("end");
517            for (View v : mViewsToHide) {
518                v.setVisibility(View.INVISIBLE);
519            }
520            mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
521        }
522    }
523
524    private static class SavedState extends BaseSavedState {
525        int mPaneState;
526
527        /**
528         * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
529         */
530        SavedState(Parcelable superState) {
531            super(superState);
532        }
533
534        /**
535         * Constructor called from {@link #CREATOR}
536         */
537        private SavedState(Parcel in) {
538            super(in);
539            mPaneState = in.readInt();
540        }
541
542        @Override
543        public void writeToParcel(Parcel out, int flags) {
544            super.writeToParcel(out, flags);
545            out.writeInt(mPaneState);
546        }
547
548        public static final Parcelable.Creator<SavedState> CREATOR
549                = new Parcelable.Creator<SavedState>() {
550            public SavedState createFromParcel(Parcel in) {
551                return new SavedState(in);
552            }
553
554            public SavedState[] newArray(int size) {
555                return new SavedState[size];
556            }
557        };
558    }
559}
560