replicant-packages_apps_Email/src/com/android/email/activity/ThreePaneLayout.java

560 lines
19 KiB
Java

/*
* 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 com.android.email.Email;
import com.android.email.R;
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;
/**
* The "three pane" layout used on tablet.
*
* It'll encapsulate the behavioral differences between portrait mode and landscape mode.
*
* TODO Unit tests, when UX is settled.
*/
public class ThreePaneLayout extends LinearLayout implements View.OnClickListener {
private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true
// STOPSHIP Make sure we're using the same parameters as gmail does
private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 80;
private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.5f);
/** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */
private static final int STATE_UNINITIALIZED = 0;
/** Mailbox list + message list */
private static final int STATE_LEFT_VISIBLE = 1;
/** Message view on portrait, + message list on landscape. */
private static final int STATE_RIGHT_VISIBLE = 2;
/** Portrait mode only: message view + expanded message list */
private static final int STATE_PORTRAIT_MIDDLE_EXPANDED = 3;
// 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;
// Views used only on portrait
private View mFoggedGlass;
private boolean mFirstSizeChangedDone;
/** Mailbox list width. Comes from resources. */
private int mMailboxListWidth;
/**
* Message list width, on:
* - the message list + message view mode, on landscape.
* - the message view + expanded message list mode, on portrait.
* 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;
// Arrays used in {@link #changePaneState}
private View[] mViewsLeft;
private View[] mViewsRight;
private View[] mViewsLeftMiddle;
private View[] mViewsMiddleRightFogged;
private View[] mViewsLeftMiddleFogged;
private Callback mCallback = EmptyCallback.INSTANCE;
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 void onFinishInflate() {
super.onFinishInflate();
mLeftPane = findViewById(R.id.left_pane);
mMiddlePane = findViewById(R.id.middle_pane);
mFoggedGlass = findViewById(R.id.fogged_glass);
if (mFoggedGlass != null) { // If it's around, it's portrait.
mRightPane = findViewById(R.id.right_pane_with_fog);
mFoggedGlass.setOnClickListener(this);
} else { // landscape
mRightPane = findViewById(R.id.right_pane);
}
mViewsLeft = new View[] {mLeftPane};
mViewsRight = new View[] {mRightPane};
mViewsLeftMiddle = new View[] {mLeftPane, mMiddlePane};
mViewsMiddleRightFogged = new View[] {mMiddlePane, mRightPane, mFoggedGlass};
mViewsLeftMiddleFogged = new View[] {mLeftPane, mMiddlePane, mFoggedGlass};
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 setCallback(Callback callback) {
mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
}
private boolean isLandscape() {
return mFoggedGlass == null;
}
@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());
mInitialPaneState = ss.mPaneState;
}
@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 onBackPressed() {
if (isLandscape()) {
switch (mPaneState) {
case STATE_RIGHT_VISIBLE:
changePaneState(STATE_LEFT_VISIBLE, true); // Close the right pane
return true;
}
} else {
switch (mPaneState) {
case STATE_RIGHT_VISIBLE:
changePaneState(STATE_PORTRAIT_MIDDLE_EXPANDED, true);
return true;
case STATE_PORTRAIT_MIDDLE_EXPANDED:
changePaneState(STATE_LEFT_VISIBLE, true);
return true;
}
}
return false;
}
/**
* Show the left most pane. (i.e. mailbox list)
*/
public void showLeftPane() {
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 void showRightPane() {
changePaneState(STATE_RIGHT_VISIBLE, true);
}
private void changePaneState(int newState, boolean animate) {
if (isLandscape() && (newState == STATE_PORTRAIT_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;
}
if (newState == mPaneState) {
return;
}
// 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
final View[] viewsToShow;
final View[] viewsToHide;
if (isLandscape()) { // Landscape
setViewWidth(mLeftPane, mMailboxListWidth);
setViewWidth(mRightPane, totalWidth - mMessageListWidth);
switch (mPaneState) {
case STATE_LEFT_VISIBLE:
// mailbox + message list
animatorLabel = "moving to [mailbox list + message list]";
expectedMailboxLeft = 0;
expectedMessageListWidth = totalWidth - mMailboxListWidth;
viewsToShow = mViewsLeft;
viewsToHide = mViewsRight;
break;
case STATE_RIGHT_VISIBLE:
// message list + message view
animatorLabel = "moving to [message list + message view]";
expectedMailboxLeft = -mMailboxListWidth;
expectedMessageListWidth = mMessageListWidth;
viewsToShow = mViewsRight;
viewsToHide = mViewsLeft;
break;
default:
throw new IllegalStateException();
}
} else { // Portrait
setViewWidth(mLeftPane, mMailboxListWidth);
setViewWidth(mRightPane, totalWidth);
switch (mPaneState) {
case STATE_LEFT_VISIBLE:
// message list + Message view -> mailbox + message list
animatorLabel = "moving to [mailbox list + message list]";
expectedMailboxLeft = 0;
expectedMessageListWidth = totalWidth - mMailboxListWidth;
viewsToShow = mViewsLeftMiddle;
viewsToHide = mViewsRight;
break;
case STATE_PORTRAIT_MIDDLE_EXPANDED:
// mailbox + message list -> message list + message view
animatorLabel = "moving to [message list + message view]";
expectedMailboxLeft = -mMailboxListWidth;
expectedMessageListWidth = mMessageListWidth;
viewsToShow = mViewsMiddleRightFogged;
viewsToHide = mViewsLeft;
break;
case STATE_RIGHT_VISIBLE:
// message view only
animatorLabel = "moving to [message view]";
expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth);
expectedMessageListWidth = mMessageListWidth;
viewsToShow = mViewsRight;
viewsToHide = mViewsLeftMiddleFogged;
break;
default:
throw new IllegalStateException();
}
}
final AnimatorListener listener = new AnimatorListener(animatorLabel, viewsToShow,
viewsToHide, 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 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;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.fogged_glass:
if (isLandscape()) {
return; // Shouldn't happen
}
changePaneState(STATE_RIGHT_VISIBLE, true);
break;
}
}
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";
@SuppressWarnings("unused")
public void setMailboxListLeftAnim(int value) {
((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
requestLayout();
}
@SuppressWarnings("unused")
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(Email.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();
}
/**
* 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[] mViewsToShow;
private final View[] mViewsToHide;
private final int mPreviousVisiblePanes;
private boolean mCancelled;
public AnimatorListener(String logLabel, View[] viewsToShow, View[] viewsToHide,
int previousVisiblePanes) {
mLogLabel = logLabel;
mViewsToShow = viewsToShow;
mViewsToHide = viewsToHide;
mPreviousVisiblePanes = previousVisiblePanes;
}
private void log(String message) {
if (ANIMATION_DEBUG) {
Log.w(Email.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 : mViewsToShow) {
v.setVisibility(View.VISIBLE);
}
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");
for (View v : mViewsToHide) {
v.setVisibility(View.INVISIBLE);
}
mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
}
}
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];
}
};
}
}