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