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