ExpandingEntryCardView.java revision a42ef76251778161d27bc07db214b8c81720e476
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 (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) { 581 // Remove the click effect 582 view.setBackground(null); 583 } 584 585 // If only the header is visible, add a top margin to match icon's top margin. 586 // Also increase the space below the header for visual comfort. 587 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE && 588 text.getVisibility() == View.GONE) { 589 RelativeLayout.LayoutParams headerLayoutParams = 590 (RelativeLayout.LayoutParams) header.getLayoutParams(); 591 headerLayoutParams.topMargin = (int) (getResources().getDimension( 592 R.dimen.expanding_entry_card_item_header_only_margin_top)); 593 headerLayoutParams.bottomMargin += (int) (getResources().getDimension( 594 R.dimen.expanding_entry_card_item_header_only_margin_bottom)); 595 header.setLayoutParams(headerLayoutParams); 596 } 597 598 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); 599 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { 600 alternateIcon.setImageDrawable(entry.getAlternateIcon()); 601 alternateIcon.setOnClickListener(mOnClickListener); 602 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent())); 603 alternateIcon.setVisibility(View.VISIBLE); 604 alternateIcon.setContentDescription(entry.getAlternateContentDescription()); 605 606 // Expand the clickable area for alternate icon to be top to bottom and to end edge 607 // of the entry view 608 view.post(new Runnable() { 609 @Override 610 public void run() { 611 final Rect alternateIconRect = new Rect(); 612 alternateIcon.getHitRect(alternateIconRect); 613 614 alternateIconRect.bottom = view.getHeight(); 615 alternateIconRect.top = 0; 616 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 617 alternateIconRect.left = 0; 618 } else { 619 alternateIconRect.right = view.getWidth(); 620 } 621 final TouchDelegate touchDelegate = 622 new TouchDelegate(alternateIconRect, alternateIcon); 623 view.setTouchDelegate(touchDelegate); 624 } 625 }); 626 } 627 628 // Adjust the top padding size for entries with an invisible icon. The padding depends on 629 // if there is a sub header or text section 630 if (iconVisibility == View.INVISIBLE && 631 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) { 632 view.setPaddingRelative(view.getPaddingStart(), 633 getResources().getDimensionPixelSize( 634 R.dimen.expanding_entry_card_item_no_icon_margin_top), 635 view.getPaddingEnd(), 636 view.getPaddingBottom()); 637 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader()) 638 && TextUtils.isEmpty(entry.getText())) { 639 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(), 640 view.getPaddingBottom()); 641 } 642 643 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 644 645 return view; 646 } 647 648 private void updateExpandCollapseButton(CharSequence buttonText, long duration) { 649 if (mIsExpanded) { 650 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 651 "rotation", 180); 652 animator.setDuration(duration); 653 animator.start(); 654 } else { 655 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 656 "rotation", 0); 657 animator.setDuration(duration); 658 animator.start(); 659 } 660 updateBadges(); 661 662 mExpandCollapseTextView.setText(buttonText); 663 } 664 665 private void updateBadges() { 666 if (mIsExpanded) { 667 mBadgeContainer.removeAllViews(); 668 } else { 669 // Inflate badges if not yet created 670 if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) { 671 for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) { 672 Drawable badgeDrawable = mEntries.get(i).get(0).getIcon(); 673 if (badgeDrawable != null) { 674 ImageView badgeView = new ImageView(getContext()); 675 LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams( 676 (int) getResources().getDimension( 677 R.dimen.expanding_entry_card_item_icon_width), 678 (int) getResources().getDimension( 679 R.dimen.expanding_entry_card_item_icon_height)); 680 badgeViewParams.setMarginEnd((int) getResources().getDimension( 681 R.dimen.expanding_entry_card_badge_separator_margin)); 682 badgeView.setLayoutParams(badgeViewParams); 683 badgeView.setImageDrawable(badgeDrawable); 684 mBadges.add(badgeView); 685 } 686 } 687 } 688 mBadgeContainer.removeAllViews(); 689 for (ImageView badge : mBadges) { 690 mBadgeContainer.addView(badge); 691 } 692 } 693 } 694 695 private void expand() { 696 ChangeBounds boundsTransition = new ChangeBounds(); 697 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 698 699 Fade fadeIn = new Fade(Fade.IN); 700 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); 701 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); 702 703 TransitionSet transitionSet = new TransitionSet(); 704 transitionSet.addTransition(boundsTransition); 705 transitionSet.addTransition(fadeIn); 706 707 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 708 this : mAnimationViewGroup; 709 710 transitionSet.addListener(new TransitionListener() { 711 @Override 712 public void onTransitionStart(Transition transition) { 713 // The listener is used to turn off suppressing, the proper delta is not necessary 714 mListener.onExpand(0); 715 } 716 717 @Override 718 public void onTransitionEnd(Transition transition) { 719 } 720 721 @Override 722 public void onTransitionCancel(Transition transition) { 723 } 724 725 @Override 726 public void onTransitionPause(Transition transition) { 727 } 728 729 @Override 730 public void onTransitionResume(Transition transition) { 731 } 732 }); 733 734 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 735 736 mIsExpanded = true; 737 // In order to insert new entries, we may need to inflate them for the first time 738 inflateAllEntries(LayoutInflater.from(getContext())); 739 insertEntriesIntoViewGroup(); 740 updateExpandCollapseButton(getCollapseButtonText(), 741 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 742 } 743 744 private void collapse() { 745 final int startingHeight = mEntriesViewGroup.getMeasuredHeight(); 746 mIsExpanded = false; 747 updateExpandCollapseButton(getExpandButtonText(), 748 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 749 750 final ChangeBounds boundsTransition = new ChangeBounds(); 751 boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 752 753 final ChangeScroll scrollTransition = new ChangeScroll(); 754 scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 755 756 TransitionSet transitionSet = new TransitionSet(); 757 transitionSet.addTransition(boundsTransition); 758 transitionSet.addTransition(scrollTransition); 759 760 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 761 this : mAnimationViewGroup; 762 763 boundsTransition.addListener(new TransitionListener() { 764 @Override 765 public void onTransitionStart(Transition transition) { 766 /* 767 * onTransitionStart is called after the view hierarchy has been changed but before 768 * the animation begins. 769 */ 770 int finishingHeight = mEntriesViewGroup.getMeasuredHeight(); 771 mListener.onCollapse(startingHeight - finishingHeight); 772 } 773 774 @Override 775 public void onTransitionEnd(Transition transition) { 776 } 777 778 @Override 779 public void onTransitionCancel(Transition transition) { 780 } 781 782 @Override 783 public void onTransitionPause(Transition transition) { 784 } 785 786 @Override 787 public void onTransitionResume(Transition transition) { 788 } 789 }); 790 791 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 792 793 insertEntriesIntoViewGroup(); 794 } 795 796 /** 797 * Returns whether the view is currently in its expanded state. 798 */ 799 public boolean isExpanded() { 800 return mIsExpanded; 801 } 802 803 /** 804 * Sets the title text of this ExpandingEntryCardView. 805 * @param title The title to set. A null title will result in the title being removed. 806 */ 807 public void setTitle(String title) { 808 if (mTitleTextView == null) { 809 Log.e(TAG, "mTitleTextView is null"); 810 } 811 mTitleTextView.setText(title); 812 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); 813 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ? 814 View.GONE : View.VISIBLE); 815 // If the title is set after children have been added, reset the top entry's padding to 816 // the default. Else if the title is cleared after children have been added, set 817 // the extra top padding 818 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 819 View firstEntry = mEntriesViewGroup.getChildAt(0); 820 firstEntry.setPadding(firstEntry.getPaddingLeft(), 821 getResources().getDimensionPixelSize( 822 R.dimen.expanding_entry_card_item_padding_top), 823 firstEntry.getPaddingRight(), 824 firstEntry.getPaddingBottom()); 825 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 826 View firstEntry = mEntriesViewGroup.getChildAt(0); 827 firstEntry.setPadding(firstEntry.getPaddingLeft(), 828 getResources().getDimensionPixelSize( 829 R.dimen.expanding_entry_card_item_padding_top) + 830 getResources().getDimensionPixelSize( 831 R.dimen.expanding_entry_card_null_title_top_extra_padding), 832 firstEntry.getPaddingRight(), 833 firstEntry.getPaddingBottom()); 834 } 835 } 836 837 public boolean shouldShow() { 838 return mEntries != null && mEntries.size() > 0; 839 } 840 841 public static final class EntryView extends RelativeLayout { 842 private EntryContextMenuInfo mEntryContextMenuInfo; 843 844 public EntryView(Context context) { 845 super(context); 846 } 847 848 public EntryView(Context context, AttributeSet attrs) { 849 super(context, attrs); 850 } 851 852 public void setContextMenuInfo(EntryContextMenuInfo info) { 853 mEntryContextMenuInfo = info; 854 } 855 856 @Override 857 protected ContextMenuInfo getContextMenuInfo() { 858 return mEntryContextMenuInfo; 859 } 860 } 861 862 public static final class EntryContextMenuInfo implements ContextMenuInfo { 863 private final String mCopyText; 864 private final String mCopyLabel; 865 866 public EntryContextMenuInfo(String copyText, String copyLabel) { 867 mCopyText = copyText; 868 mCopyLabel = copyLabel; 869 } 870 871 public String getCopyText() { 872 return mCopyText; 873 } 874 875 public String getCopyLabel() { 876 return mCopyLabel; 877 } 878 } 879 880 static final class EntryTag { 881 private final int mId; 882 private final Intent mIntent; 883 884 public EntryTag(int id, Intent intent) { 885 mId = id; 886 mIntent = intent; 887 } 888 889 public int getId() { 890 return mId; 891 } 892 893 public Intent getIntent() { 894 return mIntent; 895 } 896 } 897} 898