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