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 implements View.OnClickListener { 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 private 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 private 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 private 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 88 // Views used only when the left pane is collapsible. 89 private View mFoggedGlass; 90 91 private boolean mFirstSizeChangedDone; 92 93 /** Mailbox list width. Comes from resources. */ 94 private int mMailboxListWidth; 95 /** 96 * Message list width, on: 97 * - the message list + message view mode, when the left pane is not collapsible 98 * - the message view + expanded message list mode, when the left pane is collapsible 99 * Comes from resources. 100 */ 101 private int mMessageListWidth; 102 103 /** Hold last animator to cancel. */ 104 private Animator mLastAnimator; 105 106 /** 107 * Hold last animator listener to cancel. See {@link #startLayoutAnimation} for why 108 * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener} 109 */ 110 private AnimatorListener mLastAnimatorListener; 111 112 // 2nd index for {@link #changePaneState} 113 private static final int INDEX_VISIBLE = 0; 114 private static final int INDEX_INVISIBLE = 1; 115 private static final int INDEX_GONE = 2; 116 117 // Arrays used in {@link #changePaneState} 118 // First index: STATE_* 119 // Second index: INDEX_* 120 private View[][][] mShowHideViews; 121 122 private Callback mCallback = EmptyCallback.INSTANCE; 123 124 public interface Callback { 125 /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */ 126 public void onVisiblePanesChanged(int previousVisiblePanes); 127 } 128 129 private static final class EmptyCallback implements Callback { 130 public static final Callback INSTANCE = new EmptyCallback(); 131 132 @Override public void onVisiblePanesChanged(int previousVisiblePanes) {} 133 } 134 135 public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) { 136 super(context, attrs, defStyle); 137 initView(); 138 } 139 140 public ThreePaneLayout(Context context, AttributeSet attrs) { 141 super(context, attrs); 142 initView(); 143 } 144 145 public ThreePaneLayout(Context context) { 146 super(context); 147 initView(); 148 } 149 150 /** Perform basic initialization */ 151 private void initView() { 152 setOrientation(LinearLayout.HORIZONTAL); // Always horizontal 153 } 154 155 @Override 156 protected void onFinishInflate() { 157 super.onFinishInflate(); 158 159 mLeftPane = findViewById(R.id.left_pane); 160 mMiddlePane = findViewById(R.id.middle_pane); 161 mMessageCommandButtons = 162 (MessageCommandButtonView) findViewById(R.id.message_command_buttons); 163 164 mFoggedGlass = findViewById(R.id.fogged_glass); 165 if (mFoggedGlass != null) { 166 mRightPane = findViewById(R.id.right_pane_with_fog); 167 mFoggedGlass.setOnClickListener(this); 168 } else { 169 mRightPane = findViewById(R.id.right_pane); 170 } 171 172 if (!isPaneCollapsible()) { 173 mShowHideViews = new View[][][] { 174 // STATE_LEFT_VISIBLE 175 { 176 {mLeftPane, mMiddlePane}, // Visible 177 {mRightPane}, // Invisible 178 {mMessageCommandButtons}, // Gone 179 }, 180 // STATE_RIGHT_VISIBLE 181 { 182 {mMiddlePane, mMessageCommandButtons, mRightPane}, // Visible 183 {mLeftPane}, // Invisible 184 {}, // Gone 185 }, 186 // STATE_MIDDLE_EXPANDED 187 { 188 {}, // Visible 189 {}, // Invisible 190 {}, // Gone 191 }, 192 }; 193 } else { 194 mShowHideViews = new View[][][] { 195 // STATE_LEFT_VISIBLE 196 { 197 {mLeftPane, mMiddlePane}, // Visible 198 {mRightPane, mFoggedGlass}, // Invisible 199 {mMessageCommandButtons}, // Gone 200 }, 201 // STATE_RIGHT_VISIBLE 202 { 203 {mRightPane, mMessageCommandButtons}, // Visible 204 {mLeftPane, mMiddlePane, mFoggedGlass}, // Invisible 205 {}, // Gone 206 }, 207 // STATE_MIDDLE_EXPANDED 208 { 209 {mMiddlePane, mRightPane, mMessageCommandButtons, mFoggedGlass}, // Visible 210 {mLeftPane}, // Invisible 211 {}, // Gone 212 }, 213 }; 214 } 215 216 mInitialPaneState = STATE_LEFT_VISIBLE; 217 218 final Resources resources = getResources(); 219 mMailboxListWidth = getResources().getDimensionPixelSize( 220 R.dimen.mailbox_list_width); 221 mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width); 222 } 223 224 225 public void setCallback(Callback callback) { 226 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 227 } 228 229 /** 230 * Return whether or not the left pane should be collapsible. 231 */ 232 public boolean isPaneCollapsible() { 233 return mFoggedGlass != null; 234 } 235 236 public MessageCommandButtonView getMessageCommandButtons() { 237 return mMessageCommandButtons; 238 } 239 240 @Override 241 protected Parcelable onSaveInstanceState() { 242 SavedState ss = new SavedState(super.onSaveInstanceState()); 243 ss.mPaneState = mPaneState; 244 return ss; 245 } 246 247 @Override 248 protected void onRestoreInstanceState(Parcelable state) { 249 // Called after onFinishInflate() 250 SavedState ss = (SavedState) state; 251 super.onRestoreInstanceState(ss.getSuperState()); 252 mInitialPaneState = ss.mPaneState; 253 } 254 255 @Override 256 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 257 super.onSizeChanged(w, h, oldw, oldh); 258 if (!mFirstSizeChangedDone) { 259 mFirstSizeChangedDone = true; 260 onFirstSizeChanged(); 261 } 262 } 263 264 /** 265 * @return bit flags for visible panes. Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE} 266 * and {@link #PANE_RIGHT}, 267 */ 268 public int getVisiblePanes() { 269 int ret = 0; 270 if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT; 271 if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE; 272 if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT; 273 return ret; 274 } 275 276 public boolean isLeftPaneVisible() { 277 return mLeftPane.getVisibility() == View.VISIBLE; 278 } 279 public boolean isMiddlePaneVisible() { 280 return mMiddlePane.getVisibility() == View.VISIBLE; 281 } 282 public boolean isRightPaneVisible() { 283 return mRightPane.getVisibility() == View.VISIBLE; 284 } 285 286 /** 287 * Handles the back event. 288 * 289 */ 290 public boolean uncollapsePane() { 291 if (!isPaneCollapsible()) { 292 return false; 293 } 294 295 if (mPaneState == STATE_RIGHT_VISIBLE) { 296 return changePaneState(STATE_MIDDLE_EXPANDED, true); 297 } else if (mInitialPaneState == STATE_RIGHT_VISIBLE) { 298 mInitialPaneState = STATE_MIDDLE_EXPANDED; 299 return true; 300 } 301 302 return false; 303 } 304 305 /** 306 * Show the left most pane. (i.e. mailbox list) 307 */ 308 public boolean showLeftPane() { 309 return changePaneState(STATE_LEFT_VISIBLE, true); 310 } 311 312 /** 313 * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we 314 * can't layout properly. We just remember all the requests to {@link #changePaneState} 315 * until the first {@link #onSizeChanged}, at which point we actually change to the last 316 * requested state. 317 */ 318 private void onFirstSizeChanged() { 319 if (mInitialPaneState != STATE_UNINITIALIZED) { 320 changePaneState(mInitialPaneState, false); 321 mInitialPaneState = STATE_UNINITIALIZED; 322 } 323 } 324 325 /** 326 * Show the right most pane. (i.e. message view) 327 */ 328 public boolean showRightPane() { 329 return changePaneState(STATE_RIGHT_VISIBLE, true); 330 } 331 332 private boolean changePaneState(int newState, boolean animate) { 333 if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) { 334 newState = STATE_RIGHT_VISIBLE; 335 } 336 if (!mFirstSizeChangedDone) { 337 // Before first onSizeChanged(), we don't know the width of the view, so we can't 338 // layout properly. 339 // Just remember the new state and return. 340 mInitialPaneState = newState; 341 return false; 342 } 343 if (newState == mPaneState) { 344 return false; 345 } 346 // Just make sure the first transition doesn't animate. 347 if (mPaneState == STATE_UNINITIALIZED) { 348 animate = false; 349 } 350 351 final int previousVisiblePanes = getVisiblePanes(); 352 mPaneState = newState; 353 354 // Animate to the new state. 355 // (We still use animator even if animate == false; we just use 0 duration.) 356 final int totalWidth = getMeasuredWidth(); 357 358 final int expectedMailboxLeft; 359 final int expectedMessageListWidth; 360 361 final String animatorLabel; // for debug purpose 362 363 if (!isPaneCollapsible()) { 364 setViewWidth(mLeftPane, mMailboxListWidth); 365 setViewWidth(mRightPane, totalWidth - mMessageListWidth); 366 367 switch (mPaneState) { 368 case STATE_LEFT_VISIBLE: 369 // mailbox + message list 370 animatorLabel = "moving to [mailbox list + message list]"; 371 expectedMailboxLeft = 0; 372 expectedMessageListWidth = totalWidth - mMailboxListWidth; 373 break; 374 case STATE_RIGHT_VISIBLE: 375 // message list + message view 376 animatorLabel = "moving to [message list + message view]"; 377 expectedMailboxLeft = -mMailboxListWidth; 378 expectedMessageListWidth = mMessageListWidth; 379 break; 380 default: 381 throw new IllegalStateException(); 382 } 383 384 } else { 385 setViewWidth(mLeftPane, mMailboxListWidth); 386 setViewWidth(mRightPane, totalWidth); 387 388 switch (mPaneState) { 389 case STATE_LEFT_VISIBLE: 390 // message list + Message view -> mailbox + message list 391 animatorLabel = "moving to [mailbox list + message list]"; 392 expectedMailboxLeft = 0; 393 expectedMessageListWidth = totalWidth - mMailboxListWidth; 394 break; 395 case STATE_MIDDLE_EXPANDED: 396 // mailbox + message list -> message list + message view 397 animatorLabel = "moving to [message list + message view]"; 398 expectedMailboxLeft = -mMailboxListWidth; 399 expectedMessageListWidth = mMessageListWidth; 400 break; 401 case STATE_RIGHT_VISIBLE: 402 // message view only 403 animatorLabel = "moving to [message view]"; 404 expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth); 405 expectedMessageListWidth = mMessageListWidth; 406 break; 407 default: 408 throw new IllegalStateException(); 409 } 410 } 411 412 final View[][] showHideViews = mShowHideViews[mPaneState]; 413 final AnimatorListener listener = new AnimatorListener(animatorLabel, 414 showHideViews[INDEX_VISIBLE], 415 showHideViews[INDEX_INVISIBLE], 416 showHideViews[INDEX_GONE], 417 previousVisiblePanes); 418 419 // Animation properties -- mailbox list left and message list width, at the same time. 420 startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener, 421 PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT, 422 getCurrentMailboxLeft(), expectedMailboxLeft), 423 PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH, 424 getCurrentMessageListWidth(), expectedMessageListWidth) 425 ); 426 return true; 427 } 428 429 /** 430 * @return The ID of the view for the left pane fragment. (i.e. mailbox list) 431 */ 432 public int getLeftPaneId() { 433 return R.id.left_pane; 434 } 435 436 /** 437 * @return The ID of the view for the middle pane fragment. (i.e. message list) 438 */ 439 public int getMiddlePaneId() { 440 return R.id.middle_pane; 441 } 442 443 /** 444 * @return The ID of the view for the right pane fragment. (i.e. message view) 445 */ 446 public int getRightPaneId() { 447 return R.id.right_pane; 448 } 449 450 @Override 451 public void onClick(View v) { 452 switch (v.getId()) { 453 case R.id.fogged_glass: 454 if (!isPaneCollapsible()) { 455 return; // Shouldn't happen 456 } 457 changePaneState(STATE_RIGHT_VISIBLE, true); 458 break; 459 } 460 } 461 462 private void setViewWidth(View v, int value) { 463 v.getLayoutParams().width = value; 464 requestLayout(); 465 } 466 467 private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim"; 468 private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim"; 469 470 public void setMailboxListLeftAnim(int value) { 471 ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value; 472 requestLayout(); 473 } 474 475 public void setMessageListWidthAnim(int value) { 476 setViewWidth(mMiddlePane, value); 477 } 478 479 private int getCurrentMailboxLeft() { 480 return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin; 481 } 482 483 private int getCurrentMessageListWidth() { 484 return mMiddlePane.getLayoutParams().width; 485 } 486 487 /** 488 * Helper method to start animation. 489 */ 490 private void startLayoutAnimation(int duration, AnimatorListener listener, 491 PropertyValuesHolder... values) { 492 if (mLastAnimator != null) { 493 mLastAnimator.cancel(); 494 } 495 if (mLastAnimatorListener != null) { 496 if (ANIMATION_DEBUG) { 497 Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator); 498 } 499 // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes 500 // we end up cancelling the previous one *after* starting the next one. 501 // Directly tell the listener it's cancelled to avoid that. 502 mLastAnimatorListener.cancel(); 503 } 504 505 final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 506 this, values).setDuration(duration); 507 animator.setInterpolator(INTERPOLATOR); 508 if (listener != null) { 509 animator.addListener(listener); 510 } 511 mLastAnimator = animator; 512 mLastAnimatorListener = listener; 513 animator.start(); 514 } 515 516 /** 517 * Animation listener. 518 * 519 * Update the visibility of each pane before/after an animation. 520 */ 521 private class AnimatorListener implements Animator.AnimatorListener { 522 private final String mLogLabel; 523 private final View[] mViewsVisible; 524 private final View[] mViewsInvisible; 525 private final View[] mViewsGone; 526 private final int mPreviousVisiblePanes; 527 528 private boolean mCancelled; 529 530 public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible, 531 View[] viewsGone, int previousVisiblePanes) { 532 mLogLabel = logLabel; 533 mViewsVisible = viewsVisible; 534 mViewsInvisible = viewsInvisible; 535 mViewsGone = viewsGone; 536 mPreviousVisiblePanes = previousVisiblePanes; 537 } 538 539 private void log(String message) { 540 if (ANIMATION_DEBUG) { 541 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message); 542 } 543 } 544 545 public void cancel() { 546 log("cancel"); 547 mCancelled = true; 548 } 549 550 /** 551 * Show the about-to-become-visible panes before an animation. 552 */ 553 @Override 554 public void onAnimationStart(Animator animation) { 555 log("start"); 556 for (View v : mViewsVisible) { 557 v.setVisibility(View.VISIBLE); 558 } 559 560 // TODO These things, making invisible views and calling the visible pane changed 561 // callback, should really be done in onAnimationEnd. 562 // However, because we may want to initiate a fragment transaction in the callback but 563 // by the time animation is done, the activity may be stopped (by user's HOME press), 564 // it's not easy to get right. For now, we just do this before the animation. 565 for (View v : mViewsInvisible) { 566 v.setVisibility(View.INVISIBLE); 567 } 568 for (View v : mViewsGone) { 569 v.setVisibility(View.GONE); 570 } 571 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 572 } 573 574 @Override 575 public void onAnimationRepeat(Animator animation) { 576 } 577 578 @Override 579 public void onAnimationCancel(Animator animation) { 580 } 581 582 /** 583 * Hide the about-to-become-hidden panes after an animation. 584 */ 585 @Override 586 public void onAnimationEnd(Animator animation) { 587 if (mCancelled) { 588 return; // But they shouldn't be hidden when cancelled. 589 } 590 log("end"); 591 } 592 } 593 594 private static class SavedState extends BaseSavedState { 595 int mPaneState; 596 597 /** 598 * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()} 599 */ 600 SavedState(Parcelable superState) { 601 super(superState); 602 } 603 604 /** 605 * Constructor called from {@link #CREATOR} 606 */ 607 private SavedState(Parcel in) { 608 super(in); 609 mPaneState = in.readInt(); 610 } 611 612 @Override 613 public void writeToParcel(Parcel out, int flags) { 614 super.writeToParcel(out, flags); 615 out.writeInt(mPaneState); 616 } 617 618 public static final Parcelable.Creator<SavedState> CREATOR 619 = new Parcelable.Creator<SavedState>() { 620 public SavedState createFromParcel(Parcel in) { 621 return new SavedState(in); 622 } 623 624 public SavedState[] newArray(int size) { 625 return new SavedState[size]; 626 } 627 }; 628 } 629 } 630