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