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