ThreePaneLayout.java revision d2dac0fd6c7f8e9f6741e3b2d4cc333231a22082
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 public boolean onBackPressed() { 216 if (isLandscape()) { 217 switch (mPaneState) { 218 case STATE_RIGHT_VISIBLE: 219 changePaneState(STATE_LEFT_VISIBLE, true); // Close the right pane 220 return true; 221 } 222 } else { 223 switch (mPaneState) { 224 case STATE_RIGHT_VISIBLE: 225 changePaneState(STATE_PORTRAIT_MIDDLE_EXPANDED, true); 226 return true; 227 case STATE_PORTRAIT_MIDDLE_EXPANDED: 228 changePaneState(STATE_LEFT_VISIBLE, true); 229 return true; 230 } 231 } 232 return false; 233 } 234 235 /** 236 * Show the left most pane. (i.e. mailbox list) 237 */ 238 public void showLeftPane() { 239 changePaneState(STATE_LEFT_VISIBLE, true); 240 } 241 242 /** 243 * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we 244 * can't layout properly. We just remember all the requests to {@link #changePaneState} 245 * until the first {@link #onSizeChanged}, at which point we actually change to the last 246 * requested state. 247 */ 248 private void onFirstSizeChanged() { 249 if (mInitialPaneState != STATE_UNINITIALIZED) { 250 changePaneState(mInitialPaneState, false); 251 mInitialPaneState = STATE_UNINITIALIZED; 252 } 253 } 254 255 /** 256 * Show the right most pane. (i.e. message view) 257 */ 258 public void showRightPane() { 259 changePaneState(STATE_RIGHT_VISIBLE, true); 260 } 261 262 private void changePaneState(int newState, boolean animate) { 263 if (isLandscape() && (newState == STATE_PORTRAIT_MIDDLE_EXPANDED)) { 264 newState = STATE_RIGHT_VISIBLE; 265 } 266 if (!mFirstSizeChangedDone) { 267 // Before first onSizeChanged(), we don't know the width of the view, so we can't 268 // layout properly. 269 // Just remember the new state and return. 270 mInitialPaneState = newState; 271 return; 272 } 273 if (newState == mPaneState) { 274 return; 275 } 276 // Just make sure the first transition doesn't animate. 277 if (mPaneState == STATE_UNINITIALIZED) { 278 animate = false; 279 } 280 281 final int previousVisiblePanes = getVisiblePanes(); 282 mPaneState = newState; 283 284 // Animate to the new state. 285 // (We still use animator even if animate == false; we just use 0 duration.) 286 final int totalWidth = getMeasuredWidth(); 287 288 final int expectedMailboxLeft; 289 final int expectedMessageListWidth; 290 291 final String animatorLabel; // for debug purpose 292 293 final View[] viewsToShow; 294 final View[] viewsToHide; 295 296 if (isLandscape()) { // Landscape 297 setViewWidth(mLeftPane, mMailboxListWidth); 298 setViewWidth(mRightPane, totalWidth - mMessageListWidth); 299 300 switch (mPaneState) { 301 case STATE_LEFT_VISIBLE: 302 // mailbox + message list 303 animatorLabel = "moving to [mailbox list + message list]"; 304 expectedMailboxLeft = 0; 305 expectedMessageListWidth = totalWidth - mMailboxListWidth; 306 viewsToShow = mViewsLeft; 307 viewsToHide = mViewsRight; 308 break; 309 case STATE_RIGHT_VISIBLE: 310 // message list + message view 311 animatorLabel = "moving to [message list + message view]"; 312 expectedMailboxLeft = -mMailboxListWidth; 313 expectedMessageListWidth = mMessageListWidth; 314 viewsToShow = mViewsRight; 315 viewsToHide = mViewsLeft; 316 break; 317 default: 318 throw new IllegalStateException(); 319 } 320 321 } else { // Portrait 322 setViewWidth(mLeftPane, mMailboxListWidth); 323 setViewWidth(mRightPane, totalWidth); 324 325 switch (mPaneState) { 326 case STATE_LEFT_VISIBLE: 327 // message list + Message view -> mailbox + message list 328 animatorLabel = "moving to [mailbox list + message list]"; 329 expectedMailboxLeft = 0; 330 expectedMessageListWidth = totalWidth - mMailboxListWidth; 331 viewsToShow = mViewsLeftMiddle; 332 viewsToHide = mViewsRight; 333 break; 334 case STATE_PORTRAIT_MIDDLE_EXPANDED: 335 // mailbox + message list -> message list + message view 336 animatorLabel = "moving to [message list + message view]"; 337 expectedMailboxLeft = -mMailboxListWidth; 338 expectedMessageListWidth = mMessageListWidth; 339 viewsToShow = mViewsMiddleRightFogged; 340 viewsToHide = mViewsLeft; 341 break; 342 case STATE_RIGHT_VISIBLE: 343 // message view only 344 animatorLabel = "moving to [message view]"; 345 expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth); 346 expectedMessageListWidth = mMessageListWidth; 347 viewsToShow = mViewsRight; 348 viewsToHide = mViewsLeftMiddleFogged; 349 break; 350 default: 351 throw new IllegalStateException(); 352 } 353 } 354 355 final AnimatorListener listener = new AnimatorListener(animatorLabel, viewsToShow, 356 viewsToHide, previousVisiblePanes) ; 357 358 // Animation properties -- mailbox list left and message list width, at the same time. 359 startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener, 360 PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT, 361 getCurrentMailboxLeft(), expectedMailboxLeft), 362 PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH, 363 getCurrentMessageListWidth(), expectedMessageListWidth) 364 ); 365 } 366 367 /** 368 * @return The ID of the view for the left pane fragment. (i.e. mailbox list) 369 */ 370 public int getLeftPaneId() { 371 return R.id.left_pane; 372 } 373 374 /** 375 * @return The ID of the view for the middle pane fragment. (i.e. message list) 376 */ 377 public int getMiddlePaneId() { 378 return R.id.middle_pane; 379 } 380 381 /** 382 * @return The ID of the view for the right pane fragment. (i.e. message view) 383 */ 384 public int getRightPaneId() { 385 return R.id.right_pane; 386 } 387 388 @Override 389 public void onClick(View v) { 390 switch (v.getId()) { 391 case R.id.fogged_glass: 392 if (isLandscape()) { 393 return; // Shouldn't happen 394 } 395 changePaneState(STATE_RIGHT_VISIBLE, true); 396 break; 397 } 398 } 399 400 private void setViewWidth(View v, int value) { 401 v.getLayoutParams().width = value; 402 requestLayout(); 403 } 404 405 private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim"; 406 private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim"; 407 408 @SuppressWarnings("unused") 409 public void setMailboxListLeftAnim(int value) { 410 ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value; 411 requestLayout(); 412 } 413 414 @SuppressWarnings("unused") 415 public void setMessageListWidthAnim(int value) { 416 setViewWidth(mMiddlePane, value); 417 } 418 419 private int getCurrentMailboxLeft() { 420 return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin; 421 } 422 423 private int getCurrentMessageListWidth() { 424 return mMiddlePane.getLayoutParams().width; 425 } 426 427 /** 428 * Helper method to start animation. 429 */ 430 private void startLayoutAnimation(int duration, AnimatorListener listener, 431 PropertyValuesHolder... values) { 432 if (mLastAnimator != null) { 433 mLastAnimator.cancel(); 434 } 435 if (mLastAnimatorListener != null) { 436 if (ANIMATION_DEBUG) { 437 Log.w(Email.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator); 438 } 439 // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes 440 // we end up cancelling the previous one *after* starting the next one. 441 // Directly tell the listener it's cancelled to avoid that. 442 mLastAnimatorListener.cancel(); 443 } 444 445 final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 446 this, values).setDuration(duration); 447 animator.setInterpolator(INTERPOLATOR); 448 if (listener != null) { 449 animator.addListener(listener); 450 } 451 mLastAnimator = animator; 452 mLastAnimatorListener = listener; 453 animator.start(); 454 } 455 456 /** 457 * Animation listener. 458 * 459 * Update the visibility of each pane before/after an animation. 460 */ 461 private class AnimatorListener implements Animator.AnimatorListener { 462 private final String mLogLabel; 463 private final View[] mViewsToShow; 464 private final View[] mViewsToHide; 465 private final int mPreviousVisiblePanes; 466 467 private boolean mCancelled; 468 469 public AnimatorListener(String logLabel, View[] viewsToShow, View[] viewsToHide, 470 int previousVisiblePanes) { 471 mLogLabel = logLabel; 472 mViewsToShow = viewsToShow; 473 mViewsToHide = viewsToHide; 474 mPreviousVisiblePanes = previousVisiblePanes; 475 } 476 477 private void log(String message) { 478 if (ANIMATION_DEBUG) { 479 Log.w(Email.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message); 480 } 481 } 482 483 public void cancel() { 484 log("cancel"); 485 mCancelled = true; 486 } 487 488 /** 489 * Show the about-to-become-visible panes before an animation. 490 */ 491 @Override 492 public void onAnimationStart(Animator animation) { 493 log("start"); 494 for (View v : mViewsToShow) { 495 v.setVisibility(View.VISIBLE); 496 } 497 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 498 } 499 500 @Override 501 public void onAnimationRepeat(Animator animation) { 502 } 503 504 @Override 505 public void onAnimationCancel(Animator animation) { 506 } 507 508 /** 509 * Hide the about-to-become-hidden panes after an animation. 510 */ 511 @Override 512 public void onAnimationEnd(Animator animation) { 513 if (mCancelled) { 514 return; // But they shouldn't be hidden when cancelled. 515 } 516 log("end"); 517 for (View v : mViewsToHide) { 518 v.setVisibility(View.INVISIBLE); 519 } 520 mCallback.onVisiblePanesChanged(mPreviousVisiblePanes); 521 } 522 } 523 524 private static class SavedState extends BaseSavedState { 525 int mPaneState; 526 527 /** 528 * Constructor called from {@link ThreePaneLayout#onSaveInstanceState()} 529 */ 530 SavedState(Parcelable superState) { 531 super(superState); 532 } 533 534 /** 535 * Constructor called from {@link #CREATOR} 536 */ 537 private SavedState(Parcel in) { 538 super(in); 539 mPaneState = in.readInt(); 540 } 541 542 @Override 543 public void writeToParcel(Parcel out, int flags) { 544 super.writeToParcel(out, flags); 545 out.writeInt(mPaneState); 546 } 547 548 public static final Parcelable.Creator<SavedState> CREATOR 549 = new Parcelable.Creator<SavedState>() { 550 public SavedState createFromParcel(Parcel in) { 551 return new SavedState(in); 552 } 553 554 public SavedState[] newArray(int size) { 555 return new SavedState[size]; 556 } 557 }; 558 } 559} 560