blob: 63511553e6401f6393f12207a6d010945eef400f [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.activity;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import com.android.email.R;
import com.android.emailcommon.Logging;
/**
* The "three pane" layout used on tablet.
*
* This layout can show up to two panes at any given time, and operates in two different modes.
* See {@link #isPaneCollapsible()} for details on the two modes.
*
* TODO Unit tests, when UX is settled.
*
* TODO onVisiblePanesChanged() should be called *AFTER* the animation, not before.
*/
public class ThreePaneLayout extends LinearLayout {
private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true
private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 150;
private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.75f);
/** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */
private static final int STATE_UNINITIALIZED = -1;
/** Mailbox list + message list both visible. */
public static final int STATE_LEFT_VISIBLE = 0;
/**
* A view where the MessageView is visible. The MessageList is visible if
* {@link #isPaneCollapsible} is false, but is otherwise collapsed and hidden.
*/
public static final int STATE_RIGHT_VISIBLE = 1;
/**
* A view where the MessageView is partially visible and a collapsible MessageList on the left
* has been expanded to be in view. {@link #isPaneCollapsible} must return true for this
* state to be active.
*/
public static final int STATE_MIDDLE_EXPANDED = 2;
// Flags for getVisiblePanes()
public static final int PANE_LEFT = 1 << 2;
public static final int PANE_MIDDLE = 1 << 1;
public static final int PANE_RIGHT = 1 << 0;
/** Current pane state. See {@link #changePaneState} */
private int mPaneState = STATE_UNINITIALIZED;
/** See {@link #changePaneState} and {@link #onFirstSizeChanged} */
private int mInitialPaneState = STATE_UNINITIALIZED;
private View mLeftPane;
private View mMiddlePane;
private View mRightPane;
private MessageCommandButtonView mMessageCommandButtons;
private MessageCommandButtonView mInMessageCommandButtons;
private boolean mConvViewExpandList;
private boolean mFirstSizeChangedDone;
/** Mailbox list width. Comes from resources. */
private int mMailboxListWidth;
/**
* Message list width, on:
* - the message list + message view mode, when the left pane is not collapsible
* - the message view + expanded message list mode, when the left pane is collapsible
* Comes from resources.
*/
private int mMessageListWidth;
/** Hold last animator to cancel. */
private Animator mLastAnimator;
/**
* Hold last animator listener to cancel. See {@link #startLayoutAnimation} for why
* we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener}
*/
private AnimatorListener mLastAnimatorListener;
// 2nd index for {@link #changePaneState}
private static final int INDEX_VISIBLE = 0;
private static final int INDEX_INVISIBLE = 1;
private static final int INDEX_GONE = 2;
// Arrays used in {@link #changePaneState}
// First index: STATE_*
// Second index: INDEX_*
private View[][][] mShowHideViews;
private Callback mCallback = EmptyCallback.INSTANCE;
private boolean mIsSearchResult = false;
public interface Callback {
/** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */
public void onVisiblePanesChanged(int previousVisiblePanes);
}
private static final class EmptyCallback implements Callback {
public static final Callback INSTANCE = new EmptyCallback();
@Override public void onVisiblePanesChanged(int previousVisiblePanes) {}
}
public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public ThreePaneLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public ThreePaneLayout(Context context) {
super(context);
initView();
}
/** Perform basic initialization */
private void initView() {
setOrientation(LinearLayout.HORIZONTAL); // Always horizontal
}
@Override
protected Parcelable onSaveInstanceState() {
SavedState ss = new SavedState(super.onSaveInstanceState());
ss.mPaneState = mPaneState;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
// Called after onFinishInflate()
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (mIsSearchResult && UiUtilities.showTwoPaneSearchResults(getContext())) {
mInitialPaneState = STATE_RIGHT_VISIBLE;
} else {
mInitialPaneState = ss.mPaneState;
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mLeftPane = findViewById(R.id.left_pane);
mMiddlePane = findViewById(R.id.middle_pane);
mMessageCommandButtons = (MessageCommandButtonView)
findViewById(R.id.message_command_buttons);
mInMessageCommandButtons = (MessageCommandButtonView)
findViewById(R.id.inmessage_command_buttons);
mRightPane = findViewById(R.id.right_pane);
mConvViewExpandList = getContext().getResources().getBoolean(R.bool.expand_middle_view);
View[][] stateRightVisible = new View[][] {
{
mMiddlePane, mMessageCommandButtons, mRightPane
}, // Visible
{
mLeftPane
}, // Invisible
{
mInMessageCommandButtons
}, // Gone;
};
View[][] stateRightVisibleHideConvList = new View[][] {
{
mRightPane, mInMessageCommandButtons
}, // Visible
{
mMiddlePane, mMessageCommandButtons, mLeftPane
}, // Invisible
{}, // Gone;
};
mShowHideViews = new View[][][] {
// STATE_LEFT_VISIBLE
{
{
mLeftPane, mMiddlePane
}, // Visible
{
mRightPane
}, // Invisible
{
mMessageCommandButtons, mInMessageCommandButtons
}, // Gone
},
// STATE_RIGHT_VISIBLE
mConvViewExpandList ? stateRightVisible : stateRightVisibleHideConvList,
// STATE_MIDDLE_EXPANDED
{
{}, // Visible
{}, // Invisible
{}, // Gone
},
};
mInitialPaneState = STATE_LEFT_VISIBLE;
final Resources resources = getResources();
mMailboxListWidth = getResources().getDimensionPixelSize(
R.dimen.mailbox_list_width);
mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width);
}
public void setIsSearch(boolean isSearch) {
mIsSearchResult = isSearch;
if (mIsSearchResult && UiUtilities.showTwoPaneSearchResults(getContext())) {
mInitialPaneState = STATE_RIGHT_VISIBLE;
if (mPaneState != STATE_RIGHT_VISIBLE) {
changePaneState(STATE_RIGHT_VISIBLE, false);
}
}
}
private boolean shouldShowMailboxList() {
return !mIsSearchResult || UiUtilities.showTwoPaneSearchResults(getContext());
}
public void setCallback(Callback callback) {
mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
}
/**
* Return whether or not the left pane should be collapsible.
*/
public boolean isPaneCollapsible() {
return false;
}
public MessageCommandButtonView getMessageCommandButtons() {
return mMessageCommandButtons;
}
public MessageCommandButtonView getInMessageCommandButtons() {
return mInMessageCommandButtons;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (!mFirstSizeChangedDone) {
mFirstSizeChangedDone = true;
onFirstSizeChanged();
}
}
/**
* @return bit flags for visible panes. Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE}
* and {@link #PANE_RIGHT},
*/
public int getVisiblePanes() {
int ret = 0;
if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT;
if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE;
if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT;
return ret;
}
public boolean isLeftPaneVisible() {
return mLeftPane.getVisibility() == View.VISIBLE;
}
public boolean isMiddlePaneVisible() {
return mMiddlePane.getVisibility() == View.VISIBLE;
}
public boolean isRightPaneVisible() {
return mRightPane.getVisibility() == View.VISIBLE;
}
/**
* Show the left most pane. (i.e. mailbox list)
*/
public boolean showLeftPane() {
return changePaneState(STATE_LEFT_VISIBLE, true);
}
/**
* Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
* can't layout properly. We just remember all the requests to {@link #changePaneState}
* until the first {@link #onSizeChanged}, at which point we actually change to the last
* requested state.
*/
private void onFirstSizeChanged() {
if (mInitialPaneState != STATE_UNINITIALIZED) {
changePaneState(mInitialPaneState, false);
mInitialPaneState = STATE_UNINITIALIZED;
}
}
/**
* Show the right most pane. (i.e. message view)
*/
public boolean showRightPane() {
return changePaneState(STATE_RIGHT_VISIBLE, true);
}
private int getMailboxListWidth() {
if (!shouldShowMailboxList()) {
return 0;
}
return mMailboxListWidth;
}
private boolean changePaneState(int newState, boolean animate) {
if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) {
newState = STATE_RIGHT_VISIBLE;
}
if (!mFirstSizeChangedDone) {
// Before first onSizeChanged(), we don't know the width of the view, so we can't
// layout properly.
// Just remember the new state and return.
mInitialPaneState = newState;
return false;
}
if (newState == mPaneState) {
return false;
}
// Just make sure the first transition doesn't animate.
if (mPaneState == STATE_UNINITIALIZED) {
animate = false;
}
final int previousVisiblePanes = getVisiblePanes();
mPaneState = newState;
// Animate to the new state.
// (We still use animator even if animate == false; we just use 0 duration.)
final int totalWidth = getMeasuredWidth();
final int expectedMailboxLeft;
final int expectedMessageListWidth;
final String animatorLabel; // for debug purpose
setViewWidth(mLeftPane, getMailboxListWidth());
setViewWidth(mRightPane, totalWidth - getMessageListWidth());
switch (mPaneState) {
case STATE_LEFT_VISIBLE:
// mailbox + message list
animatorLabel = "moving to [mailbox list + message list]";
expectedMailboxLeft = 0;
expectedMessageListWidth = totalWidth - getMailboxListWidth();
break;
case STATE_RIGHT_VISIBLE:
// message list + message view
animatorLabel = "moving to [message list + message view]";
expectedMailboxLeft = -getMailboxListWidth();
expectedMessageListWidth = getMessageListWidth();
break;
default:
throw new IllegalStateException();
}
setViewWidth(mMiddlePane, expectedMessageListWidth);
final View[][] showHideViews = mShowHideViews[mPaneState];
final AnimatorListener listener = new AnimatorListener(animatorLabel,
showHideViews[INDEX_VISIBLE],
showHideViews[INDEX_INVISIBLE],
showHideViews[INDEX_GONE],
previousVisiblePanes);
// Animation properties -- mailbox list left and message list width, at the same time.
startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
getCurrentMailboxLeft(), expectedMailboxLeft),
PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
getCurrentMessageListWidth(), expectedMessageListWidth)
);
return true;
}
private int getMessageListWidth() {
if (!mConvViewExpandList && mPaneState == STATE_RIGHT_VISIBLE) {
return 0;
}
return mMessageListWidth;
}
/**
* @return The ID of the view for the left pane fragment. (i.e. mailbox list)
*/
public int getLeftPaneId() {
return R.id.left_pane;
}
/**
* @return The ID of the view for the middle pane fragment. (i.e. message list)
*/
public int getMiddlePaneId() {
return R.id.middle_pane;
}
/**
* @return The ID of the view for the right pane fragment. (i.e. message view)
*/
public int getRightPaneId() {
return R.id.right_pane;
}
private void setViewWidth(View v, int value) {
v.getLayoutParams().width = value;
requestLayout();
}
private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
public void setMailboxListLeftAnim(int value) {
((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
requestLayout();
}
public void setMessageListWidthAnim(int value) {
setViewWidth(mMiddlePane, value);
}
private int getCurrentMailboxLeft() {
return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
}
private int getCurrentMessageListWidth() {
return mMiddlePane.getLayoutParams().width;
}
/**
* Helper method to start animation.
*/
private void startLayoutAnimation(int duration, AnimatorListener listener,
PropertyValuesHolder... values) {
if (mLastAnimator != null) {
mLastAnimator.cancel();
}
if (mLastAnimatorListener != null) {
if (ANIMATION_DEBUG) {
Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
}
// Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
// we end up cancelling the previous one *after* starting the next one.
// Directly tell the listener it's cancelled to avoid that.
mLastAnimatorListener.cancel();
}
final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
this, values).setDuration(duration);
animator.setInterpolator(INTERPOLATOR);
if (listener != null) {
animator.addListener(listener);
}
mLastAnimator = animator;
mLastAnimatorListener = listener;
animator.start();
}
/**
* Get the state of the view. Returns ones of: STATE_UNINITIALIZED,
* STATE_LEFT_VISIBLE, STATE_MIDDLE_EXPANDED, STATE_RIGHT_VISIBLE
*/
public int getPaneState() {
return mPaneState;
}
/**
* Animation listener.
*
* Update the visibility of each pane before/after an animation.
*/
private class AnimatorListener implements Animator.AnimatorListener {
private final String mLogLabel;
private final View[] mViewsVisible;
private final View[] mViewsInvisible;
private final View[] mViewsGone;
private final int mPreviousVisiblePanes;
private boolean mCancelled;
public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible,
View[] viewsGone, int previousVisiblePanes) {
mLogLabel = logLabel;
mViewsVisible = viewsVisible;
mViewsInvisible = viewsInvisible;
mViewsGone = viewsGone;
mPreviousVisiblePanes = previousVisiblePanes;
}
private void log(String message) {
if (ANIMATION_DEBUG) {
Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
}
}
public void cancel() {
log("cancel");
mCancelled = true;
}
/**
* Show the about-to-become-visible panes before an animation.
*/
@Override
public void onAnimationStart(Animator animation) {
log("start");
for (View v : mViewsVisible) {
v.setVisibility(View.VISIBLE);
}
// TODO These things, making invisible views and calling the visible pane changed
// callback, should really be done in onAnimationEnd.
// However, because we may want to initiate a fragment transaction in the callback but
// by the time animation is done, the activity may be stopped (by user's HOME press),
// it's not easy to get right. For now, we just do this before the animation.
for (View v : mViewsInvisible) {
v.setVisibility(View.INVISIBLE);
}
for (View v : mViewsGone) {
v.setVisibility(View.GONE);
}
mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
/**
* Hide the about-to-become-hidden panes after an animation.
*/
@Override
public void onAnimationEnd(Animator animation) {
if (mCancelled) {
return; // But they shouldn't be hidden when cancelled.
}
log("end");
}
}
private static class SavedState extends BaseSavedState {
int mPaneState;
/**
* Constructor called from {@link ThreePaneLayout#onSaveInstanceState()}
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
mPaneState = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(mPaneState);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}