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