/******************************************************************************* * Copyright (C) 2012 Google Inc. * Licensed to 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.mail.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import com.android.mail.R; import com.android.mail.ui.ViewMode.ModeChangeListener; import com.android.mail.utils.LogUtils; import com.android.mail.utils.Utils; import com.android.mail.utils.ViewUtils; import com.google.common.annotations.VisibleForTesting; /** * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet) * activity, and the transitions between them. * * This is not intended to be a generic layout; it is specific to the {@code Fragment}s * available in {@link MailActivity} and assumes their existence. It merely configures them * according to the specific modes the {@link Activity} can be in. * * Currently, the layout differs in three dimensions: orientation, two aspects of view modes. * This results in essentially three states: One where the folders are on the left and conversation * list is on the right, and two states where the conversation list is on the left: one in which * it's collapsed and another where it is not. * * In folder or conversation list view, conversations are hidden and folders and conversation lists * are visible. This is the case in both portrait and landscape * * In Conversation List or Conversation View, folders are hidden, and conversation lists and * conversation view is visible. This is the case in both portrait and landscape. * * In the Gmail source code, this was called TriStateSplitLayout */ final class TwoPaneLayout extends FrameLayout implements ModeChangeListener { private static final String LOG_TAG = "TwoPaneLayout"; private static final long SLIDE_DURATION_MS = 300; private final int mDrawerWidthMini; private final int mDrawerWidthOpen; private final double mConversationListWeight; private final TimeInterpolator mSlideInterpolator; /** * If true, this layout group will treat the thread list and conversation view as full-width * panes to switch between.
*
* If false, always show a conversation view right next to the conversation list. This view will * also be populated (preview / "peek" mode) with a default conversation if none is selected by * the user. */ private final boolean mListCollapsible; /** * The current mode that the tablet layout is in. This is a constant integer that holds values * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}. */ private int mCurrentMode = ViewMode.UNKNOWN; /** * This mode represents the current positions of the three panes. This is split out from the * current mode to give context to state transitions. */ private int mPositionedMode = ViewMode.UNKNOWN; private TwoPaneController mController; private LayoutListener mListener; private boolean mIsSearchResult; private View mMiscellaneousView; private View mConversationView; private View mFoldersView; private View mListView; public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane; private final Runnable mTransitionCompleteRunnable = new Runnable() { @Override public void run() { onTransitionComplete(); } }; public TwoPaneLayout(Context context) { this(context, null); } public TwoPaneLayout(Context context, AttributeSet attrs) { super(context, attrs); final Resources res = getResources(); // The conversation list might be visible now, depending on the layout: in portrait we // don't show the conversation list, but in landscape we do. This information is stored // in the constants mListCollapsible = res.getBoolean(R.bool.list_collapsible); mDrawerWidthMini = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_mini); mDrawerWidthOpen = res.getDimensionPixelSize(R.dimen.two_pane_drawer_width_open); mSlideInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.decelerate_cubic); final int convListWeight = res.getInteger(R.integer.conversation_list_weight); final int convViewWeight = res.getInteger(R.integer.conversation_view_weight); mConversationListWeight = (double) convListWeight / (convListWeight + convViewWeight); } @Override protected void onFinishInflate() { super.onFinishInflate(); mFoldersView = findViewById(R.id.drawer); mListView = findViewById(R.id.conversation_list_pane); mConversationView = findViewById(R.id.conversation_pane); mMiscellaneousView = findViewById(MISCELLANEOUS_VIEW_ID); // all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes mCurrentMode = ViewMode.UNKNOWN; mFoldersView.setVisibility(GONE); mListView.setVisibility(GONE); mConversationView.setVisibility(GONE); mMiscellaneousView.setVisibility(GONE); } @VisibleForTesting public void setController(TwoPaneController controller, boolean isSearchResult) { mController = controller; mListener = controller; mIsSearchResult = isSearchResult; ((ConversationViewFrame) mConversationView).setDownEventListener(mController); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this); setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this); positionPanes(getMeasuredWidth()); super.onLayout(changed, l, t, r, b); } /** * Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes * have the correct widths set for the current overall size and view mode. * * @param parentWidth this view's new width */ private void setupPaneWidths(int parentWidth) { // only adjust the pane widths when my width changes if (parentWidth != getMeasuredWidth()) { final int convWidth = computeConversationWidth(parentWidth); setPaneWidth(mMiscellaneousView, convWidth); setPaneWidth(mConversationView, convWidth); setPaneWidth(mListView, computeConversationListWidth(parentWidth)); } } /** * Positions the three sliding panes at the correct X offset (using {@link View#setX(float)}). * When switching from list->conversation mode or vice versa, animate the change in X. * * @param width */ private void positionPanes(int width) { final int convX, listX, foldersX; final boolean isRtl = ViewUtils.isViewRtl(this); final int foldersW = isDrawerOpen() ? mDrawerWidthOpen : mDrawerWidthMini; final int listW = getPaneWidth(mListView); boolean cvOnScreen = true; if (!mListCollapsible) { if (isRtl) { foldersX = width - mDrawerWidthOpen; listX = width - foldersW - listW; convX = listX - getPaneWidth(mConversationView); } else { foldersX = 0; listX = foldersW; convX = listX + listW; } } else { if (mController.getCurrentConversation() != null && !mController.isCurrentConversationJustPeeking()) { // CV mode if (isRtl) { convX = 0; listX = getPaneWidth(mConversationView); foldersX = listX + width - mDrawerWidthOpen; } else { convX = 0; listX = -listW; foldersX = listX - foldersW; } } else { // TL mode cvOnScreen = false; if (isRtl) { foldersX = width - mDrawerWidthOpen; listX = width - foldersW - listW; convX = listX - getPaneWidth(mConversationView); } else { foldersX = 0; listX = foldersW; convX = listX + listW; } } } animatePanes(foldersX, listX, convX); // For views that are not on the screen, let's set their visibility for accessibility. mFoldersView.setVisibility(foldersX >= 0 ? VISIBLE : INVISIBLE); mListView.setVisibility(listX >= 0 ? VISIBLE : INVISIBLE); mConversationView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); mMiscellaneousView.setVisibility(cvOnScreen ? VISIBLE : INVISIBLE); mPositionedMode = mCurrentMode; } private final AnimatorListenerAdapter mPaneAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { useHardwareLayer(false); onTransitionComplete(); } @Override public void onAnimationCancel(Animator animation) { useHardwareLayer(false); } }; private void animatePanes(int foldersX, int listX, int convX) { // If positioning has not yet happened, we don't need to animate panes into place. // This happens on first layout, rotate, and when jumping straight to a conversation from // a view intent. if (mPositionedMode == ViewMode.UNKNOWN) { mConversationView.setX(convX); mMiscellaneousView.setX(convX); mListView.setX(listX); mFoldersView.setX(foldersX); // listeners need to know that the "transition" is complete, even if one is not run. // defer notifying listeners because we're in a layout pass, and they might do layout. post(mTransitionCompleteRunnable); return; } useHardwareLayer(true); if (ViewMode.isAdMode(mCurrentMode)) { mMiscellaneousView.animate().x(convX); } else { mConversationView.animate().x(convX); } mFoldersView.animate().x(foldersX); mListView.animate() .x(listX) .setListener(mPaneAnimationListener); configureAnimations(mConversationView, mFoldersView, mListView, mMiscellaneousView); } private void configureAnimations(View... views) { for (View v : views) { v.animate() .setInterpolator(mSlideInterpolator) .setDuration(SLIDE_DURATION_MS); } } private void useHardwareLayer(boolean useHardware) { final int layerType = useHardware ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE; mFoldersView.setLayerType(layerType, null); mListView.setLayerType(layerType, null); mConversationView.setLayerType(layerType, null); mMiscellaneousView.setLayerType(layerType, null); if (useHardware) { // these buildLayer calls are safe because layout is the only way we get here // (i.e. these views must already be attached) mFoldersView.buildLayer(); mListView.buildLayer(); mConversationView.buildLayer(); mMiscellaneousView.buildLayer(); } } private void onTransitionComplete() { if (mController.isDestroyed()) { // quit early if the hosting activity was destroyed before the animation finished LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early"); return; } switch (mCurrentMode) { case ViewMode.CONVERSATION: case ViewMode.SEARCH_RESULTS_CONVERSATION: dispatchConversationVisibilityChanged(true); dispatchConversationListVisibilityChange(!isConversationListCollapsed()); break; case ViewMode.CONVERSATION_LIST: case ViewMode.SEARCH_RESULTS_LIST: dispatchConversationVisibilityChanged(false); dispatchConversationListVisibilityChange(true); break; case ViewMode.AD: dispatchConversationVisibilityChanged(false); dispatchConversationListVisibilityChange(!isConversationListCollapsed()); break; default: break; } } /** * Computes the width of the conversation list in stable state of the current mode. */ public int computeConversationListWidth() { return computeConversationListWidth(getMeasuredWidth()); } /** * Computes the width of the conversation list in stable state of the current mode. */ private int computeConversationListWidth(int parentWidth) { final int availWidth = parentWidth - mDrawerWidthMini; return mListCollapsible ? availWidth : (int) (availWidth * mConversationListWeight); } public int computeConversationWidth() { return computeConversationWidth(getMeasuredWidth()); } /** * Computes the width of the conversation pane in stable state of the * current mode. */ private int computeConversationWidth(int parentWidth) { return mListCollapsible ? parentWidth : parentWidth - computeConversationListWidth(parentWidth) - mDrawerWidthMini; } private void dispatchConversationListVisibilityChange(boolean visible) { if (mListener != null) { mListener.onConversationListVisibilityChanged(visible); } } private void dispatchConversationVisibilityChanged(boolean visible) { if (mListener != null) { mListener.onConversationVisibilityChanged(visible); } } // does not apply to drawer children. will return zero for those. private int getPaneWidth(View pane) { return pane.getLayoutParams().width; } private boolean isDrawerOpen() { return mController != null && mController.isDrawerOpen(); } /** * @return Whether or not the conversation list is visible on screen. */ @Deprecated public boolean isConversationListCollapsed() { return !ViewMode.isListMode(mCurrentMode) && mListCollapsible; } @Override public void onViewModeChanged(int newMode) { // make all initially GONE panes visible only when the view mode is first determined if (mCurrentMode == ViewMode.UNKNOWN) { mFoldersView.setVisibility(VISIBLE); mListView.setVisibility(VISIBLE); } if (ViewMode.isAdMode(newMode)) { mMiscellaneousView.setVisibility(VISIBLE); mConversationView.setVisibility(GONE); } else { mConversationView.setVisibility(VISIBLE); mMiscellaneousView.setVisibility(GONE); } // detach the pager immediately from its data source (to prevent processing updates) if (ViewMode.isConversationMode(mCurrentMode)) { mController.disablePagerUpdates(); } mCurrentMode = newMode; LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode); // do all the real work in onMeasure/onLayout, when panes are sized and positioned for the // current width/height anyway requestLayout(); } public boolean isModeChangePending() { return mPositionedMode != mCurrentMode; } private void setPaneWidth(View pane, int w) { final ViewGroup.LayoutParams lp = pane.getLayoutParams(); if (lp.width == w) { return; } lp.width = w; pane.setLayoutParams(lp); if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { final String s; if (pane == mFoldersView) { s = "folders"; } else if (pane == mListView) { s = "conv-list"; } else if (pane == mConversationView) { s = "conv-view"; } else if (pane == mMiscellaneousView) { s = "misc-view"; } else { s = "???:" + pane; } LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s); } } public boolean shouldShowPreviewPanel() { return !mListCollapsible; } }