1/* 2 * Copyright (C) 2014 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 android.support.v17.leanback.widget; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.drawable.Drawable; 22import android.support.annotation.VisibleForTesting; 23import android.support.v17.leanback.R; 24import android.util.AttributeSet; 25import android.util.Log; 26import android.view.View; 27import android.view.ViewDebug; 28import android.view.ViewGroup; 29import android.view.animation.AccelerateDecelerateInterpolator; 30import android.view.animation.Animation; 31import android.view.animation.DecelerateInterpolator; 32import android.view.animation.Transformation; 33import android.widget.FrameLayout; 34 35import java.util.ArrayList; 36 37/** 38 * A card style layout that responds to certain state changes. It arranges its 39 * children in a vertical column, with different regions becoming visible at 40 * different times. 41 * 42 * <p> 43 * A BaseCardView will draw its children based on its type, the region 44 * visibilities of the child types, and the state of the widget. A child may be 45 * marked as belonging to one of three regions: main, info, or extra. The main 46 * region is always visible, while the info and extra regions can be set to 47 * display based on the activated or selected state of the View. The card states 48 * are set by calling {@link #setActivated(boolean) setActivated} and 49 * {@link #setSelected(boolean) setSelected}. 50 * <p> 51 * See {@link BaseCardView.LayoutParams} for layout attributes. 52 * </p> 53 */ 54public class BaseCardView extends FrameLayout { 55 private static final String TAG = "BaseCardView"; 56 private static final boolean DEBUG = false; 57 58 /** 59 * A simple card type with a single layout area. This card type does not 60 * change its layout or size as it transitions between 61 * Activated/Not-Activated or Selected/Unselected states. 62 * 63 * @see #getCardType() 64 */ 65 public static final int CARD_TYPE_MAIN_ONLY = 0; 66 67 /** 68 * A Card type with 2 layout areas: A main area which is always visible, and 69 * an info area that fades in over the main area when it is visible. 70 * The card height will not change. 71 * 72 * @see #getCardType() 73 */ 74 public static final int CARD_TYPE_INFO_OVER = 1; 75 76 /** 77 * A Card type with 2 layout areas: A main area which is always visible, and 78 * an info area that appears below the main area. When the info area is visible 79 * the total card height will change. 80 * 81 * @see #getCardType() 82 */ 83 public static final int CARD_TYPE_INFO_UNDER = 2; 84 85 /** 86 * A Card type with 3 layout areas: A main area which is always visible; an 87 * info area which will appear below the main area, and an extra area that 88 * only appears after a short delay. The info area appears below the main 89 * area, causing the total card height to change. The extra area animates in 90 * at the bottom of the card, shifting up the info view without affecting 91 * the card height. 92 * 93 * @see #getCardType() 94 */ 95 public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3; 96 97 /** 98 * Indicates that a card region is always visible. 99 */ 100 public static final int CARD_REGION_VISIBLE_ALWAYS = 0; 101 102 /** 103 * Indicates that a card region is visible when the card is activated. 104 */ 105 public static final int CARD_REGION_VISIBLE_ACTIVATED = 1; 106 107 /** 108 * Indicates that a card region is visible when the card is selected. 109 */ 110 public static final int CARD_REGION_VISIBLE_SELECTED = 2; 111 112 private static final int CARD_TYPE_INVALID = 4; 113 114 private int mCardType; 115 private int mInfoVisibility; 116 private int mExtraVisibility; 117 118 private ArrayList<View> mMainViewList; 119 ArrayList<View> mInfoViewList; 120 ArrayList<View> mExtraViewList; 121 122 private int mMeasuredWidth; 123 private int mMeasuredHeight; 124 private boolean mDelaySelectedAnim; 125 private int mSelectedAnimationDelay; 126 private final int mActivatedAnimDuration; 127 private final int mSelectedAnimDuration; 128 129 /** 130 * Distance of top of info view to bottom of MainView, it will shift up when extra view appears. 131 */ 132 float mInfoOffset; 133 float mInfoVisFraction; 134 float mInfoAlpha; 135 private Animation mAnim; 136 137 private final static int[] LB_PRESSED_STATE_SET = new int[]{ 138 android.R.attr.state_pressed}; 139 140 private final Runnable mAnimationTrigger = new Runnable() { 141 @Override 142 public void run() { 143 animateInfoOffset(true); 144 } 145 }; 146 147 public BaseCardView(Context context) { 148 this(context, null); 149 } 150 151 public BaseCardView(Context context, AttributeSet attrs) { 152 this(context, attrs, R.attr.baseCardViewStyle); 153 } 154 155 public BaseCardView(Context context, AttributeSet attrs, int defStyleAttr) { 156 super(context, attrs, defStyleAttr); 157 158 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView, 159 defStyleAttr, 0); 160 161 try { 162 mCardType = a.getInteger(R.styleable.lbBaseCardView_cardType, CARD_TYPE_MAIN_ONLY); 163 Drawable cardForeground = a.getDrawable(R.styleable.lbBaseCardView_cardForeground); 164 if (cardForeground != null) { 165 setForeground(cardForeground); 166 } 167 Drawable cardBackground = a.getDrawable(R.styleable.lbBaseCardView_cardBackground); 168 if (cardBackground != null) { 169 setBackground(cardBackground); 170 } 171 mInfoVisibility = a.getInteger(R.styleable.lbBaseCardView_infoVisibility, 172 CARD_REGION_VISIBLE_ACTIVATED); 173 mExtraVisibility = a.getInteger(R.styleable.lbBaseCardView_extraVisibility, 174 CARD_REGION_VISIBLE_SELECTED); 175 // Extra region should never show before info region. 176 if (mExtraVisibility < mInfoVisibility) { 177 mExtraVisibility = mInfoVisibility; 178 } 179 180 mSelectedAnimationDelay = a.getInteger( 181 R.styleable.lbBaseCardView_selectedAnimationDelay, 182 getResources().getInteger(R.integer.lb_card_selected_animation_delay)); 183 184 mSelectedAnimDuration = a.getInteger( 185 R.styleable.lbBaseCardView_selectedAnimationDuration, 186 getResources().getInteger(R.integer.lb_card_selected_animation_duration)); 187 188 mActivatedAnimDuration = 189 a.getInteger(R.styleable.lbBaseCardView_activatedAnimationDuration, 190 getResources().getInteger(R.integer.lb_card_activated_animation_duration)); 191 } finally { 192 a.recycle(); 193 } 194 195 mDelaySelectedAnim = true; 196 197 mMainViewList = new ArrayList<View>(); 198 mInfoViewList = new ArrayList<View>(); 199 mExtraViewList = new ArrayList<View>(); 200 201 mInfoOffset = 0.0f; 202 mInfoVisFraction = getFinalInfoVisFraction(); 203 mInfoAlpha = getFinalInfoAlpha(); 204 } 205 206 /** 207 * Sets a flag indicating if the Selected animation (if the selected card 208 * type implements one) should run immediately after the card is selected, 209 * or if it should be delayed. The default behavior is to delay this 210 * animation. This is a one-shot override. If set to false, after the card 211 * is selected and the selected animation is triggered, this flag is 212 * automatically reset to true. This is useful when you want to change the 213 * default behavior, and have the selected animation run immediately. One 214 * such case could be when focus moves from one row to the other, when 215 * instead of delaying the selected animation until the user pauses on a 216 * card, it may be desirable to trigger the animation for that card 217 * immediately. 218 * 219 * @param delay True (default) if the selected animation should be delayed 220 * after the card is selected, or false if the animation should 221 * run immediately the next time the card is Selected. 222 */ 223 public void setSelectedAnimationDelayed(boolean delay) { 224 mDelaySelectedAnim = delay; 225 } 226 227 /** 228 * Returns a boolean indicating if the selected animation will run 229 * immediately or be delayed the next time the card is Selected. 230 * 231 * @return true if this card is set to delay the selected animation the next 232 * time it is selected, or false if the selected animation will run 233 * immediately the next time the card is selected. 234 */ 235 public boolean isSelectedAnimationDelayed() { 236 return mDelaySelectedAnim; 237 } 238 239 /** 240 * Sets the type of this Card. 241 * 242 * @param type The desired card type. 243 */ 244 public void setCardType(int type) { 245 if (mCardType != type) { 246 if (type >= CARD_TYPE_MAIN_ONLY && type < CARD_TYPE_INVALID) { 247 // Valid card type 248 mCardType = type; 249 } else { 250 Log.e(TAG, "Invalid card type specified: " + type 251 + ". Defaulting to type CARD_TYPE_MAIN_ONLY."); 252 mCardType = CARD_TYPE_MAIN_ONLY; 253 } 254 requestLayout(); 255 } 256 } 257 258 /** 259 * Returns the type of this Card. 260 * 261 * @return The type of this card. 262 */ 263 public int getCardType() { 264 return mCardType; 265 } 266 267 /** 268 * Sets the visibility of the info region of the card. 269 * 270 * @param visibility The region visibility to use for the info region. Must 271 * be one of {@link #CARD_REGION_VISIBLE_ALWAYS}, 272 * {@link #CARD_REGION_VISIBLE_SELECTED}, or 273 * {@link #CARD_REGION_VISIBLE_ACTIVATED}. 274 */ 275 public void setInfoVisibility(int visibility) { 276 if (mInfoVisibility != visibility) { 277 cancelAnimations(); 278 mInfoVisibility = visibility; 279 mInfoVisFraction = getFinalInfoVisFraction(); 280 requestLayout(); 281 float newInfoAlpha = getFinalInfoAlpha(); 282 if (newInfoAlpha != mInfoAlpha) { 283 mInfoAlpha = newInfoAlpha; 284 for (int i = 0; i < mInfoViewList.size(); i++) { 285 mInfoViewList.get(i).setAlpha(mInfoAlpha); 286 } 287 } 288 } 289 } 290 291 final float getFinalInfoVisFraction() { 292 return mCardType == CARD_TYPE_INFO_UNDER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED 293 && !isSelected() ? 0.0f : 1.0f; 294 } 295 296 final float getFinalInfoAlpha() { 297 return mCardType == CARD_TYPE_INFO_OVER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED 298 && !isSelected() ? 0.0f : 1.0f; 299 } 300 301 /** 302 * Returns the visibility of the info region of the card. 303 */ 304 public int getInfoVisibility() { 305 return mInfoVisibility; 306 } 307 308 /** 309 * Sets the visibility of the extra region of the card. 310 * 311 * @param visibility The region visibility to use for the extra region. Must 312 * be one of {@link #CARD_REGION_VISIBLE_ALWAYS}, 313 * {@link #CARD_REGION_VISIBLE_SELECTED}, or 314 * {@link #CARD_REGION_VISIBLE_ACTIVATED}. 315 * @deprecated Extra view's visibility is controlled by {@link #setInfoVisibility(int)} 316 */ 317 @Deprecated 318 public void setExtraVisibility(int visibility) { 319 if (mExtraVisibility != visibility) { 320 mExtraVisibility = visibility; 321 } 322 } 323 324 /** 325 * Returns the visibility of the extra region of the card. 326 * @deprecated Extra view's visibility is controlled by {@link #getInfoVisibility()} 327 */ 328 @Deprecated 329 public int getExtraVisibility() { 330 return mExtraVisibility; 331 } 332 333 /** 334 * Sets the Activated state of this Card. This can trigger changes in the 335 * card layout, resulting in views to become visible or hidden. A card is 336 * normally set to Activated state when its parent container (like a Row) 337 * receives focus, and then activates all of its children. 338 * 339 * @param activated True if the card is ACTIVE, or false if INACTIVE. 340 * @see #isActivated() 341 */ 342 @Override 343 public void setActivated(boolean activated) { 344 if (activated != isActivated()) { 345 super.setActivated(activated); 346 applyActiveState(isActivated()); 347 } 348 } 349 350 /** 351 * Sets the Selected state of this Card. This can trigger changes in the 352 * card layout, resulting in views to become visible or hidden. A card is 353 * normally set to Selected state when it receives input focus. 354 * 355 * @param selected True if the card is Selected, or false otherwise. 356 * @see #isSelected() 357 */ 358 @Override 359 public void setSelected(boolean selected) { 360 if (selected != isSelected()) { 361 super.setSelected(selected); 362 applySelectedState(isSelected()); 363 } 364 } 365 366 @Override 367 public boolean shouldDelayChildPressedState() { 368 return false; 369 } 370 371 @Override 372 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 373 mMeasuredWidth = 0; 374 mMeasuredHeight = 0; 375 int state = 0; 376 int mainHeight = 0; 377 int infoHeight = 0; 378 int extraHeight = 0; 379 380 findChildrenViews(); 381 382 final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 383 // MAIN is always present 384 for (int i = 0; i < mMainViewList.size(); i++) { 385 View mainView = mMainViewList.get(i); 386 if (mainView.getVisibility() != View.GONE) { 387 measureChild(mainView, unspecifiedSpec, unspecifiedSpec); 388 mMeasuredWidth = Math.max(mMeasuredWidth, mainView.getMeasuredWidth()); 389 mainHeight += mainView.getMeasuredHeight(); 390 state = View.combineMeasuredStates(state, mainView.getMeasuredState()); 391 } 392 } 393 setPivotX(mMeasuredWidth / 2); 394 setPivotY(mainHeight / 2); 395 396 397 // The MAIN area determines the card width 398 int cardWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY); 399 400 if (hasInfoRegion()) { 401 for (int i = 0; i < mInfoViewList.size(); i++) { 402 View infoView = mInfoViewList.get(i); 403 if (infoView.getVisibility() != View.GONE) { 404 measureChild(infoView, cardWidthMeasureSpec, unspecifiedSpec); 405 if (mCardType != CARD_TYPE_INFO_OVER) { 406 infoHeight += infoView.getMeasuredHeight(); 407 } 408 state = View.combineMeasuredStates(state, infoView.getMeasuredState()); 409 } 410 } 411 412 if (hasExtraRegion()) { 413 for (int i = 0; i < mExtraViewList.size(); i++) { 414 View extraView = mExtraViewList.get(i); 415 if (extraView.getVisibility() != View.GONE) { 416 measureChild(extraView, cardWidthMeasureSpec, unspecifiedSpec); 417 extraHeight += extraView.getMeasuredHeight(); 418 state = View.combineMeasuredStates(state, extraView.getMeasuredState()); 419 } 420 } 421 } 422 } 423 424 boolean infoAnimating = hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED; 425 mMeasuredHeight = (int) (mainHeight 426 + (infoAnimating ? (infoHeight * mInfoVisFraction) : infoHeight) 427 + extraHeight - (infoAnimating ? 0 : mInfoOffset)); 428 429 // Report our final dimensions. 430 setMeasuredDimension(View.resolveSizeAndState(mMeasuredWidth + getPaddingLeft() 431 + getPaddingRight(), widthMeasureSpec, state), 432 View.resolveSizeAndState(mMeasuredHeight + getPaddingTop() + getPaddingBottom(), 433 heightMeasureSpec, state << View.MEASURED_HEIGHT_STATE_SHIFT)); 434 } 435 436 @Override 437 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 438 float currBottom = getPaddingTop(); 439 440 // MAIN is always present 441 for (int i = 0; i < mMainViewList.size(); i++) { 442 View mainView = mMainViewList.get(i); 443 if (mainView.getVisibility() != View.GONE) { 444 mainView.layout(getPaddingLeft(), 445 (int) currBottom, 446 mMeasuredWidth + getPaddingLeft(), 447 (int) (currBottom + mainView.getMeasuredHeight())); 448 currBottom += mainView.getMeasuredHeight(); 449 } 450 } 451 452 if (hasInfoRegion()) { 453 float infoHeight = 0f; 454 for (int i = 0; i < mInfoViewList.size(); i++) { 455 infoHeight += mInfoViewList.get(i).getMeasuredHeight(); 456 } 457 458 if (mCardType == CARD_TYPE_INFO_OVER) { 459 // retract currBottom to overlap the info views on top of main 460 currBottom -= infoHeight; 461 if (currBottom < 0) { 462 currBottom = 0; 463 } 464 } else if (mCardType == CARD_TYPE_INFO_UNDER) { 465 if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) { 466 infoHeight = infoHeight * mInfoVisFraction; 467 } 468 } else { 469 currBottom -= mInfoOffset; 470 } 471 472 for (int i = 0; i < mInfoViewList.size(); i++) { 473 View infoView = mInfoViewList.get(i); 474 if (infoView.getVisibility() != View.GONE) { 475 int viewHeight = infoView.getMeasuredHeight(); 476 if (viewHeight > infoHeight) { 477 viewHeight = (int) infoHeight; 478 } 479 infoView.layout(getPaddingLeft(), 480 (int) currBottom, 481 mMeasuredWidth + getPaddingLeft(), 482 (int) (currBottom + viewHeight)); 483 currBottom += viewHeight; 484 infoHeight -= viewHeight; 485 if (infoHeight <= 0) { 486 break; 487 } 488 } 489 } 490 491 if (hasExtraRegion()) { 492 for (int i = 0; i < mExtraViewList.size(); i++) { 493 View extraView = mExtraViewList.get(i); 494 if (extraView.getVisibility() != View.GONE) { 495 extraView.layout(getPaddingLeft(), 496 (int) currBottom, 497 mMeasuredWidth + getPaddingLeft(), 498 (int) (currBottom + extraView.getMeasuredHeight())); 499 currBottom += extraView.getMeasuredHeight(); 500 } 501 } 502 } 503 } 504 // Force update drawable bounds. 505 onSizeChanged(0, 0, right - left, bottom - top); 506 } 507 508 @Override 509 protected void onDetachedFromWindow() { 510 super.onDetachedFromWindow(); 511 removeCallbacks(mAnimationTrigger); 512 cancelAnimations(); 513 } 514 515 private boolean hasInfoRegion() { 516 return mCardType != CARD_TYPE_MAIN_ONLY; 517 } 518 519 private boolean hasExtraRegion() { 520 return mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA; 521 } 522 523 /** 524 * Returns target visibility of info region. 525 */ 526 private boolean isRegionVisible(int regionVisibility) { 527 switch (regionVisibility) { 528 case CARD_REGION_VISIBLE_ALWAYS: 529 return true; 530 case CARD_REGION_VISIBLE_ACTIVATED: 531 return isActivated(); 532 case CARD_REGION_VISIBLE_SELECTED: 533 return isSelected(); 534 default: 535 if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility); 536 return false; 537 } 538 } 539 540 /** 541 * Unlike isRegionVisible(), this method returns true when it is fading out when unselected. 542 */ 543 private boolean isCurrentRegionVisible(int regionVisibility) { 544 switch (regionVisibility) { 545 case CARD_REGION_VISIBLE_ALWAYS: 546 return true; 547 case CARD_REGION_VISIBLE_ACTIVATED: 548 return isActivated(); 549 case CARD_REGION_VISIBLE_SELECTED: 550 if (mCardType == CARD_TYPE_INFO_UNDER) { 551 return mInfoVisFraction > 0f; 552 } else { 553 return isSelected(); 554 } 555 default: 556 if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility); 557 return false; 558 } 559 } 560 561 private void findChildrenViews() { 562 mMainViewList.clear(); 563 mInfoViewList.clear(); 564 mExtraViewList.clear(); 565 566 final int count = getChildCount(); 567 568 boolean infoVisible = hasInfoRegion() && isCurrentRegionVisible(mInfoVisibility); 569 boolean extraVisible = hasExtraRegion() && mInfoOffset > 0f; 570 571 for (int i = 0; i < count; i++) { 572 final View child = getChildAt(i); 573 574 if (child == null) { 575 continue; 576 } 577 578 BaseCardView.LayoutParams lp = (BaseCardView.LayoutParams) child 579 .getLayoutParams(); 580 if (lp.viewType == LayoutParams.VIEW_TYPE_INFO) { 581 child.setAlpha(mInfoAlpha); 582 mInfoViewList.add(child); 583 child.setVisibility(infoVisible ? View.VISIBLE : View.GONE); 584 } else if (lp.viewType == LayoutParams.VIEW_TYPE_EXTRA) { 585 mExtraViewList.add(child); 586 child.setVisibility(extraVisible ? View.VISIBLE : View.GONE); 587 } else { 588 // Default to MAIN 589 mMainViewList.add(child); 590 child.setVisibility(View.VISIBLE); 591 } 592 } 593 594 } 595 596 @Override 597 protected int[] onCreateDrawableState(int extraSpace) { 598 // filter out focus states, since leanback does not fade foreground on focus. 599 final int[] s = super.onCreateDrawableState(extraSpace); 600 final int N = s.length; 601 boolean pressed = false; 602 boolean enabled = false; 603 for (int i = 0; i < N; i++) { 604 if (s[i] == android.R.attr.state_pressed) { 605 pressed = true; 606 } 607 if (s[i] == android.R.attr.state_enabled) { 608 enabled = true; 609 } 610 } 611 if (pressed && enabled) { 612 return View.PRESSED_ENABLED_STATE_SET; 613 } else if (pressed) { 614 return LB_PRESSED_STATE_SET; 615 } else if (enabled) { 616 return View.ENABLED_STATE_SET; 617 } else { 618 return View.EMPTY_STATE_SET; 619 } 620 } 621 622 private void applyActiveState(boolean active) { 623 if (hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_ACTIVATED) { 624 setInfoViewVisibility(isRegionVisible(mInfoVisibility)); 625 } 626 } 627 628 private void setInfoViewVisibility(boolean visible) { 629 if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) { 630 // Active state changes for card type 631 // CARD_TYPE_INFO_UNDER_WITH_EXTRA 632 if (visible) { 633 for (int i = 0; i < mInfoViewList.size(); i++) { 634 mInfoViewList.get(i).setVisibility(View.VISIBLE); 635 } 636 } else { 637 for (int i = 0; i < mInfoViewList.size(); i++) { 638 mInfoViewList.get(i).setVisibility(View.GONE); 639 } 640 for (int i = 0; i < mExtraViewList.size(); i++) { 641 mExtraViewList.get(i).setVisibility(View.GONE); 642 } 643 mInfoOffset = 0.0f; 644 } 645 } else if (mCardType == CARD_TYPE_INFO_UNDER) { 646 // Active state changes for card type CARD_TYPE_INFO_UNDER 647 if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) { 648 animateInfoHeight(visible); 649 } else { 650 for (int i = 0; i < mInfoViewList.size(); i++) { 651 mInfoViewList.get(i).setVisibility(visible ? View.VISIBLE : View.GONE); 652 } 653 } 654 } else if (mCardType == CARD_TYPE_INFO_OVER) { 655 // Active state changes for card type CARD_TYPE_INFO_OVER 656 animateInfoAlpha(visible); 657 } 658 } 659 660 private void applySelectedState(boolean focused) { 661 removeCallbacks(mAnimationTrigger); 662 663 if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) { 664 // Focus changes for card type CARD_TYPE_INFO_UNDER_WITH_EXTRA 665 if (focused) { 666 if (!mDelaySelectedAnim) { 667 post(mAnimationTrigger); 668 mDelaySelectedAnim = true; 669 } else { 670 postDelayed(mAnimationTrigger, mSelectedAnimationDelay); 671 } 672 } else { 673 animateInfoOffset(false); 674 } 675 } else if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) { 676 setInfoViewVisibility(focused); 677 } 678 } 679 680 private void cancelAnimations() { 681 if (mAnim != null) { 682 mAnim.cancel(); 683 mAnim = null; 684 // force-clear the animation, as Animation#cancel() doesn't work prior to N, 685 // and will instead cause the animation to infinitely loop 686 clearAnimation(); 687 } 688 } 689 690 // This animation changes the Y offset of the info and extra views, 691 // so that they animate UP to make the extra info area visible when a 692 // card is selected. 693 void animateInfoOffset(boolean shown) { 694 cancelAnimations(); 695 696 int extraHeight = 0; 697 if (shown) { 698 int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY); 699 int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 700 701 for (int i = 0; i < mExtraViewList.size(); i++) { 702 View extraView = mExtraViewList.get(i); 703 extraView.setVisibility(View.VISIBLE); 704 extraView.measure(widthSpec, heightSpec); 705 extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight()); 706 } 707 } 708 709 mAnim = new InfoOffsetAnimation(mInfoOffset, shown ? extraHeight : 0); 710 mAnim.setDuration(mSelectedAnimDuration); 711 mAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 712 mAnim.setAnimationListener(new Animation.AnimationListener() { 713 @Override 714 public void onAnimationStart(Animation animation) { 715 } 716 717 @Override 718 public void onAnimationEnd(Animation animation) { 719 if (mInfoOffset == 0f) { 720 for (int i = 0; i < mExtraViewList.size(); i++) { 721 mExtraViewList.get(i).setVisibility(View.GONE); 722 } 723 } 724 } 725 726 @Override 727 public void onAnimationRepeat(Animation animation) { 728 } 729 730 }); 731 startAnimation(mAnim); 732 } 733 734 // This animation changes the visible height of the info views, 735 // so that they animate in and out of view. 736 private void animateInfoHeight(boolean shown) { 737 cancelAnimations(); 738 739 if (shown) { 740 for (int i = 0; i < mInfoViewList.size(); i++) { 741 View extraView = mInfoViewList.get(i); 742 extraView.setVisibility(View.VISIBLE); 743 } 744 } 745 746 float targetFraction = shown ? 1.0f : 0f; 747 if (mInfoVisFraction == targetFraction) { 748 return; 749 } 750 mAnim = new InfoHeightAnimation(mInfoVisFraction, targetFraction); 751 mAnim.setDuration(mSelectedAnimDuration); 752 mAnim.setInterpolator(new AccelerateDecelerateInterpolator()); 753 mAnim.setAnimationListener(new Animation.AnimationListener() { 754 @Override 755 public void onAnimationStart(Animation animation) { 756 } 757 758 @Override 759 public void onAnimationEnd(Animation animation) { 760 if (mInfoVisFraction == 0f) { 761 for (int i = 0; i < mInfoViewList.size(); i++) { 762 mInfoViewList.get(i).setVisibility(View.GONE); 763 } 764 } 765 } 766 767 @Override 768 public void onAnimationRepeat(Animation animation) { 769 } 770 771 }); 772 startAnimation(mAnim); 773 } 774 775 // This animation changes the alpha of the info views, so they animate in 776 // and out. It's meant to be used when the info views are overlaid on top of 777 // the main view area. It gets triggered by a change in the Active state of 778 // the card. 779 private void animateInfoAlpha(boolean shown) { 780 cancelAnimations(); 781 782 if (shown) { 783 for (int i = 0; i < mInfoViewList.size(); i++) { 784 mInfoViewList.get(i).setVisibility(View.VISIBLE); 785 } 786 } 787 float targetAlpha = shown ? 1.0f : 0.0f; 788 if (targetAlpha == mInfoAlpha) { 789 return; 790 } 791 792 mAnim = new InfoAlphaAnimation(mInfoAlpha, shown ? 1.0f : 0.0f); 793 mAnim.setDuration(mActivatedAnimDuration); 794 mAnim.setInterpolator(new DecelerateInterpolator()); 795 mAnim.setAnimationListener(new Animation.AnimationListener() { 796 @Override 797 public void onAnimationStart(Animation animation) { 798 } 799 800 @Override 801 public void onAnimationEnd(Animation animation) { 802 if (mInfoAlpha == 0.0) { 803 for (int i = 0; i < mInfoViewList.size(); i++) { 804 mInfoViewList.get(i).setVisibility(View.GONE); 805 } 806 } 807 } 808 809 @Override 810 public void onAnimationRepeat(Animation animation) { 811 } 812 813 }); 814 startAnimation(mAnim); 815 } 816 817 @Override 818 public LayoutParams generateLayoutParams(AttributeSet attrs) { 819 return new BaseCardView.LayoutParams(getContext(), attrs); 820 } 821 822 @Override 823 protected LayoutParams generateDefaultLayoutParams() { 824 return new BaseCardView.LayoutParams( 825 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 826 } 827 828 @Override 829 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 830 if (lp instanceof LayoutParams) { 831 return new LayoutParams((LayoutParams) lp); 832 } else { 833 return new LayoutParams(lp); 834 } 835 } 836 837 @Override 838 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 839 return p instanceof BaseCardView.LayoutParams; 840 } 841 842 /** 843 * Per-child layout information associated with BaseCardView. 844 */ 845 public static class LayoutParams extends FrameLayout.LayoutParams { 846 public static final int VIEW_TYPE_MAIN = 0; 847 public static final int VIEW_TYPE_INFO = 1; 848 public static final int VIEW_TYPE_EXTRA = 2; 849 850 /** 851 * Card component type for the view associated with these LayoutParams. 852 */ 853 @ViewDebug.ExportedProperty(category = "layout", mapping = { 854 @ViewDebug.IntToString(from = VIEW_TYPE_MAIN, to = "MAIN"), 855 @ViewDebug.IntToString(from = VIEW_TYPE_INFO, to = "INFO"), 856 @ViewDebug.IntToString(from = VIEW_TYPE_EXTRA, to = "EXTRA") 857 }) 858 public int viewType = VIEW_TYPE_MAIN; 859 860 /** 861 * {@inheritDoc} 862 */ 863 public LayoutParams(Context c, AttributeSet attrs) { 864 super(c, attrs); 865 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView_Layout); 866 867 viewType = a.getInt( 868 R.styleable.lbBaseCardView_Layout_layout_viewType, VIEW_TYPE_MAIN); 869 870 a.recycle(); 871 } 872 873 /** 874 * {@inheritDoc} 875 */ 876 public LayoutParams(int width, int height) { 877 super(width, height); 878 } 879 880 /** 881 * {@inheritDoc} 882 */ 883 public LayoutParams(ViewGroup.LayoutParams p) { 884 super(p); 885 } 886 887 /** 888 * Copy constructor. Clones the width, height, and View Type of the 889 * source. 890 * 891 * @param source The layout params to copy from. 892 */ 893 public LayoutParams(LayoutParams source) { 894 super(source); 895 896 this.viewType = source.viewType; 897 } 898 } 899 900 class AnimationBase extends Animation { 901 902 @VisibleForTesting 903 final void mockStart() { 904 getTransformation(0, null); 905 } 906 907 @VisibleForTesting 908 final void mockEnd() { 909 applyTransformation(1f, null); 910 cancelAnimations(); 911 } 912 } 913 914 // Helper animation class used in the animation of the info and extra 915 // fields vertically within the card 916 final class InfoOffsetAnimation extends AnimationBase { 917 private float mStartValue; 918 private float mDelta; 919 920 public InfoOffsetAnimation(float start, float end) { 921 mStartValue = start; 922 mDelta = end - start; 923 } 924 925 @Override 926 protected void applyTransformation(float interpolatedTime, Transformation t) { 927 mInfoOffset = mStartValue + (interpolatedTime * mDelta); 928 requestLayout(); 929 } 930 } 931 932 // Helper animation class used in the animation of the visible height 933 // for the info fields. 934 final class InfoHeightAnimation extends AnimationBase { 935 private float mStartValue; 936 private float mDelta; 937 938 public InfoHeightAnimation(float start, float end) { 939 mStartValue = start; 940 mDelta = end - start; 941 } 942 943 @Override 944 protected void applyTransformation(float interpolatedTime, Transformation t) { 945 mInfoVisFraction = mStartValue + (interpolatedTime * mDelta); 946 requestLayout(); 947 } 948 } 949 950 // Helper animation class used to animate the alpha for the info views 951 // when they are fading in or out of view. 952 final class InfoAlphaAnimation extends AnimationBase { 953 private float mStartValue; 954 private float mDelta; 955 956 public InfoAlphaAnimation(float start, float end) { 957 mStartValue = start; 958 mDelta = end - start; 959 } 960 961 @Override 962 protected void applyTransformation(float interpolatedTime, Transformation t) { 963 mInfoAlpha = mStartValue + (interpolatedTime * mDelta); 964 for (int i = 0; i < mInfoViewList.size(); i++) { 965 mInfoViewList.get(i).setAlpha(mInfoAlpha); 966 } 967 } 968 } 969 970 @Override 971 public String toString() { 972 if (DEBUG) { 973 StringBuilder sb = new StringBuilder(); 974 sb.append(this.getClass().getSimpleName()).append(" : "); 975 sb.append("cardType="); 976 switch(mCardType) { 977 case CARD_TYPE_MAIN_ONLY: 978 sb.append("MAIN_ONLY"); 979 break; 980 case CARD_TYPE_INFO_OVER: 981 sb.append("INFO_OVER"); 982 break; 983 case CARD_TYPE_INFO_UNDER: 984 sb.append("INFO_UNDER"); 985 break; 986 case CARD_TYPE_INFO_UNDER_WITH_EXTRA: 987 sb.append("INFO_UNDER_WITH_EXTRA"); 988 break; 989 default: 990 sb.append("INVALID"); 991 break; 992 } 993 sb.append(" : "); 994 sb.append(mMainViewList.size()).append(" main views, "); 995 sb.append(mInfoViewList.size()).append(" info views, "); 996 sb.append(mExtraViewList.size()).append(" extra views : "); 997 sb.append("infoVisibility=").append(mInfoVisibility).append(" "); 998 sb.append("extraVisibility=").append(mExtraVisibility).append(" "); 999 sb.append("isActivated=").append(isActivated()); 1000 sb.append(" : "); 1001 sb.append("isSelected=").append(isSelected()); 1002 return sb.toString(); 1003 } else { 1004 return super.toString(); 1005 } 1006 } 1007} 1008