ThreePaneLayout.java revision 761390a04193fe41a0ee2292e2051cf1b640b81c
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 17package com.android.email.activity; 18 19import com.android.email.R; 20import com.android.emailcommon.Logging; 21 22import android.animation.Animator; 23import android.animation.ObjectAnimator; 24import android.animation.PropertyValuesHolder; 25import android.animation.TimeInterpolator; 26import android.content.Context; 27import android.content.res.Resources; 28import android.os.Parcel; 29import android.os.Parcelable; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.animation.DecelerateInterpolator; 35import android.widget.LinearLayout; 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 */ 45public class ThreePaneLayout extends LinearLayout implements View.OnClickListener { 46 private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true 47 48 private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 80; 49 private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.5f); 50 51 /** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */ 52 private static final int STATE_UNINITIALIZED = -1; 53 54 /** Mailbox list + message list both visible. */ 55 private static final int STATE_LEFT_VISIBLE = 0; 56 57 /** 58 * A view where the MessageView is visible. The MessageList is visible if 59 * {@link #isPaneCollapsible} is false, but is otherwise collapsed and hidden. 60 */ 61 private static final int STATE_RIGHT_VISIBLE = 1; 62 63 /** 64 * A view where the MessageView is partially visible and a collapsible MessageList on the left 65 * has been expanded to be in view. {@link #isPaneCollapsible} must return true for this 66 * state to be active. 67 */ 68 private static final int STATE_MIDDLE_EXPANDED = 2; 69 70 // Flags for getVisiblePanes() 71 public static final int PANE_LEFT = 1 << 2; 72 public static final int PANE_MIDDLE = 1 << 1; 73 public static final int PANE_RIGHT = 1 << 0; 74 75 /** Current pane state. See {@link #changePaneState} */ 76 private int mPaneState = STATE_UNINITIALIZED; 77 78 /** See {@link #changePaneState} and {@link #onFirstSizeChanged} */ 79 private int mInitialPaneState = STATE_UNINITIALIZED; 80 81 private View mLeftPane; 82 private View mMiddlePane; 83 private View mRightPane; 84 private MessageCommandButtonView mMessageCommandButtons; 85 86 // Views used only when the left pane is collapsible. 87 private View mFoggedGlass; 88 89 private boolean mFirstSizeChangedDone; 90 91 /** Mailbox list width. Comes from resources. */ 92 private int mMailboxListWidth; 93 /** 94 * Message list width, on: 95 * - the message list + message view mode, when the left pane is not collapsible 96 * - the message view + expanded message list mode, when the left pane is collapsible 97 * Comes from resources. 98 */ 99 private int mMessageListWidth; 100 101 /** Hold last animator to cancel. */ 102 private Animator mLastAnimator; 103 104 /** 105 * Hold last animator listener to cancel. See {@link #startLayoutAnimation} for why 106 * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener} 107 */ 108 private AnimatorListener mLastAnimatorListener; 109 110 // 2nd index for {@link #changePaneState} 111 private static final int INDEX_VISIBLE = 0; 112 private static final int INDEX_INVISIBLE = 1; 113 private static final int INDEX_GONE = 2; 114 115 // Arrays used in {@link #changePaneState} 116 // First index: STATE_* 117 // Second index: INDEX_* 118 private View[][][] mShowHideViews; 119 120 private Callback mCallback = EmptyCallback.INSTANCE; 121 122 public interface Callback { 123 /** Called when {@link ThreePaneLayout#getVisiblePanes()} has changed. */ 124 public void onVisiblePanesChanged(int previousVisiblePanes); 125 } 126 127 private static final class EmptyCallback implements Callback { 128 public static final Callback INSTANCE = new EmptyCallback(); 129 130 @Override public void onVisiblePanesChanged(int previousVisiblePanes) {} 131 } 132 133 public ThreePaneLayout(Context context, AttributeSet attrs, int defStyle) { 134 super(context, attrs, defStyle); 135 initView(); 136 } 137 138 public ThreePaneLayout(Context context, AttributeSet attrs) { 139 super(context, attrs); 140 initView(); 141 } 142 143 public ThreePaneLayout(Context context) { 144 super(context); 145 initView(); 146 } 147 148 /** Perform basic initialization */ 149 private void initView() { 150 setOrientation(LinearLayout.HORIZONTAL); // Always horizontal 151 } 152 153 @Override 154 protected void onFinishInflate() { 155 super.onFinishInflate(); 156 157 mLeftPane = findViewById(R.id.left_pane); 158 mMiddlePane = findViewById(R.id.middle_pane); 159 mMessageCommandButtons = 160 (MessageCommandButtonView) findViewById(R.id.message_command_buttons); 161 162 mFoggedGlass = findViewById(R.id.fogged_glass); 163 if (mFoggedGlass != null) { 164 mRightPane = findViewById(R.id.right_pane_with_fog); 165 mFoggedGlass.setOnClickListener(this); 166 } else { 167 mRightPane = findViewById(R.id.right_pane); 168 } 169 170 if (!isPaneCollapsible()) { 171 mShowHideViews = new View[][][] { 172 // STATE_LEFT_VISIBLE 173 { 174 {mLeftPane, mMiddlePane}, // Visible 175 {mRightPane}, // Invisible 176 {mMessageCommandButtons}, // Gone 177 }, 178 // STATE_RIGHT_VISIBLE 179 { 180 {mMiddlePane, mMessageCommandButtons, mRightPane}, // Visible 181 {mLeftPane}, // Invisible 182 {}, // Gone 183 }, 184 // STATE_MIDDLE_EXPANDED 185 { 186 {}, // Visible 187 {}, // Invisible 188 {}, // Gone 189 }, 190 }; 191 } else { 192 mShowHideViews = new View[][][] { 193 // STATE_LEFT_VISIBLE 194 { 195 {mLeftPane, mMiddlePane}, // Visible 196 {mRightPane, mFoggedGlass}, // Invisible 197 {mMessageCommandButtons}, // Gone 198 }, 199 // STATE_RIGHT_VISIBLE 200 { 201 {mRightPane, mMessageCommandButtons}, // Visible 202 {mLeftPane, mMiddlePane, mFoggedGlass}, // Invisible 203 {}, // Gone 204 }, 205 // STATE_MIDDLE_EXPANDED 206 { 207 {mMiddlePane, mRightPane, mMessageCommandButtons, mFoggedGlass}, // Visible 208 {mLeftPane}, // Invisible 209 {}, // Gone 210 }, 211 }; 212 } 213 214 mInitialPaneState = STATE_LEFT_VISIBLE; 215 216 final Resources resources = getResources(); 217 mMailboxListWidth = getResources().getDimensionPixelSize( 218 R.dimen.mailbox_list_width); 219 mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width); 220 } 221 222 223 public void setCallback(Callback callback) { 224 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 225 } 226 227 /** 228 * Return whether or not the left pane should be collapsible. 229 */ 230 private boolean isPaneCollapsible() { 231 return mFoggedGlass != null; 232 } 233 234 public MessageCommandButtonView getMessageCommandButtons() { 235 return mMessageCommandButtons; 236 } 237 238 @Override 239 protected Parcelable onSaveInstanceState() { 240 SavedState ss = new SavedState(super.onSaveInstanceState()); 241 ss.mPaneState = mPaneState; 242 return ss; 243 } 244 245 @Override 246 protected void onRestoreInstanceState(Parcelable state) { 247 // Called after onFinishInflate() 248 SavedState ss = (SavedState) state; 249 super.onRestoreInstanceState(ss.getSuperState()); 250 mInitialPaneState = ss.mPaneState; 251 } 252 253 @Override 254 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 255 super.onSizeChanged(w, h, oldw, oldh); 256 if (!mFirstSizeChangedDone) { 257 mFirstSizeChangedDone = true; 258 onFirstSizeChanged(); 259 } 260 } 261 262 /** 263 * @return bit flags for visible panes. Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE} 264 * and {@link #PANE_RIGHT}, 265 */ 266 public int getVisiblePanes() { 267 int ret = 0; 268 if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT; 269 if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE; 270 if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT; 271 return ret; 272 } 273 274 /** 275 * Handles the back event. 276 * 277 * @param isSystemBackKey set true if the system back key is pressed, rather than the home 278 * icon on action bar. 279 * @return true if the event is handled. 280 */ 281 public boolean onBackPressed(boolean isSystemBackKey) { 282 if (!isPaneCollapsible()) { 283 switch (mPaneState) { 284 case STATE_RIGHT_VISIBLE: 285 changePaneState(STATE_LEFT_VISIBLE, true); // Close the right pane 286 return true; 287 } 288 } else { 289 switch (mPaneState) { 290 case STATE_RIGHT_VISIBLE: 291 if (isSystemBackKey) { 292 changePaneState(STATE_LEFT_VISIBLE, true); 293 } else { 294 changePaneState(STATE_MIDDLE_EXPANDED, true); 295 } 296 return true; 297 case STATE_MIDDLE_EXPANDED: 298 changePaneState(STATE_LEFT_VISIBLE, true); 299 return true; 300 } 301 } 302 return false; 303 } 304 305 /** 306 * Show the left most pane. (i.e. mailbox list) 307 */ 308 public void showLeftPane() { 309 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 void showRightPane() { 329 changePaneState(STATE_RIGHT_VISIBLE, true); 330 } 331 332 private void 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; 342 } 343 if (newState == mPaneState) { 344 return; 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 } 427 428 /** 429 * @return The ID of the view for the left pane fragment. (i.e. mailbox list) 430 */ 431 public int getLeftPaneId() { 432 return R.id.left_pane; 433 } 434 435 /** 436 * @return The ID of the view for the middle pane fragment. (i.e. message list) 437 */ 438 public int getMiddlePaneId() { 439 return R.id.middle_pane; 440 } 441 442 /** 443 * @return The ID of the view for the right pane fragment. (i.e. message view) 444 */ 445 public int getRightPaneId() { 446 return R.id.right_pane; 447 } 448 449 @Override 450 public void onClick(View v) { 451 switch (v.getId()) { 452 case R.id.fogged_glass: 453 if (!isPaneCollapsible()) { 454 return; // Shouldn't happen 455 } 456 changePaneState(STATE_RIGHT_VISIBLE, true); 457 break; 458 } 459 } 460 461 private void setViewWidth(View v, int value) { 462 v.getLayoutParams().width = value; 463 requestLayout(); 464 } 465 466 private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim"; 467 private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim"; 468 469 public void setMailboxListLeftAnim(int value) { 470 ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value; 471 requestLayout(); 472 } 473 474 public void setMessageListWidthAnim(int value) { 475 setViewWidth(mMiddlePane, value); 476 } 477 478 private int getCurrentMailboxLeft() { 479 return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin; 480 } 481 482 private int getCurrentMessageListWidth() { 483 return mMiddlePane.getLayoutParams().width; 484 } 485 486 /** 487 * Helper method to start animation. 488 */ 489 private void startLayoutAnimation(int duration, AnimatorListener listener, 490 PropertyValuesHolder... values) { 491 if (mLastAnimator != null) { 492 mLastAnimator.cancel(); 493 } 494 if (mLastAnimatorListener != null) { 495 if (ANIMATION_DEBUG) { 496 Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator); 497 } 498 // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes 499 // we end up cancelling the previous one *after* starting the next one. 500 // Directly tell the listener it's cancelled to avoid that. 501 mLastAnimatorListener.cancel(); 502 } 503 504 final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 505 this, values).setDuration(duration); 506 animator.setInterpolator(INTERPOLATOR); 507 if (listener != null) { 508 animator.addListener(listener); 509 } 510 mLastAnimator = animator; 511 mLastAnimatorListener = listener; 512 animator.start(); 513 } 514 515 /** 516 * Animation listener. 517 * 518 * Update the visibility of each pane before/after an animation. 519 */ 520 private class AnimatorListener implements Animator.AnimatorListener { 521 private final String mLogLabel; 522 private final View[] mViewsVisible; 523 private final View[] mViewsInvisible; 524 private final View[] mViewsGone; 525 private final int mPreviousVisiblePanes; 526 527 private boolean mCancelled; 528 529 public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible, 530 View[] viewsGone, int previousVisiblePanes) { 531 mLogLabel = logLabel; 532 mViewsVisible = viewsVisible; 533 mViewsInvisible = viewsInvisible; 534 mViewsGone = viewsGone; 535 mPreviousVisiblePanes = previousVisiblePanes; 536 } 537 538 private void log(String message) { 539 if (ANIMATION_DEBUG) { 540 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message); 541 } 542 } 543 544 public void cancel() { 545 log("cancel"); 546 mCancelled = true; 547 } 548 549 /** 550 * Show the about-to-become-visible panes before an animation. 551 */ 552 @Override 553 public void onAnimationStart(Animator animation) { 554 log("start"); 555 for (View v : mViewsVisible) { 556 v.setVisibility(View.VISIBLE); 557 } 558 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 559 } 560 561 @Override 562 public void onAnimationRepeat(Animator animation) { 563 } 564 565 @Override 566 public void onAnimationCancel(Animator animation) { 567 } 568 569 /** 570 * Hide the about-to-become-hidden panes after an animation. 571 */ 572 @Override 573 public void onAnimationEnd(Animator animation) { 574 if (mCancelled) { 575 return; // But they shouldn't be hidden when cancelled. 576 } 577 log("end"); 578 for (View v : mViewsInvisible) { 579 v.setVisibility(View.INVISIBLE); 580 } 581 for (View v : mViewsGone) { 582 v.setVisibility(View.GONE); 583 } 584 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 585 } 586 } 587 588 private static class SavedState extends BaseSavedState { 589 int mPaneState; 590 591 /** 592 * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()} 593 */ 594 SavedState(Parcelable superState) { 595 super(superState); 596 } 597 598 /** 599 * Constructor called from {@link #CREATOR} 600 */ 601 private SavedState(Parcel in) { 602 super(in); 603 mPaneState = in.readInt(); 604 } 605 606 @Override 607 public void writeToParcel(Parcel out, int flags) { 608 super.writeToParcel(out, flags); 609 out.writeInt(mPaneState); 610 } 611 612 public static final Parcelable.Creator<SavedState> CREATOR 613 = new Parcelable.Creator<SavedState>() { 614 public SavedState createFromParcel(Parcel in) { 615 return new SavedState(in); 616 } 617 618 public SavedState[] newArray(int size) { 619 return new SavedState[size]; 620 } 621 }; 622 } 623} 624