ThreePaneLayout.java revision d6decef1d2a8d14aa8a65229bc784e6fdbb31864
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 boolean mConvViewExpandList;
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 static final int INDEX_VISIBLE = 0;
112    private static final int INDEX_INVISIBLE = 1;
113    private static 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    private boolean mIsSearchResult = false;
123
124    public interface Callback {
125        /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */
126        public void onVisiblePanesChanged(int previousVisiblePanes);
127    }
128
129    private static final class EmptyCallback implements Callback {
130        public static final Callback INSTANCE = new EmptyCallback();
131
132        @Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
133    }
134
135    public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
136        super(context, attrs, defStyle);
137        initView();
138    }
139
140    public ThreePaneLayout(Context context, AttributeSet attrs) {
141        super(context, attrs);
142        initView();
143    }
144
145    public ThreePaneLayout(Context context) {
146        super(context);
147        initView();
148    }
149
150    /** Perform basic initialization */
151    private void initView() {
152        setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
153    }
154
155    @Override
156    protected void onFinishInflate() {
157        super.onFinishInflate();
158
159        mLeftPane = findViewById(R.id.left_pane);
160        mMiddlePane = findViewById(R.id.middle_pane);
161        mMessageCommandButtons = (MessageCommandButtonView) findViewById(R.id.message_command_buttons);
162
163        mRightPane = findViewById(R.id.right_pane);
164        mConvViewExpandList = getContext().getResources().getBoolean(R.bool.expand_middle_view);
165        View[][] stateRightVisible = new View[][] {
166                {
167                    mMiddlePane, mMessageCommandButtons, mRightPane
168                }, // Visible
169                {
170                    mLeftPane
171                }, // Invisible
172                {}, // Gone;
173        };
174        View[][] stateRightVisibleHideConvList = new View[][] {
175                {
176                        mRightPane
177                }, // Visible
178                {
179                        mMiddlePane, mMessageCommandButtons, mLeftPane
180                }, // Invisible
181                {}, // Gone;
182        };
183        mShowHideViews = new View[][][] {
184                // STATE_LEFT_VISIBLE
185                {
186                        {
187                           mLeftPane, mMiddlePane
188                        }, // Visible
189                        {
190                            mRightPane
191                        }, // Invisible
192                        {
193                            mMessageCommandButtons
194                        }, // Gone
195                },
196                // STATE_RIGHT_VISIBLE
197                mConvViewExpandList ? stateRightVisible : stateRightVisibleHideConvList,
198                // STATE_MIDDLE_EXPANDED
199                {
200                        {}, // Visible
201                        {}, // Invisible
202                        {}, // Gone
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    public void setIsSearch(boolean isSearch) {
215        mIsSearchResult = isSearch;
216    }
217
218    private boolean shouldShowMailboxList() {
219        return !mIsSearchResult || UiUtilities.showTwoPaneSearchResults(getContext());
220    }
221
222    public void setCallback(Callback callback) {
223        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
224    }
225
226    /**
227     * Return whether or not the left pane should be collapsible.
228     */
229    public boolean isPaneCollapsible() {
230        return false;
231    }
232
233    public MessageCommandButtonView getMessageCommandButtons() {
234        return mMessageCommandButtons;
235    }
236
237    @Override
238    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
239        super.onSizeChanged(w, h, oldw, oldh);
240        if (!mFirstSizeChangedDone) {
241            mFirstSizeChangedDone = true;
242            onFirstSizeChanged();
243        }
244    }
245
246    /**
247     * @return bit flags for visible panes.  Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE}
248     * and {@link #PANE_RIGHT},
249     */
250    public int getVisiblePanes() {
251        int ret = 0;
252        if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT;
253        if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE;
254        if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT;
255        return ret;
256    }
257
258    public boolean isLeftPaneVisible() {
259        return mLeftPane.getVisibility() == View.VISIBLE;
260    }
261    public boolean isMiddlePaneVisible() {
262        return mMiddlePane.getVisibility() == View.VISIBLE;
263    }
264    public boolean isRightPaneVisible() {
265        return mRightPane.getVisibility() == View.VISIBLE;
266    }
267
268    /**
269     * Show the left most pane.  (i.e. mailbox list)
270     */
271    public boolean showLeftPane() {
272        return changePaneState(STATE_LEFT_VISIBLE, true);
273    }
274
275    /**
276     * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
277     * can't layout properly.  We just remember all the requests to {@link #changePaneState}
278     * until the first {@link #onSizeChanged}, at which point we actually change to the last
279     * requested state.
280     */
281    private void onFirstSizeChanged() {
282        if (mInitialPaneState != STATE_UNINITIALIZED) {
283            changePaneState(mInitialPaneState, false);
284            mInitialPaneState = STATE_UNINITIALIZED;
285        }
286    }
287
288    /**
289     * Show the right most pane.  (i.e. message view)
290     */
291    public boolean showRightPane() {
292        return changePaneState(STATE_RIGHT_VISIBLE, true);
293    }
294
295    private int getMailboxListWidth() {
296        if (!shouldShowMailboxList()) {
297            return 0;
298        }
299        return mMailboxListWidth;
300    }
301
302    private boolean changePaneState(int newState, boolean animate) {
303        if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) {
304            newState = STATE_RIGHT_VISIBLE;
305        }
306        if (!mFirstSizeChangedDone) {
307            // Before first onSizeChanged(), we don't know the width of the view, so we can't
308            // layout properly.
309            // Just remember the new state and return.
310            mInitialPaneState = newState;
311            return false;
312        }
313        if (newState == mPaneState) {
314            return false;
315        }
316        // Just make sure the first transition doesn't animate.
317        if (mPaneState == STATE_UNINITIALIZED) {
318            animate = false;
319        }
320
321        final int previousVisiblePanes = getVisiblePanes();
322        mPaneState = newState;
323
324        // Animate to the new state.
325        // (We still use animator even if animate == false; we just use 0 duration.)
326        final int totalWidth = getMeasuredWidth();
327
328        final int expectedMailboxLeft;
329        final int expectedMessageListWidth;
330
331        final String animatorLabel; // for debug purpose
332
333        setViewWidth(mLeftPane, getMailboxListWidth());
334        setViewWidth(mRightPane, totalWidth - getMessageListWidth());
335
336        switch (mPaneState) {
337            case STATE_LEFT_VISIBLE:
338                // mailbox + message list
339                animatorLabel = "moving to [mailbox list + message list]";
340                expectedMailboxLeft = 0;
341                expectedMessageListWidth = totalWidth - getMailboxListWidth();
342                break;
343            case STATE_RIGHT_VISIBLE:
344                // message list + message view
345                animatorLabel = "moving to [message list + message view]";
346                expectedMailboxLeft = -getMailboxListWidth();
347                expectedMessageListWidth = getMessageListWidth();
348                break;
349            default:
350                throw new IllegalStateException();
351        }
352        setViewWidth(mMiddlePane, expectedMessageListWidth);
353        final View[][] showHideViews = mShowHideViews[mPaneState];
354        final AnimatorListener listener = new AnimatorListener(animatorLabel,
355                showHideViews[INDEX_VISIBLE],
356                showHideViews[INDEX_INVISIBLE],
357                showHideViews[INDEX_GONE],
358                previousVisiblePanes);
359
360        // Animation properties -- mailbox list left and message list width, at the same time.
361        startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
362                PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
363                        getCurrentMailboxLeft(), expectedMailboxLeft),
364                PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
365                        getCurrentMessageListWidth(), expectedMessageListWidth)
366                );
367        return true;
368    }
369
370    private int getMessageListWidth() {
371        if (!mConvViewExpandList && mPaneState == STATE_RIGHT_VISIBLE) {
372            return 0;
373        }
374        return mMessageListWidth;
375    }
376    /**
377     * @return The ID of the view for the left pane fragment.  (i.e. mailbox list)
378     */
379    public int getLeftPaneId() {
380        return R.id.left_pane;
381    }
382
383    /**
384     * @return The ID of the view for the middle pane fragment.  (i.e. message list)
385     */
386    public int getMiddlePaneId() {
387        return R.id.middle_pane;
388    }
389
390    /**
391     * @return The ID of the view for the right pane fragment.  (i.e. message view)
392     */
393    public int getRightPaneId() {
394        return R.id.right_pane;
395    }
396
397    private void setViewWidth(View v, int value) {
398        v.getLayoutParams().width = value;
399        requestLayout();
400    }
401
402    private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
403    private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
404
405    public void setMailboxListLeftAnim(int value) {
406        ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
407        requestLayout();
408    }
409
410    public void setMessageListWidthAnim(int value) {
411        setViewWidth(mMiddlePane, value);
412    }
413
414    private int getCurrentMailboxLeft() {
415        return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
416    }
417
418    private int getCurrentMessageListWidth() {
419        return mMiddlePane.getLayoutParams().width;
420    }
421
422    /**
423     * Helper method to start animation.
424     */
425    private void startLayoutAnimation(int duration, AnimatorListener listener,
426            PropertyValuesHolder... values) {
427        if (mLastAnimator != null) {
428            mLastAnimator.cancel();
429        }
430        if (mLastAnimatorListener != null) {
431            if (ANIMATION_DEBUG) {
432                Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
433            }
434            // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
435            // we end up cancelling the previous one *after* starting the next one.
436            // Directly tell the listener it's cancelled to avoid that.
437            mLastAnimatorListener.cancel();
438        }
439
440        final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
441                this, values).setDuration(duration);
442        animator.setInterpolator(INTERPOLATOR);
443        if (listener != null) {
444            animator.addListener(listener);
445        }
446        mLastAnimator = animator;
447        mLastAnimatorListener = listener;
448        animator.start();
449    }
450
451    /**
452     * Get the state of the view. Returns ones of: STATE_UNINITIALIZED,
453     * STATE_LEFT_VISIBLE, STATE_MIDDLE_EXPANDED, STATE_RIGHT_VISIBLE
454     */
455    public int getPaneState() {
456        return mPaneState;
457    }
458    /**
459     * Animation listener.
460     *
461     * Update the visibility of each pane before/after an animation.
462     */
463    private class AnimatorListener implements Animator.AnimatorListener {
464        private final String mLogLabel;
465        private final View[] mViewsVisible;
466        private final View[] mViewsInvisible;
467        private final View[] mViewsGone;
468        private final int mPreviousVisiblePanes;
469
470        private boolean mCancelled;
471
472        public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible,
473                View[] viewsGone, int previousVisiblePanes) {
474            mLogLabel = logLabel;
475            mViewsVisible = viewsVisible;
476            mViewsInvisible = viewsInvisible;
477            mViewsGone = viewsGone;
478            mPreviousVisiblePanes = previousVisiblePanes;
479        }
480
481        private void log(String message) {
482            if (ANIMATION_DEBUG) {
483                Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
484            }
485        }
486
487        public void cancel() {
488            log("cancel");
489            mCancelled = true;
490        }
491
492        /**
493         * Show the about-to-become-visible panes before an animation.
494         */
495        @Override
496        public void onAnimationStart(Animator animation) {
497            log("start");
498            for (View v : mViewsVisible) {
499                v.setVisibility(View.VISIBLE);
500            }
501
502            // TODO These things, making invisible views and calling the visible pane changed
503            // callback, should really be done in onAnimationEnd.
504            // However, because we may want to initiate a fragment transaction in the callback but
505            // by the time animation is done, the activity may be stopped (by user's HOME press),
506            // it's not easy to get right.  For now, we just do this before the animation.
507            for (View v : mViewsInvisible) {
508                v.setVisibility(View.INVISIBLE);
509            }
510            for (View v : mViewsGone) {
511                v.setVisibility(View.GONE);
512            }
513            mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
514        }
515
516        @Override
517        public void onAnimationRepeat(Animator animation) {
518        }
519
520        @Override
521        public void onAnimationCancel(Animator animation) {
522        }
523
524        /**
525         * Hide the about-to-become-hidden panes after an animation.
526         */
527        @Override
528        public void onAnimationEnd(Animator animation) {
529            if (mCancelled) {
530                return; // But they shouldn't be hidden when cancelled.
531            }
532            log("end");
533        }
534    }
535
536    private static class SavedState extends BaseSavedState {
537        int mPaneState;
538
539        /**
540         * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
541         */
542        SavedState(Parcelable superState) {
543            super(superState);
544        }
545
546        /**
547         * Constructor called from {@link #CREATOR}
548         */
549        private SavedState(Parcel in) {
550            super(in);
551            mPaneState = in.readInt();
552        }
553
554        @Override
555        public void writeToParcel(Parcel out, int flags) {
556            super.writeToParcel(out, flags);
557            out.writeInt(mPaneState);
558        }
559
560        public static final Parcelable.Creator<SavedState> CREATOR
561                = new Parcelable.Creator<SavedState>() {
562            public SavedState createFromParcel(Parcel in) {
563                return new SavedState(in);
564            }
565
566            public SavedState[] newArray(int size) {
567                return new SavedState[size];
568            }
569        };
570    }
571}
572