ThreePaneLayout.java revision d6decef1d2a8d14aa8a65229bc784e6fdbb31864
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 android.animation.Animator; 20import android.animation.ObjectAnimator; 21import android.animation.PropertyValuesHolder; 22import android.animation.TimeInterpolator; 23import android.content.Context; 24import android.content.res.Resources; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.animation.DecelerateInterpolator; 32import android.widget.LinearLayout; 33 34import com.android.email.R; 35import 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 */ 47public 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 boolean mConvViewExpandList; 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 private boolean mIsSearchResult = false; 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 = (MessageCommandButtonView) findViewById(R.id.message_command_buttons); 162 163 mRightPane = findViewById(R.id.right_pane); 164 mConvViewExpandList = getContext().getResources().getBoolean(R.bool.expand_middle_view); 165 View[][] stateRightVisible = new View[][] { 166 { 167 mMiddlePane, mMessageCommandButtons, mRightPane 168 }, // Visible 169 { 170 mLeftPane 171 }, // Invisible 172 {}, // Gone; 173 }; 174 View[][] stateRightVisibleHideConvList = new View[][] { 175 { 176 mRightPane 177 }, // Visible 178 { 179 mMiddlePane, mMessageCommandButtons, mLeftPane 180 }, // Invisible 181 {}, // Gone; 182 }; 183 mShowHideViews = new View[][][] { 184 // STATE_LEFT_VISIBLE 185 { 186 { 187 mLeftPane, mMiddlePane 188 }, // Visible 189 { 190 mRightPane 191 }, // Invisible 192 { 193 mMessageCommandButtons 194 }, // Gone 195 }, 196 // STATE_RIGHT_VISIBLE 197 mConvViewExpandList ? stateRightVisible : stateRightVisibleHideConvList, 198 // STATE_MIDDLE_EXPANDED 199 { 200 {}, // Visible 201 {}, // Invisible 202 {}, // Gone 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 public void setIsSearch(boolean isSearch) { 215 mIsSearchResult = isSearch; 216 } 217 218 private boolean shouldShowMailboxList() { 219 return !mIsSearchResult || UiUtilities.showTwoPaneSearchResults(getContext()); 220 } 221 222 public void setCallback(Callback callback) { 223 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 224 } 225 226 /** 227 * Return whether or not the left pane should be collapsible. 228 */ 229 public boolean isPaneCollapsible() { 230 return false; 231 } 232 233 public MessageCommandButtonView getMessageCommandButtons() { 234 return mMessageCommandButtons; 235 } 236 237 @Override 238 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 239 super.onSizeChanged(w, h, oldw, oldh); 240 if (!mFirstSizeChangedDone) { 241 mFirstSizeChangedDone = true; 242 onFirstSizeChanged(); 243 } 244 } 245 246 /** 247 * @return bit flags for visible panes. Combination of {@link #PANE_LEFT}, {@link #PANE_MIDDLE} 248 * and {@link #PANE_RIGHT}, 249 */ 250 public int getVisiblePanes() { 251 int ret = 0; 252 if (mLeftPane.getVisibility() == View.VISIBLE) ret |= PANE_LEFT; 253 if (mMiddlePane.getVisibility() == View.VISIBLE) ret |= PANE_MIDDLE; 254 if (mRightPane.getVisibility() == View.VISIBLE) ret |= PANE_RIGHT; 255 return ret; 256 } 257 258 public boolean isLeftPaneVisible() { 259 return mLeftPane.getVisibility() == View.VISIBLE; 260 } 261 public boolean isMiddlePaneVisible() { 262 return mMiddlePane.getVisibility() == View.VISIBLE; 263 } 264 public boolean isRightPaneVisible() { 265 return mRightPane.getVisibility() == View.VISIBLE; 266 } 267 268 /** 269 * Show the left most pane. (i.e. mailbox list) 270 */ 271 public boolean showLeftPane() { 272 return changePaneState(STATE_LEFT_VISIBLE, true); 273 } 274 275 /** 276 * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we 277 * can't layout properly. We just remember all the requests to {@link #changePaneState} 278 * until the first {@link #onSizeChanged}, at which point we actually change to the last 279 * requested state. 280 */ 281 private void onFirstSizeChanged() { 282 if (mInitialPaneState != STATE_UNINITIALIZED) { 283 changePaneState(mInitialPaneState, false); 284 mInitialPaneState = STATE_UNINITIALIZED; 285 } 286 } 287 288 /** 289 * Show the right most pane. (i.e. message view) 290 */ 291 public boolean showRightPane() { 292 return changePaneState(STATE_RIGHT_VISIBLE, true); 293 } 294 295 private int getMailboxListWidth() { 296 if (!shouldShowMailboxList()) { 297 return 0; 298 } 299 return mMailboxListWidth; 300 } 301 302 private boolean changePaneState(int newState, boolean animate) { 303 if (!isPaneCollapsible() && (newState == STATE_MIDDLE_EXPANDED)) { 304 newState = STATE_RIGHT_VISIBLE; 305 } 306 if (!mFirstSizeChangedDone) { 307 // Before first onSizeChanged(), we don't know the width of the view, so we can't 308 // layout properly. 309 // Just remember the new state and return. 310 mInitialPaneState = newState; 311 return false; 312 } 313 if (newState == mPaneState) { 314 return false; 315 } 316 // Just make sure the first transition doesn't animate. 317 if (mPaneState == STATE_UNINITIALIZED) { 318 animate = false; 319 } 320 321 final int previousVisiblePanes = getVisiblePanes(); 322 mPaneState = newState; 323 324 // Animate to the new state. 325 // (We still use animator even if animate == false; we just use 0 duration.) 326 final int totalWidth = getMeasuredWidth(); 327 328 final int expectedMailboxLeft; 329 final int expectedMessageListWidth; 330 331 final String animatorLabel; // for debug purpose 332 333 setViewWidth(mLeftPane, getMailboxListWidth()); 334 setViewWidth(mRightPane, totalWidth - getMessageListWidth()); 335 336 switch (mPaneState) { 337 case STATE_LEFT_VISIBLE: 338 // mailbox + message list 339 animatorLabel = "moving to [mailbox list + message list]"; 340 expectedMailboxLeft = 0; 341 expectedMessageListWidth = totalWidth - getMailboxListWidth(); 342 break; 343 case STATE_RIGHT_VISIBLE: 344 // message list + message view 345 animatorLabel = "moving to [message list + message view]"; 346 expectedMailboxLeft = -getMailboxListWidth(); 347 expectedMessageListWidth = getMessageListWidth(); 348 break; 349 default: 350 throw new IllegalStateException(); 351 } 352 setViewWidth(mMiddlePane, expectedMessageListWidth); 353 final View[][] showHideViews = mShowHideViews[mPaneState]; 354 final AnimatorListener listener = new AnimatorListener(animatorLabel, 355 showHideViews[INDEX_VISIBLE], 356 showHideViews[INDEX_INVISIBLE], 357 showHideViews[INDEX_GONE], 358 previousVisiblePanes); 359 360 // Animation properties -- mailbox list left and message list width, at the same time. 361 startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener, 362 PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT, 363 getCurrentMailboxLeft(), expectedMailboxLeft), 364 PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH, 365 getCurrentMessageListWidth(), expectedMessageListWidth) 366 ); 367 return true; 368 } 369 370 private int getMessageListWidth() { 371 if (!mConvViewExpandList && mPaneState == STATE_RIGHT_VISIBLE) { 372 return 0; 373 } 374 return mMessageListWidth; 375 } 376 /** 377 * @return The ID of the view for the left pane fragment. (i.e. mailbox list) 378 */ 379 public int getLeftPaneId() { 380 return R.id.left_pane; 381 } 382 383 /** 384 * @return The ID of the view for the middle pane fragment. (i.e. message list) 385 */ 386 public int getMiddlePaneId() { 387 return R.id.middle_pane; 388 } 389 390 /** 391 * @return The ID of the view for the right pane fragment. (i.e. message view) 392 */ 393 public int getRightPaneId() { 394 return R.id.right_pane; 395 } 396 397 private void setViewWidth(View v, int value) { 398 v.getLayoutParams().width = value; 399 requestLayout(); 400 } 401 402 private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim"; 403 private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim"; 404 405 public void setMailboxListLeftAnim(int value) { 406 ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value; 407 requestLayout(); 408 } 409 410 public void setMessageListWidthAnim(int value) { 411 setViewWidth(mMiddlePane, value); 412 } 413 414 private int getCurrentMailboxLeft() { 415 return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin; 416 } 417 418 private int getCurrentMessageListWidth() { 419 return mMiddlePane.getLayoutParams().width; 420 } 421 422 /** 423 * Helper method to start animation. 424 */ 425 private void startLayoutAnimation(int duration, AnimatorListener listener, 426 PropertyValuesHolder... values) { 427 if (mLastAnimator != null) { 428 mLastAnimator.cancel(); 429 } 430 if (mLastAnimatorListener != null) { 431 if (ANIMATION_DEBUG) { 432 Log.w(Logging.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator); 433 } 434 // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes 435 // we end up cancelling the previous one *after* starting the next one. 436 // Directly tell the listener it's cancelled to avoid that. 437 mLastAnimatorListener.cancel(); 438 } 439 440 final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 441 this, values).setDuration(duration); 442 animator.setInterpolator(INTERPOLATOR); 443 if (listener != null) { 444 animator.addListener(listener); 445 } 446 mLastAnimator = animator; 447 mLastAnimatorListener = listener; 448 animator.start(); 449 } 450 451 /** 452 * Get the state of the view. Returns ones of: STATE_UNINITIALIZED, 453 * STATE_LEFT_VISIBLE, STATE_MIDDLE_EXPANDED, STATE_RIGHT_VISIBLE 454 */ 455 public int getPaneState() { 456 return mPaneState; 457 } 458 /** 459 * Animation listener. 460 * 461 * Update the visibility of each pane before/after an animation. 462 */ 463 private class AnimatorListener implements Animator.AnimatorListener { 464 private final String mLogLabel; 465 private final View[] mViewsVisible; 466 private final View[] mViewsInvisible; 467 private final View[] mViewsGone; 468 private final int mPreviousVisiblePanes; 469 470 private boolean mCancelled; 471 472 public AnimatorListener(String logLabel, View[] viewsVisible, View[] viewsInvisible, 473 View[] viewsGone, int previousVisiblePanes) { 474 mLogLabel = logLabel; 475 mViewsVisible = viewsVisible; 476 mViewsInvisible = viewsInvisible; 477 mViewsGone = viewsGone; 478 mPreviousVisiblePanes = previousVisiblePanes; 479 } 480 481 private void log(String message) { 482 if (ANIMATION_DEBUG) { 483 Log.w(Logging.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message); 484 } 485 } 486 487 public void cancel() { 488 log("cancel"); 489 mCancelled = true; 490 } 491 492 /** 493 * Show the about-to-become-visible panes before an animation. 494 */ 495 @Override 496 public void onAnimationStart(Animator animation) { 497 log("start"); 498 for (View v : mViewsVisible) { 499 v.setVisibility(View.VISIBLE); 500 } 501 502 // TODO These things, making invisible views and calling the visible pane changed 503 // callback, should really be done in onAnimationEnd. 504 // However, because we may want to initiate a fragment transaction in the callback but 505 // by the time animation is done, the activity may be stopped (by user's HOME press), 506 // it's not easy to get right. For now, we just do this before the animation. 507 for (View v : mViewsInvisible) { 508 v.setVisibility(View.INVISIBLE); 509 } 510 for (View v : mViewsGone) { 511 v.setVisibility(View.GONE); 512 } 513 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 514 } 515 516 @Override 517 public void onAnimationRepeat(Animator animation) { 518 } 519 520 @Override 521 public void onAnimationCancel(Animator animation) { 522 } 523 524 /** 525 * Hide the about-to-become-hidden panes after an animation. 526 */ 527 @Override 528 public void onAnimationEnd(Animator animation) { 529 if (mCancelled) { 530 return; // But they shouldn't be hidden when cancelled. 531 } 532 log("end"); 533 } 534 } 535 536 private static class SavedState extends BaseSavedState { 537 int mPaneState; 538 539 /** 540 * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()} 541 */ 542 SavedState(Parcelable superState) { 543 super(superState); 544 } 545 546 /** 547 * Constructor called from {@link #CREATOR} 548 */ 549 private SavedState(Parcel in) { 550 super(in); 551 mPaneState = in.readInt(); 552 } 553 554 @Override 555 public void writeToParcel(Parcel out, int flags) { 556 super.writeToParcel(out, flags); 557 out.writeInt(mPaneState); 558 } 559 560 public static final Parcelable.Creator<SavedState> CREATOR 561 = new Parcelable.Creator<SavedState>() { 562 public SavedState createFromParcel(Parcel in) { 563 return new SavedState(in); 564 } 565 566 public SavedState[] newArray(int size) { 567 return new SavedState[size]; 568 } 569 }; 570 } 571} 572