• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.activity;
18 
19 import android.animation.Animator;
20 import android.animation.ObjectAnimator;
21 import android.animation.PropertyValuesHolder;
22 import android.animation.TimeInterpolator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.animation.DecelerateInterpolator;
32 import android.widget.LinearLayout;
33 
34 import com.android.email.R;
35 import 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  */
47 public 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. */
onVisiblePanesChanged(int previousVisiblePanes)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 
onVisiblePanesChanged(int previousVisiblePanes)133         @Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
134     }
135 
ThreePaneLayout(Context context, AttributeSet attrs, int defStyle)136     public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
137         super(context, attrs, defStyle);
138         initView();
139     }
140 
ThreePaneLayout(Context context, AttributeSet attrs)141     public ThreePaneLayout(Context context, AttributeSet attrs) {
142         super(context, attrs);
143         initView();
144     }
145 
ThreePaneLayout(Context context)146     public ThreePaneLayout(Context context) {
147         super(context);
148         initView();
149     }
150 
151     /** Perform basic initialization */
initView()152     private void initView() {
153         setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
154     }
155 
156     @Override
onSaveInstanceState()157     protected Parcelable onSaveInstanceState() {
158         SavedState ss = new SavedState(super.onSaveInstanceState());
159         ss.mPaneState = mPaneState;
160         return ss;
161     }
162 
163     @Override
onRestoreInstanceState(Parcelable state)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
onFinishInflate()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 
setIsSearch(boolean isSearch)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 
shouldShowMailboxList()249     private boolean shouldShowMailboxList() {
250         return !mIsSearchResult || UiUtilities.showTwoPaneSearchResults(getContext());
251     }
252 
setCallback(Callback callback)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      */
isPaneCollapsible()260     public boolean isPaneCollapsible() {
261         return false;
262     }
263 
getMessageCommandButtons()264     public MessageCommandButtonView getMessageCommandButtons() {
265         return mMessageCommandButtons;
266     }
267 
getInMessageCommandButtons()268     public MessageCommandButtonView getInMessageCommandButtons() {
269         return mInMessageCommandButtons;
270     }
271 
272     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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      */
getVisiblePanes()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 
isLeftPaneVisible()293     public boolean isLeftPaneVisible() {
294         return mLeftPane.getVisibility() == View.VISIBLE;
295     }
isMiddlePaneVisible()296     public boolean isMiddlePaneVisible() {
297         return mMiddlePane.getVisibility() == View.VISIBLE;
298     }
isRightPaneVisible()299     public boolean isRightPaneVisible() {
300         return mRightPane.getVisibility() == View.VISIBLE;
301     }
302 
303     /**
304      * Show the left most pane.  (i.e. mailbox list)
305      */
showLeftPane()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      */
onFirstSizeChanged()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      */
showRightPane()326     public boolean showRightPane() {
327         return changePaneState(STATE_RIGHT_VISIBLE, true);
328     }
329 
getMailboxListWidth()330     private int getMailboxListWidth() {
331         if (!shouldShowMailboxList()) {
332             return 0;
333         }
334         return mMailboxListWidth;
335     }
336 
changePaneState(int newState, boolean animate)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 
getMessageListWidth()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      */
getLeftPaneId()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      */
getMiddlePaneId()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      */
getRightPaneId()428     public int getRightPaneId() {
429         return R.id.right_pane;
430     }
431 
setViewWidth(View v, int value)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 
setMailboxListLeftAnim(int value)440     public void setMailboxListLeftAnim(int value) {
441         ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
442         requestLayout();
443     }
444 
setMessageListWidthAnim(int value)445     public void setMessageListWidthAnim(int value) {
446         setViewWidth(mMiddlePane, value);
447     }
448 
getCurrentMailboxLeft()449     private int getCurrentMailboxLeft() {
450         return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
451     }
452 
getCurrentMessageListWidth()453     private int getCurrentMessageListWidth() {
454         return mMiddlePane.getLayoutParams().width;
455     }
456 
457     /**
458      * Helper method to start animation.
459      */
startLayoutAnimation(int duration, AnimatorListener listener, PropertyValuesHolder... values)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      */
getPaneState()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 
AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible, View[] viewsGone, int previousVisiblePanes)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 
log(String message)516         private void log(String message) {
517             if (ANIMATION_DEBUG) {
518                 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
519             }
520         }
521 
cancel()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
onAnimationStart(Animator animation)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
onAnimationRepeat(Animator animation)552         public void onAnimationRepeat(Animator animation) {
553         }
554 
555         @Override
onAnimationCancel(Animator animation)556         public void onAnimationCancel(Animator animation) {
557         }
558 
559         /**
560          * Hide the about-to-become-hidden panes after an animation.
561          */
562         @Override
onAnimationEnd(Animator animation)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          */
SavedState(Parcelable superState)577         SavedState(Parcelable superState) {
578             super(superState);
579         }
580 
581         /**
582          * Constructor called from {@link #CREATOR}
583          */
SavedState(Parcel in)584         private SavedState(Parcel in) {
585             super(in);
586             mPaneState = in.readInt();
587         }
588 
589         @Override
writeToParcel(Parcel out, int flags)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