ExpandingEntryCardView.java revision edd44f03e0e7aa57d4eb721147d4a3f3d2551c1a
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 */ 16package com.android.contacts.quickcontact; 17 18import android.animation.ObjectAnimator; 19import android.content.Context; 20import android.content.Intent; 21import android.content.res.Resources; 22import android.graphics.ColorFilter; 23import android.graphics.Rect; 24import android.graphics.drawable.Drawable; 25import android.support.v7.widget.CardView; 26import android.text.TextUtils; 27import android.transition.ChangeBounds; 28import android.transition.ChangeScroll; 29import android.transition.Fade; 30import android.transition.Transition; 31import android.transition.Transition.TransitionListener; 32import android.transition.TransitionManager; 33import android.transition.TransitionSet; 34import android.util.AttributeSet; 35import android.util.Log; 36import android.view.ContextMenu.ContextMenuInfo; 37import android.view.LayoutInflater; 38import android.view.TouchDelegate; 39import android.view.View; 40import android.view.View.OnCreateContextMenuListener; 41import android.view.ViewGroup; 42import android.widget.FrameLayout; 43import android.widget.ImageView; 44import android.widget.LinearLayout; 45import android.widget.RelativeLayout; 46import android.widget.TextView; 47 48import com.android.contacts.R; 49 50import java.util.ArrayList; 51import java.util.List; 52 53/** 54 * Display entries in a LinearLayout that can be expanded to show all entries. 55 */ 56public class ExpandingEntryCardView extends CardView { 57 58 private static final String TAG = "ExpandingEntryCardView"; 59 private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200; 60 private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100; 61 62 public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300; 63 public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300; 64 65 /** 66 * Entry data. 67 */ 68 public static final class Entry { 69 70 private final int mId; 71 private final Drawable mIcon; 72 private final String mHeader; 73 private final String mSubHeader; 74 private final Drawable mSubHeaderIcon; 75 private final String mText; 76 private final Drawable mTextIcon; 77 private final Intent mIntent; 78 private final Drawable mAlternateIcon; 79 private final Intent mAlternateIntent; 80 private final String mAlternateContentDescription; 81 private final boolean mShouldApplyColor; 82 private final boolean mIsEditable; 83 private final EntryContextMenuInfo mEntryContextMenuInfo; 84 85 public Entry(int id, Drawable icon, String header, String subHeader, String text, 86 Intent intent, Drawable alternateIcon, Intent alternateIntent, 87 String alternateContentDescription, boolean shouldApplyColor, 88 boolean isEditable, EntryContextMenuInfo entryContextMenuInfo) { 89 this(id, icon, header, subHeader, null, text, null, intent, alternateIcon, 90 alternateIntent, alternateContentDescription, shouldApplyColor, isEditable, 91 entryContextMenuInfo); 92 } 93 94 public Entry(int id, Drawable mainIcon, String header, String subHeader, 95 Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent, 96 Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription, 97 boolean shouldApplyColor, boolean isEditable, 98 EntryContextMenuInfo entryContextMenuInfo) { 99 mId = id; 100 mIcon = mainIcon; 101 mHeader = header; 102 mSubHeader = subHeader; 103 mSubHeaderIcon = subHeaderIcon; 104 mText = text; 105 mTextIcon = textIcon; 106 mIntent = intent; 107 mAlternateIcon = alternateIcon; 108 mAlternateIntent = alternateIntent; 109 mAlternateContentDescription = alternateContentDescription; 110 mShouldApplyColor = shouldApplyColor; 111 mIsEditable = isEditable; 112 mEntryContextMenuInfo = entryContextMenuInfo; 113 } 114 115 Drawable getIcon() { 116 return mIcon; 117 } 118 119 String getHeader() { 120 return mHeader; 121 } 122 123 String getSubHeader() { 124 return mSubHeader; 125 } 126 127 Drawable getSubHeaderIcon() { 128 return mSubHeaderIcon; 129 } 130 131 public String getText() { 132 return mText; 133 } 134 135 Drawable getTextIcon() { 136 return mTextIcon; 137 } 138 139 Intent getIntent() { 140 return mIntent; 141 } 142 143 Drawable getAlternateIcon() { 144 return mAlternateIcon; 145 } 146 147 Intent getAlternateIntent() { 148 return mAlternateIntent; 149 } 150 151 String getAlternateContentDescription() { 152 return mAlternateContentDescription; 153 } 154 155 boolean shouldApplyColor() { 156 return mShouldApplyColor; 157 } 158 159 boolean isEditable() { 160 return mIsEditable; 161 } 162 163 int getId() { 164 return mId; 165 } 166 167 EntryContextMenuInfo getEntryContextMenuInfo() { 168 return mEntryContextMenuInfo; 169 } 170 } 171 172 public interface ExpandingEntryCardViewListener { 173 void onCollapse(int heightDelta); 174 void onExpand(int heightDelta); 175 } 176 177 private View mExpandCollapseButton; 178 private TextView mExpandCollapseTextView; 179 private TextView mTitleTextView; 180 private CharSequence mExpandButtonText; 181 private CharSequence mCollapseButtonText; 182 private OnClickListener mOnClickListener; 183 private OnCreateContextMenuListener mOnCreateContextMenuListener; 184 private boolean mIsExpanded = false; 185 /** 186 * The max number of entries to show in a collapsed card. If there are less entries passed in, 187 * then they are all shown. 188 */ 189 private int mCollapsedEntriesCount; 190 private ExpandingEntryCardViewListener mListener; 191 private List<List<Entry>> mEntries; 192 private int mNumEntries = 0; 193 private boolean mAllEntriesInflated = false; 194 private List<List<View>> mEntryViews; 195 private LinearLayout mEntriesViewGroup; 196 private final ImageView mExpandCollapseArrow; 197 private int mThemeColor; 198 private ColorFilter mThemeColorFilter; 199 private boolean mIsAlwaysExpanded; 200 /** The ViewGroup to run the expand/collapse animation on */ 201 private ViewGroup mAnimationViewGroup; 202 private LinearLayout mBadgeContainer; 203 private final List<ImageView> mBadges; 204 private LinearLayout mContainer; 205 206 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() { 207 @Override 208 public void onClick(View v) { 209 if (mIsExpanded) { 210 collapse(); 211 } else { 212 expand(); 213 } 214 } 215 }; 216 217 public ExpandingEntryCardView(Context context) { 218 this(context, null); 219 } 220 221 public ExpandingEntryCardView(Context context, AttributeSet attrs) { 222 super(context, attrs); 223 LayoutInflater inflater = LayoutInflater.from(context); 224 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); 225 mEntriesViewGroup = (LinearLayout) 226 expandingEntryCardView.findViewById(R.id.content_area_linear_layout); 227 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); 228 mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container); 229 230 mExpandCollapseButton = inflater.inflate( 231 R.layout.quickcontact_expanding_entry_card_button, this, false); 232 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text); 233 mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow); 234 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener); 235 mBadgeContainer = (LinearLayout) mExpandCollapseButton.findViewById(R.id.badge_container); 236 237 mBadges = new ArrayList<ImageView>(); 238 } 239 240 /** 241 * Sets the Entry list to display. 242 * 243 * @param entries The Entry list to display. 244 */ 245 public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries, 246 boolean isExpanded, boolean isAlwaysExpanded, 247 ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) { 248 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 249 mIsExpanded = isExpanded; 250 mIsAlwaysExpanded = isAlwaysExpanded; 251 // If isAlwaysExpanded is true, mIsExpanded should be true 252 mIsExpanded |= mIsAlwaysExpanded; 253 mEntryViews = new ArrayList<List<View>>(entries.size()); 254 mEntries = entries; 255 mNumEntries = 0; 256 mAllEntriesInflated = false; 257 for (List<Entry> entryList : mEntries) { 258 mNumEntries += entryList.size(); 259 mEntryViews.add(new ArrayList<View>()); 260 } 261 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries); 262 mListener = listener; 263 mAnimationViewGroup = animationViewGroup; 264 265 if (mIsExpanded) { 266 updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0); 267 inflateAllEntries(layoutInflater); 268 } else { 269 updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0); 270 inflateInitialEntries(layoutInflater); 271 } 272 insertEntriesIntoViewGroup(); 273 applyColor(); 274 } 275 276 /** 277 * Sets the text for the expand button. 278 * 279 * @param expandButtonText The expand button text. 280 */ 281 public void setExpandButtonText(CharSequence expandButtonText) { 282 mExpandButtonText = expandButtonText; 283 if (mExpandCollapseTextView != null && !mIsExpanded) { 284 mExpandCollapseTextView.setText(expandButtonText); 285 } 286 } 287 288 /** 289 * Sets the text for the expand button. 290 * 291 * @param expandButtonText The expand button text. 292 */ 293 public void setCollapseButtonText(CharSequence expandButtonText) { 294 mCollapseButtonText = expandButtonText; 295 if (mExpandCollapseTextView != null && mIsExpanded) { 296 mExpandCollapseTextView.setText(mCollapseButtonText); 297 } 298 } 299 300 @Override 301 public void setOnClickListener(OnClickListener listener) { 302 mOnClickListener = listener; 303 } 304 305 @Override 306 public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) { 307 mOnCreateContextMenuListener = listener; 308 } 309 310 private void insertEntriesIntoViewGroup() { 311 mEntriesViewGroup.removeAllViews(); 312 313 if (mIsExpanded) { 314 for (List<View> viewList : mEntryViews) { 315 if (viewList != mEntryViews.get(0)) { 316 addSeparator(viewList.get(0)); 317 } 318 for (View view : viewList) { 319 addEntry(view); 320 } 321 } 322 } else { 323 // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the 324 // number of entries that need to be added that are not the head element of a list 325 // to reach mCollapsedEntriesCount. 326 int numInViewGroup = 0; 327 int extraEntries = mCollapsedEntriesCount - mEntryViews.size(); 328 for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount; 329 i++) { 330 List<View> entryViewList = mEntryViews.get(i); 331 if (i > 0) { 332 addSeparator(entryViewList.get(0)); 333 } 334 addEntry(entryViewList.get(0)); 335 numInViewGroup++; 336 // Insert entries in this list to hit mCollapsedEntriesCount. 337 for (int j = 1; 338 j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount && 339 extraEntries > 0; 340 j++) { 341 addEntry(entryViewList.get(j)); 342 numInViewGroup++; 343 extraEntries--; 344 } 345 } 346 } 347 348 removeView(mExpandCollapseButton); 349 if (mCollapsedEntriesCount < mNumEntries 350 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) { 351 mContainer.addView(mExpandCollapseButton, -1); 352 } 353 } 354 355 private void addEntry(View entry) { 356 // If no title and the first entry in the group, add extra padding 357 if (TextUtils.isEmpty(mTitleTextView.getText()) && 358 mEntriesViewGroup.getChildCount() == 0) { 359 entry.setPadding(entry.getPaddingLeft(), 360 entry.getPaddingTop() + getResources().getDimensionPixelSize( 361 R.dimen.expanding_entry_card_null_title_top_extra_padding), 362 entry.getPaddingRight(), 363 entry.getPaddingBottom()); 364 } 365 mEntriesViewGroup.addView(entry); 366 } 367 368 private void addSeparator(View entry) { 369 View separator = new View(getContext()); 370 Resources res = getResources(); 371 372 separator.setBackgroundColor(res.getColor( 373 R.color.expanding_entry_card_item_separator_color)); 374 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 375 ViewGroup.LayoutParams.MATCH_PARENT, 376 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_separator_height)); 377 // The separator is aligned with the text in the entry. This is offset by a default 378 // margin. If there is an icon present, the icon's width and margin are added 379 int marginStart = res.getDimensionPixelSize( 380 R.dimen.expanding_entry_card_item_padding_start); 381 ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon); 382 if (entryIcon.getVisibility() == View.VISIBLE) { 383 int imageWidthAndMargin = 384 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) + 385 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing); 386 marginStart += imageWidthAndMargin; 387 } 388 layoutParams.setMarginStart(marginStart); 389 separator.setLayoutParams(layoutParams); 390 mEntriesViewGroup.addView(separator); 391 } 392 393 private CharSequence getExpandButtonText() { 394 if (!TextUtils.isEmpty(mExpandButtonText)) { 395 return mExpandButtonText; 396 } else { 397 // Default to "See more". 398 return getResources().getText(R.string.expanding_entry_card_view_see_more); 399 } 400 } 401 402 private CharSequence getCollapseButtonText() { 403 if (!TextUtils.isEmpty(mCollapseButtonText)) { 404 return mCollapseButtonText; 405 } else { 406 // Default to "See less". 407 return getResources().getText(R.string.expanding_entry_card_view_see_less); 408 } 409 } 410 411 /** 412 * Inflates the initial entries to be shown. 413 */ 414 private void inflateInitialEntries(LayoutInflater layoutInflater) { 415 // If the number of collapsed entries equals total entries, inflate all 416 if (mCollapsedEntriesCount == mNumEntries) { 417 inflateAllEntries(layoutInflater); 418 } else { 419 // Otherwise inflate the top entry from each list 420 // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached. 421 int numInflated = 0; 422 int extraEntries = mCollapsedEntriesCount - mEntries.size(); 423 for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) { 424 List<Entry> entryList = mEntries.get(i); 425 List<View> entryViewList = mEntryViews.get(i); 426 427 entryViewList.add(createEntryView(layoutInflater, entryList.get(0), 428 /* showIcon = */ View.VISIBLE)); 429 numInflated++; 430 // Inflate entries in this list to hit mCollapsedEntriesCount. 431 for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount && 432 extraEntries > 0; j++) { 433 entryViewList.add(createEntryView(layoutInflater, entryList.get(j), 434 /* showIcon = */ View.INVISIBLE)); 435 numInflated++; 436 extraEntries--; 437 } 438 } 439 } 440 } 441 442 /** 443 * Inflates all entries. 444 */ 445 private void inflateAllEntries(LayoutInflater layoutInflater) { 446 if (mAllEntriesInflated) { 447 return; 448 } 449 for (int i = 0; i < mEntries.size(); i++) { 450 List<Entry> entryList = mEntries.get(i); 451 List<View> viewList = mEntryViews.get(i); 452 for (int j = viewList.size(); j < entryList.size(); j++) { 453 final int iconVisibility; 454 final Entry entry = entryList.get(j); 455 // If the entry does not have an icon, mark gone. Else if it has an icon, show 456 // for the first Entry in the list only 457 if (entry.getIcon() == null) { 458 iconVisibility = View.GONE; 459 } else if (j == 0) { 460 iconVisibility = View.VISIBLE; 461 } else { 462 iconVisibility = View.INVISIBLE; 463 } 464 viewList.add(createEntryView(layoutInflater, entry, iconVisibility)); 465 } 466 } 467 mAllEntriesInflated = true; 468 } 469 470 public void setColorAndFilter(int color, ColorFilter colorFilter) { 471 mThemeColor = color; 472 mThemeColorFilter = colorFilter; 473 applyColor(); 474 } 475 476 public void setEntryHeaderColor(int color) { 477 if (mEntries != null) { 478 for (List<View> entryList : mEntryViews) { 479 for (View entryView : entryList) { 480 TextView header = (TextView) entryView.findViewById(R.id.header); 481 if (header != null) { 482 header.setTextColor(color); 483 } 484 } 485 } 486 } 487 } 488 489 /** 490 * The ColorFilter is passed in along with the color so that a new one only needs to be created 491 * once for the entire activity. 492 * 1. Title 493 * 2. Entry icons 494 * 3. Expand/Collapse Text 495 * 4. Expand/Collapse Button 496 */ 497 public void applyColor() { 498 if (mThemeColor != 0 && mThemeColorFilter != null) { 499 // Title 500 if (mTitleTextView != null) { 501 mTitleTextView.setTextColor(mThemeColor); 502 } 503 504 // Entry icons 505 if (mEntries != null) { 506 for (List<Entry> entryList : mEntries) { 507 for (Entry entry : entryList) { 508 if (entry.shouldApplyColor()) { 509 Drawable icon = entry.getIcon(); 510 if (icon != null) { 511 icon.setColorFilter(mThemeColorFilter); 512 } 513 } 514 Drawable alternateIcon = entry.getAlternateIcon(); 515 if (alternateIcon != null) { 516 alternateIcon.setColorFilter(mThemeColorFilter); 517 } 518 } 519 } 520 } 521 522 // Expand/Collapse 523 mExpandCollapseTextView.setTextColor(mThemeColor); 524 mExpandCollapseArrow.setColorFilter(mThemeColorFilter); 525 } 526 } 527 528 private View createEntryView(LayoutInflater layoutInflater, final Entry entry, 529 int iconVisibility) { 530 final EntryView view = (EntryView) layoutInflater.inflate( 531 R.layout.expanding_entry_card_item, this, false); 532 533 view.setContextMenuInfo(entry.getEntryContextMenuInfo()); 534 535 final ImageView icon = (ImageView) view.findViewById(R.id.icon); 536 icon.setVisibility(iconVisibility); 537 if (entry.getIcon() != null) { 538 icon.setImageDrawable(entry.getIcon()); 539 } 540 final TextView header = (TextView) view.findViewById(R.id.header); 541 if (!TextUtils.isEmpty(entry.getHeader())) { 542 header.setText(entry.getHeader()); 543 } else { 544 header.setVisibility(View.GONE); 545 } 546 547 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header); 548 if (!TextUtils.isEmpty(entry.getSubHeader())) { 549 subHeader.setText(entry.getSubHeader()); 550 } else { 551 subHeader.setVisibility(View.GONE); 552 } 553 554 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); 555 if (entry.getSubHeaderIcon() != null) { 556 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); 557 } else { 558 subHeaderIcon.setVisibility(View.GONE); 559 } 560 561 final TextView text = (TextView) view.findViewById(R.id.text); 562 if (!TextUtils.isEmpty(entry.getText())) { 563 text.setText(entry.getText()); 564 } else { 565 text.setVisibility(View.GONE); 566 } 567 568 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); 569 if (entry.getTextIcon() != null) { 570 textIcon.setImageDrawable(entry.getTextIcon()); 571 } else { 572 textIcon.setVisibility(View.GONE); 573 } 574 575 if (entry.getIntent() != null) { 576 view.setOnClickListener(mOnClickListener); 577 view.setTag(new EntryTag(entry.getId(), entry.getIntent())); 578 } 579 580 // If only the header is visible, add a top margin to match icon's top margin. 581 // Also increase the space below the header for visual comfort. 582 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE && 583 text.getVisibility() == View.GONE) { 584 RelativeLayout.LayoutParams headerLayoutParams = 585 (RelativeLayout.LayoutParams) header.getLayoutParams(); 586 headerLayoutParams.topMargin = (int) (getResources().getDimension( 587 R.dimen.expanding_entry_card_item_header_only_margin_top)); 588 headerLayoutParams.bottomMargin += (int) (getResources().getDimension( 589 R.dimen.expanding_entry_card_item_header_only_margin_bottom)); 590 header.setLayoutParams(headerLayoutParams); 591 } 592 593 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); 594 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { 595 alternateIcon.setImageDrawable(entry.getAlternateIcon()); 596 alternateIcon.setOnClickListener(mOnClickListener); 597 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent())); 598 alternateIcon.setVisibility(View.VISIBLE); 599 alternateIcon.setContentDescription(entry.getAlternateContentDescription()); 600 601 // Expand the clickable area for alternate icon to be top to bottom and to end edge 602 // of the entry view 603 view.post(new Runnable() { 604 @Override 605 public void run() { 606 final Rect alternateIconRect = new Rect(); 607 alternateIcon.getHitRect(alternateIconRect); 608 609 alternateIconRect.bottom = view.getHeight(); 610 alternateIconRect.top = 0; 611 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 612 alternateIconRect.left = 0; 613 } else { 614 alternateIconRect.right = view.getWidth(); 615 } 616 final TouchDelegate touchDelegate = 617 new TouchDelegate(alternateIconRect, alternateIcon); 618 view.setTouchDelegate(touchDelegate); 619 } 620 }); 621 } 622 623 // Adjust the top padding size for entries with an invisible icon. The padding depends on 624 // if there is a sub header or text section 625 if (iconVisibility == View.INVISIBLE && 626 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) { 627 view.setPaddingRelative(view.getPaddingStart(), 628 getResources().getDimensionPixelSize( 629 R.dimen.expanding_entry_card_item_no_icon_margin_top), 630 view.getPaddingEnd(), 631 view.getPaddingBottom()); 632 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader()) 633 && TextUtils.isEmpty(entry.getText())) { 634 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(), 635 view.getPaddingBottom()); 636 } 637 638 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 639 640 return view; 641 } 642 643 private void updateExpandCollapseButton(CharSequence buttonText, long duration) { 644 if (mIsExpanded) { 645 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 646 "rotation", 180); 647 animator.setDuration(duration); 648 animator.start(); 649 } else { 650 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 651 "rotation", 0); 652 animator.setDuration(duration); 653 animator.start(); 654 } 655 updateBadges(); 656 657 mExpandCollapseTextView.setText(buttonText); 658 } 659 660 private void updateBadges() { 661 if (mIsExpanded) { 662 mBadgeContainer.removeAllViews(); 663 } else { 664 // Inflate badges if not yet created 665 if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) { 666 for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) { 667 Drawable badgeDrawable = mEntries.get(i).get(0).getIcon(); 668 if (badgeDrawable != null) { 669 ImageView badgeView = new ImageView(getContext()); 670 LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams( 671 (int) getResources().getDimension( 672 R.dimen.expanding_entry_card_item_icon_width), 673 (int) getResources().getDimension( 674 R.dimen.expanding_entry_card_item_icon_height)); 675 badgeViewParams.setMarginEnd((int) getResources().getDimension( 676 R.dimen.expanding_entry_card_badge_separator_margin)); 677 badgeView.setLayoutParams(badgeViewParams); 678 badgeView.setImageDrawable(badgeDrawable); 679 mBadges.add(badgeView); 680 } 681 } 682 } 683 mBadgeContainer.removeAllViews(); 684 for (ImageView badge : mBadges) { 685 mBadgeContainer.addView(badge); 686 } 687 } 688 } 689 690 private void expand() { 691 ChangeBounds boundsTransition = new ChangeBounds(); 692 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 693 694 Fade fadeIn = new Fade(Fade.IN); 695 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); 696 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); 697 698 TransitionSet transitionSet = new TransitionSet(); 699 transitionSet.addTransition(boundsTransition); 700 transitionSet.addTransition(fadeIn); 701 702 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 703 this : mAnimationViewGroup; 704 705 transitionSet.addListener(new TransitionListener() { 706 @Override 707 public void onTransitionStart(Transition transition) { 708 // The listener is used to turn off suppressing, the proper delta is not necessary 709 mListener.onExpand(0); 710 } 711 712 @Override 713 public void onTransitionEnd(Transition transition) { 714 } 715 716 @Override 717 public void onTransitionCancel(Transition transition) { 718 } 719 720 @Override 721 public void onTransitionPause(Transition transition) { 722 } 723 724 @Override 725 public void onTransitionResume(Transition transition) { 726 } 727 }); 728 729 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 730 731 mIsExpanded = true; 732 // In order to insert new entries, we may need to inflate them for the first time 733 inflateAllEntries(LayoutInflater.from(getContext())); 734 insertEntriesIntoViewGroup(); 735 updateExpandCollapseButton(getCollapseButtonText(), 736 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 737 } 738 739 private void collapse() { 740 final int startingHeight = mEntriesViewGroup.getMeasuredHeight(); 741 mIsExpanded = false; 742 updateExpandCollapseButton(getExpandButtonText(), 743 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 744 745 final ChangeBounds boundsTransition = new ChangeBounds(); 746 boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 747 748 final ChangeScroll scrollTransition = new ChangeScroll(); 749 scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 750 751 TransitionSet transitionSet = new TransitionSet(); 752 transitionSet.addTransition(boundsTransition); 753 transitionSet.addTransition(scrollTransition); 754 755 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 756 this : mAnimationViewGroup; 757 758 boundsTransition.addListener(new TransitionListener() { 759 @Override 760 public void onTransitionStart(Transition transition) { 761 /* 762 * onTransitionStart is called after the view hierarchy has been changed but before 763 * the animation begins. 764 */ 765 int finishingHeight = mEntriesViewGroup.getMeasuredHeight(); 766 mListener.onCollapse(startingHeight - finishingHeight); 767 } 768 769 @Override 770 public void onTransitionEnd(Transition transition) { 771 } 772 773 @Override 774 public void onTransitionCancel(Transition transition) { 775 } 776 777 @Override 778 public void onTransitionPause(Transition transition) { 779 } 780 781 @Override 782 public void onTransitionResume(Transition transition) { 783 } 784 }); 785 786 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 787 788 insertEntriesIntoViewGroup(); 789 } 790 791 /** 792 * Returns whether the view is currently in its expanded state. 793 */ 794 public boolean isExpanded() { 795 return mIsExpanded; 796 } 797 798 /** 799 * Sets the title text of this ExpandingEntryCardView. 800 * @param title The title to set. A null title will result in the title being removed. 801 */ 802 public void setTitle(String title) { 803 if (mTitleTextView == null) { 804 Log.e(TAG, "mTitleTextView is null"); 805 } 806 mTitleTextView.setText(title); 807 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); 808 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ? 809 View.GONE : View.VISIBLE); 810 // If the title is set after children have been added, reset the top entry's padding to 811 // the default. Else if the title is cleared after children have been added, set 812 // the extra top padding 813 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 814 View firstEntry = mEntriesViewGroup.getChildAt(0); 815 firstEntry.setPadding(firstEntry.getPaddingLeft(), 816 getResources().getDimensionPixelSize( 817 R.dimen.expanding_entry_card_item_padding_top), 818 firstEntry.getPaddingRight(), 819 firstEntry.getPaddingBottom()); 820 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 821 View firstEntry = mEntriesViewGroup.getChildAt(0); 822 firstEntry.setPadding(firstEntry.getPaddingLeft(), 823 getResources().getDimensionPixelSize( 824 R.dimen.expanding_entry_card_item_padding_top) + 825 getResources().getDimensionPixelSize( 826 R.dimen.expanding_entry_card_null_title_top_extra_padding), 827 firstEntry.getPaddingRight(), 828 firstEntry.getPaddingBottom()); 829 } 830 } 831 832 public boolean shouldShow() { 833 return mEntries != null && mEntries.size() > 0; 834 } 835 836 public static final class EntryView extends RelativeLayout { 837 private EntryContextMenuInfo mEntryContextMenuInfo; 838 839 public EntryView(Context context) { 840 super(context); 841 } 842 843 public EntryView(Context context, AttributeSet attrs) { 844 super(context, attrs); 845 } 846 847 public void setContextMenuInfo(EntryContextMenuInfo info) { 848 mEntryContextMenuInfo = info; 849 } 850 851 @Override 852 protected ContextMenuInfo getContextMenuInfo() { 853 return mEntryContextMenuInfo; 854 } 855 } 856 857 public static final class EntryContextMenuInfo implements ContextMenuInfo { 858 private final String mCopyText; 859 private final String mCopyLabel; 860 861 public EntryContextMenuInfo(String copyText, String copyLabel) { 862 mCopyText = copyText; 863 mCopyLabel = copyLabel; 864 } 865 866 public String getCopyText() { 867 return mCopyText; 868 } 869 870 public String getCopyLabel() { 871 return mCopyLabel; 872 } 873 } 874 875 static final class EntryTag { 876 private final int mId; 877 private final Intent mIntent; 878 879 public EntryTag(int id, Intent intent) { 880 mId = id; 881 mIntent = intent; 882 } 883 884 public int getId() { 885 return mId; 886 } 887 888 public Intent getIntent() { 889 return mIntent; 890 } 891 } 892} 893