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