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