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