ExpandingEntryCardView.java revision 7ce5352a70e8aaf120bf4f7bd05d595f46abb080
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.Spannable; 27import android.text.TextUtils; 28import android.transition.ChangeBounds; 29import android.transition.ChangeScroll; 30import android.transition.Fade; 31import android.transition.Transition; 32import android.transition.Transition.TransitionListener; 33import android.transition.TransitionManager; 34import android.transition.TransitionSet; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.view.ContextMenu.ContextMenuInfo; 38import android.view.LayoutInflater; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.ViewConfiguration; 42import android.view.ViewGroup; 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 Spannable mPrimaryContentDescription; 78 private final Intent mIntent; 79 private final Drawable mAlternateIcon; 80 private final Intent mAlternateIntent; 81 private final String mAlternateContentDescription; 82 private final boolean mShouldApplyColor; 83 private final boolean mIsEditable; 84 private final EntryContextMenuInfo mEntryContextMenuInfo; 85 private final Drawable mThirdIcon; 86 private final Intent mThirdIntent; 87 private final String mThirdContentDescription; 88 private final int mIconResourceId; 89 90 public Entry(int id, Drawable mainIcon, String header, String subHeader, 91 Drawable subHeaderIcon, String text, Drawable textIcon, 92 Spannable primaryContentDescription, Intent intent, 93 Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription, 94 boolean shouldApplyColor, boolean isEditable, 95 EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent, 96 String thirdContentDescription, int iconResourceId) { 97 mId = id; 98 mIcon = mainIcon; 99 mHeader = header; 100 mSubHeader = subHeader; 101 mSubHeaderIcon = subHeaderIcon; 102 mText = text; 103 mTextIcon = textIcon; 104 mPrimaryContentDescription = primaryContentDescription; 105 mIntent = intent; 106 mAlternateIcon = alternateIcon; 107 mAlternateIntent = alternateIntent; 108 mAlternateContentDescription = alternateContentDescription; 109 mShouldApplyColor = shouldApplyColor; 110 mIsEditable = isEditable; 111 mEntryContextMenuInfo = entryContextMenuInfo; 112 mThirdIcon = thirdIcon; 113 mThirdIntent = thirdIntent; 114 mThirdContentDescription = thirdContentDescription; 115 mIconResourceId = iconResourceId; 116 } 117 118 Drawable getIcon() { 119 return mIcon; 120 } 121 122 String getHeader() { 123 return mHeader; 124 } 125 126 String getSubHeader() { 127 return mSubHeader; 128 } 129 130 Drawable getSubHeaderIcon() { 131 return mSubHeaderIcon; 132 } 133 134 public String getText() { 135 return mText; 136 } 137 138 Drawable getTextIcon() { 139 return mTextIcon; 140 } 141 142 Spannable getPrimaryContentDescription() { 143 return mPrimaryContentDescription; 144 } 145 146 Intent getIntent() { 147 return mIntent; 148 } 149 150 Drawable getAlternateIcon() { 151 return mAlternateIcon; 152 } 153 154 Intent getAlternateIntent() { 155 return mAlternateIntent; 156 } 157 158 String getAlternateContentDescription() { 159 return mAlternateContentDescription; 160 } 161 162 boolean shouldApplyColor() { 163 return mShouldApplyColor; 164 } 165 166 boolean isEditable() { 167 return mIsEditable; 168 } 169 170 int getId() { 171 return mId; 172 } 173 174 EntryContextMenuInfo getEntryContextMenuInfo() { 175 return mEntryContextMenuInfo; 176 } 177 178 Drawable getThirdIcon() { 179 return mThirdIcon; 180 } 181 182 Intent getThirdIntent() { 183 return mThirdIntent; 184 } 185 186 String getThirdContentDescription() { 187 return mThirdContentDescription; 188 } 189 190 int getIconResourceId() { 191 return mIconResourceId; 192 } 193 } 194 195 public interface ExpandingEntryCardViewListener { 196 void onCollapse(int heightDelta); 197 void onExpand(int heightDelta); 198 } 199 200 private View mExpandCollapseButton; 201 private TextView mExpandCollapseTextView; 202 private TextView mTitleTextView; 203 private CharSequence mExpandButtonText; 204 private CharSequence mCollapseButtonText; 205 private OnClickListener mOnClickListener; 206 private OnCreateContextMenuListener mOnCreateContextMenuListener; 207 private boolean mIsExpanded = false; 208 /** 209 * The max number of entries to show in a collapsed card. If there are less entries passed in, 210 * then they are all shown. 211 */ 212 private int mCollapsedEntriesCount; 213 private ExpandingEntryCardViewListener mListener; 214 private List<List<Entry>> mEntries; 215 private int mNumEntries = 0; 216 private boolean mAllEntriesInflated = false; 217 private List<List<View>> mEntryViews; 218 private LinearLayout mEntriesViewGroup; 219 private final ImageView mExpandCollapseArrow; 220 private int mThemeColor; 221 private ColorFilter mThemeColorFilter; 222 private boolean mIsAlwaysExpanded; 223 /** The ViewGroup to run the expand/collapse animation on */ 224 private ViewGroup mAnimationViewGroup; 225 private LinearLayout mBadgeContainer; 226 private final List<ImageView> mBadges; 227 private final List<Integer> mBadgeIds; 228 /** 229 * List to hold the separators. This saves us from reconstructing every expand/collapse and 230 * provides a smoother animation. 231 */ 232 private List<View> mSeparators; 233 private LinearLayout mContainer; 234 235 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() { 236 @Override 237 public void onClick(View v) { 238 if (mIsExpanded) { 239 collapse(); 240 } else { 241 expand(); 242 } 243 } 244 }; 245 246 public ExpandingEntryCardView(Context context) { 247 this(context, null); 248 } 249 250 public ExpandingEntryCardView(Context context, AttributeSet attrs) { 251 super(context, attrs); 252 LayoutInflater inflater = LayoutInflater.from(context); 253 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); 254 mEntriesViewGroup = (LinearLayout) 255 expandingEntryCardView.findViewById(R.id.content_area_linear_layout); 256 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); 257 mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container); 258 259 mExpandCollapseButton = inflater.inflate( 260 R.layout.quickcontact_expanding_entry_card_button, this, false); 261 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text); 262 mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow); 263 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener); 264 mBadgeContainer = (LinearLayout) mExpandCollapseButton.findViewById(R.id.badge_container); 265 266 mBadges = new ArrayList<ImageView>(); 267 mBadgeIds = new ArrayList<Integer>(); 268 } 269 270 /** 271 * Sets the Entry list to display. 272 * 273 * @param entries The Entry list to display. 274 */ 275 public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries, 276 boolean isExpanded, boolean isAlwaysExpanded, 277 ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) { 278 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 279 mIsExpanded = isExpanded; 280 mIsAlwaysExpanded = isAlwaysExpanded; 281 // If isAlwaysExpanded is true, mIsExpanded should be true 282 mIsExpanded |= mIsAlwaysExpanded; 283 mEntryViews = new ArrayList<List<View>>(entries.size()); 284 mEntries = entries; 285 mNumEntries = 0; 286 mAllEntriesInflated = false; 287 for (List<Entry> entryList : mEntries) { 288 mNumEntries += entryList.size(); 289 mEntryViews.add(new ArrayList<View>()); 290 } 291 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries); 292 // We need a separator between each list, but not after the last one 293 if (entries.size() > 1) { 294 mSeparators = new ArrayList<>(entries.size() - 1); 295 } 296 mListener = listener; 297 mAnimationViewGroup = animationViewGroup; 298 299 if (mIsExpanded) { 300 updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0); 301 inflateAllEntries(layoutInflater); 302 } else { 303 updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0); 304 inflateInitialEntries(layoutInflater); 305 } 306 insertEntriesIntoViewGroup(); 307 applyColor(); 308 } 309 310 /** 311 * Sets the text for the expand button. 312 * 313 * @param expandButtonText The expand button text. 314 */ 315 public void setExpandButtonText(CharSequence expandButtonText) { 316 mExpandButtonText = expandButtonText; 317 if (mExpandCollapseTextView != null && !mIsExpanded) { 318 mExpandCollapseTextView.setText(expandButtonText); 319 } 320 } 321 322 /** 323 * Sets the text for the expand button. 324 * 325 * @param expandButtonText The expand button text. 326 */ 327 public void setCollapseButtonText(CharSequence expandButtonText) { 328 mCollapseButtonText = expandButtonText; 329 if (mExpandCollapseTextView != null && mIsExpanded) { 330 mExpandCollapseTextView.setText(mCollapseButtonText); 331 } 332 } 333 334 @Override 335 public void setOnClickListener(OnClickListener listener) { 336 mOnClickListener = listener; 337 } 338 339 @Override 340 public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) { 341 mOnCreateContextMenuListener = listener; 342 } 343 344 private void insertEntriesIntoViewGroup() { 345 mEntriesViewGroup.removeAllViews(); 346 347 if (mIsExpanded) { 348 for (int i = 0; i < mEntryViews.size(); i++) { 349 List<View> viewList = mEntryViews.get(i); 350 if (i > 0) { 351 View separator; 352 if (mSeparators.size() <= i - 1) { 353 separator = generateSeparator(viewList.get(0)); 354 mSeparators.add(separator); 355 } else { 356 separator = mSeparators.get(i - 1); 357 } 358 mEntriesViewGroup.addView(separator); 359 } 360 for (View view : viewList) { 361 addEntry(view); 362 } 363 } 364 } else { 365 // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the 366 // number of entries that need to be added that are not the head element of a list 367 // to reach mCollapsedEntriesCount. 368 int numInViewGroup = 0; 369 int extraEntries = mCollapsedEntriesCount - mEntryViews.size(); 370 for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount; 371 i++) { 372 List<View> entryViewList = mEntryViews.get(i); 373 if (i > 0) { 374 View separator; 375 if (mSeparators.size() <= i - 1) { 376 separator = generateSeparator(entryViewList.get(0)); 377 mSeparators.add(separator); 378 } else { 379 separator = mSeparators.get(i - 1); 380 } 381 mEntriesViewGroup.addView(separator); 382 } 383 addEntry(entryViewList.get(0)); 384 numInViewGroup++; 385 // Insert entries in this list to hit mCollapsedEntriesCount. 386 for (int j = 1; 387 j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount && 388 extraEntries > 0; 389 j++) { 390 addEntry(entryViewList.get(j)); 391 numInViewGroup++; 392 extraEntries--; 393 } 394 } 395 } 396 397 removeView(mExpandCollapseButton); 398 if (mCollapsedEntriesCount < mNumEntries 399 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) { 400 mContainer.addView(mExpandCollapseButton, -1); 401 } 402 } 403 404 private void addEntry(View entry) { 405 // If no title and the first entry in the group, add extra padding 406 if (TextUtils.isEmpty(mTitleTextView.getText()) && 407 mEntriesViewGroup.getChildCount() == 0) { 408 entry.setPadding(entry.getPaddingLeft(), 409 getResources().getDimensionPixelSize( 410 R.dimen.expanding_entry_card_item_padding_top) + 411 getResources().getDimensionPixelSize( 412 R.dimen.expanding_entry_card_null_title_top_extra_padding), 413 entry.getPaddingRight(), 414 entry.getPaddingBottom()); 415 } 416 mEntriesViewGroup.addView(entry); 417 } 418 419 private View generateSeparator(View entry) { 420 View separator = new View(getContext()); 421 Resources res = getResources(); 422 423 separator.setBackgroundColor(res.getColor( 424 R.color.expanding_entry_card_item_separator_color)); 425 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 426 ViewGroup.LayoutParams.MATCH_PARENT, 427 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_separator_height)); 428 // The separator is aligned with the text in the entry. This is offset by a default 429 // margin. If there is an icon present, the icon's width and margin are added 430 int marginStart = res.getDimensionPixelSize( 431 R.dimen.expanding_entry_card_item_padding_start); 432 ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon); 433 if (entryIcon.getVisibility() == View.VISIBLE) { 434 int imageWidthAndMargin = 435 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) + 436 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing); 437 marginStart += imageWidthAndMargin; 438 } 439 layoutParams.setMarginStart(marginStart); 440 separator.setLayoutParams(layoutParams); 441 return separator; 442 } 443 444 private CharSequence getExpandButtonText() { 445 if (!TextUtils.isEmpty(mExpandButtonText)) { 446 return mExpandButtonText; 447 } else { 448 // Default to "See more". 449 return getResources().getText(R.string.expanding_entry_card_view_see_more); 450 } 451 } 452 453 private CharSequence getCollapseButtonText() { 454 if (!TextUtils.isEmpty(mCollapseButtonText)) { 455 return mCollapseButtonText; 456 } else { 457 // Default to "See less". 458 return getResources().getText(R.string.expanding_entry_card_view_see_less); 459 } 460 } 461 462 /** 463 * Inflates the initial entries to be shown. 464 */ 465 private void inflateInitialEntries(LayoutInflater layoutInflater) { 466 // If the number of collapsed entries equals total entries, inflate all 467 if (mCollapsedEntriesCount == mNumEntries) { 468 inflateAllEntries(layoutInflater); 469 } else { 470 // Otherwise inflate the top entry from each list 471 // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached. 472 int numInflated = 0; 473 int extraEntries = mCollapsedEntriesCount - mEntries.size(); 474 for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) { 475 List<Entry> entryList = mEntries.get(i); 476 List<View> entryViewList = mEntryViews.get(i); 477 478 entryViewList.add(createEntryView(layoutInflater, entryList.get(0), 479 /* showIcon = */ View.VISIBLE)); 480 numInflated++; 481 // Inflate entries in this list to hit mCollapsedEntriesCount. 482 for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount && 483 extraEntries > 0; j++) { 484 entryViewList.add(createEntryView(layoutInflater, entryList.get(j), 485 /* showIcon = */ View.INVISIBLE)); 486 numInflated++; 487 extraEntries--; 488 } 489 } 490 } 491 } 492 493 /** 494 * Inflates all entries. 495 */ 496 private void inflateAllEntries(LayoutInflater layoutInflater) { 497 if (mAllEntriesInflated) { 498 return; 499 } 500 for (int i = 0; i < mEntries.size(); i++) { 501 List<Entry> entryList = mEntries.get(i); 502 List<View> viewList = mEntryViews.get(i); 503 for (int j = viewList.size(); j < entryList.size(); j++) { 504 final int iconVisibility; 505 final Entry entry = entryList.get(j); 506 // If the entry does not have an icon, mark gone. Else if it has an icon, show 507 // for the first Entry in the list only 508 if (entry.getIcon() == null) { 509 iconVisibility = View.GONE; 510 } else if (j == 0) { 511 iconVisibility = View.VISIBLE; 512 } else { 513 iconVisibility = View.INVISIBLE; 514 } 515 viewList.add(createEntryView(layoutInflater, entry, iconVisibility)); 516 } 517 } 518 mAllEntriesInflated = true; 519 } 520 521 public void setColorAndFilter(int color, ColorFilter colorFilter) { 522 mThemeColor = color; 523 mThemeColorFilter = colorFilter; 524 applyColor(); 525 } 526 527 public void setEntryHeaderColor(int color) { 528 if (mEntries != null) { 529 for (List<View> entryList : mEntryViews) { 530 for (View entryView : entryList) { 531 TextView header = (TextView) entryView.findViewById(R.id.header); 532 if (header != null) { 533 header.setTextColor(color); 534 } 535 } 536 } 537 } 538 } 539 540 /** 541 * The ColorFilter is passed in along with the color so that a new one only needs to be created 542 * once for the entire activity. 543 * 1. Title 544 * 2. Entry icons 545 * 3. Expand/Collapse Text 546 * 4. Expand/Collapse Button 547 */ 548 public void applyColor() { 549 if (mThemeColor != 0 && mThemeColorFilter != null) { 550 // Title 551 if (mTitleTextView != null) { 552 mTitleTextView.setTextColor(mThemeColor); 553 } 554 555 // Entry icons 556 if (mEntries != null) { 557 for (List<Entry> entryList : mEntries) { 558 for (Entry entry : entryList) { 559 if (entry.shouldApplyColor()) { 560 Drawable icon = entry.getIcon(); 561 if (icon != null) { 562 icon.setColorFilter(mThemeColorFilter); 563 } 564 } 565 Drawable alternateIcon = entry.getAlternateIcon(); 566 if (alternateIcon != null) { 567 alternateIcon.setColorFilter(mThemeColorFilter); 568 } 569 Drawable thirdIcon = entry.getThirdIcon(); 570 if (thirdIcon != null) { 571 thirdIcon.setColorFilter(mThemeColorFilter); 572 } 573 } 574 } 575 } 576 577 // Expand/Collapse 578 mExpandCollapseTextView.setTextColor(mThemeColor); 579 mExpandCollapseArrow.setColorFilter(mThemeColorFilter); 580 } 581 } 582 583 private View createEntryView(LayoutInflater layoutInflater, final Entry entry, 584 int iconVisibility) { 585 final EntryView view = (EntryView) layoutInflater.inflate( 586 R.layout.expanding_entry_card_item, this, false); 587 588 view.setContextMenuInfo(entry.getEntryContextMenuInfo()); 589 if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) { 590 view.setContentDescription(entry.getPrimaryContentDescription()); 591 } 592 593 final ImageView icon = (ImageView) view.findViewById(R.id.icon); 594 icon.setVisibility(iconVisibility); 595 if (entry.getIcon() != null) { 596 icon.setImageDrawable(entry.getIcon()); 597 } 598 final TextView header = (TextView) view.findViewById(R.id.header); 599 if (!TextUtils.isEmpty(entry.getHeader())) { 600 header.setText(entry.getHeader()); 601 } else { 602 header.setVisibility(View.GONE); 603 } 604 605 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header); 606 if (!TextUtils.isEmpty(entry.getSubHeader())) { 607 subHeader.setText(entry.getSubHeader()); 608 } else { 609 subHeader.setVisibility(View.GONE); 610 } 611 612 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); 613 if (entry.getSubHeaderIcon() != null) { 614 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); 615 } else { 616 subHeaderIcon.setVisibility(View.GONE); 617 } 618 619 final TextView text = (TextView) view.findViewById(R.id.text); 620 if (!TextUtils.isEmpty(entry.getText())) { 621 text.setText(entry.getText()); 622 } else { 623 text.setVisibility(View.GONE); 624 } 625 626 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); 627 if (entry.getTextIcon() != null) { 628 textIcon.setImageDrawable(entry.getTextIcon()); 629 } else { 630 textIcon.setVisibility(View.GONE); 631 } 632 633 if (entry.getIntent() != null) { 634 view.setOnClickListener(mOnClickListener); 635 view.setTag(new EntryTag(entry.getId(), entry.getIntent())); 636 } 637 638 if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) { 639 // Remove the click effect 640 view.setBackground(null); 641 } 642 643 // If only the header is visible, add a top margin to match icon's top margin. 644 // Also increase the space below the header for visual comfort. 645 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE && 646 text.getVisibility() == View.GONE) { 647 RelativeLayout.LayoutParams headerLayoutParams = 648 (RelativeLayout.LayoutParams) header.getLayoutParams(); 649 headerLayoutParams.topMargin = (int) (getResources().getDimension( 650 R.dimen.expanding_entry_card_item_header_only_margin_top)); 651 headerLayoutParams.bottomMargin += (int) (getResources().getDimension( 652 R.dimen.expanding_entry_card_item_header_only_margin_bottom)); 653 header.setLayoutParams(headerLayoutParams); 654 } 655 656 // Adjust the top padding size for entries with an invisible icon. The padding depends on 657 // if there is a sub header or text section 658 if (iconVisibility == View.INVISIBLE && 659 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) { 660 view.setPaddingRelative(view.getPaddingStart(), 661 getResources().getDimensionPixelSize( 662 R.dimen.expanding_entry_card_item_no_icon_margin_top), 663 view.getPaddingEnd(), 664 view.getPaddingBottom()); 665 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader()) 666 && TextUtils.isEmpty(entry.getText())) { 667 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(), 668 view.getPaddingBottom()); 669 } 670 671 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); 672 final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon); 673 674 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { 675 alternateIcon.setImageDrawable(entry.getAlternateIcon()); 676 alternateIcon.setOnClickListener(mOnClickListener); 677 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent())); 678 alternateIcon.setVisibility(View.VISIBLE); 679 alternateIcon.setContentDescription(entry.getAlternateContentDescription()); 680 } 681 682 if (entry.getThirdIcon() != null && entry.getThirdIntent() != null) { 683 thirdIcon.setImageDrawable(entry.getThirdIcon()); 684 thirdIcon.setOnClickListener(mOnClickListener); 685 thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent())); 686 thirdIcon.setVisibility(View.VISIBLE); 687 thirdIcon.setContentDescription(entry.getThirdContentDescription()); 688 } 689 690 // Set a custom touch listener for expanding the extra icon touch areas 691 view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon)); 692 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 693 694 return view; 695 } 696 697 private void updateExpandCollapseButton(CharSequence buttonText, long duration) { 698 if (mIsExpanded) { 699 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 700 "rotation", 180); 701 animator.setDuration(duration); 702 animator.start(); 703 } else { 704 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 705 "rotation", 0); 706 animator.setDuration(duration); 707 animator.start(); 708 } 709 updateBadges(); 710 711 mExpandCollapseTextView.setText(buttonText); 712 } 713 714 private void updateBadges() { 715 if (mIsExpanded) { 716 mBadgeContainer.removeAllViews(); 717 } else { 718 // Inflate badges if not yet created 719 if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) { 720 for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) { 721 Drawable badgeDrawable = mEntries.get(i).get(0).getIcon(); 722 int badgeResourceId = mEntries.get(i).get(0).getIconResourceId(); 723 // Do not add the same badge twice 724 if (badgeResourceId != 0 && mBadgeIds.contains(badgeResourceId)) { 725 continue; 726 } 727 if (badgeDrawable != null) { 728 ImageView badgeView = new ImageView(getContext()); 729 LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams( 730 (int) getResources().getDimension( 731 R.dimen.expanding_entry_card_item_icon_width), 732 (int) getResources().getDimension( 733 R.dimen.expanding_entry_card_item_icon_height)); 734 badgeViewParams.setMarginEnd((int) getResources().getDimension( 735 R.dimen.expanding_entry_card_badge_separator_margin)); 736 badgeView.setLayoutParams(badgeViewParams); 737 badgeView.setImageDrawable(badgeDrawable); 738 mBadges.add(badgeView); 739 mBadgeIds.add(badgeResourceId); 740 } 741 } 742 } 743 mBadgeContainer.removeAllViews(); 744 for (ImageView badge : mBadges) { 745 mBadgeContainer.addView(badge); 746 } 747 } 748 } 749 750 private void expand() { 751 ChangeBounds boundsTransition = new ChangeBounds(); 752 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 753 754 Fade fadeIn = new Fade(Fade.IN); 755 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); 756 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); 757 758 TransitionSet transitionSet = new TransitionSet(); 759 transitionSet.addTransition(boundsTransition); 760 transitionSet.addTransition(fadeIn); 761 762 transitionSet.excludeTarget(R.id.text, /* exclude = */ true); 763 764 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 765 this : mAnimationViewGroup; 766 767 transitionSet.addListener(new TransitionListener() { 768 @Override 769 public void onTransitionStart(Transition transition) { 770 // The listener is used to turn off suppressing, the proper delta is not necessary 771 mListener.onExpand(0); 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 mIsExpanded = true; 794 // In order to insert new entries, we may need to inflate them for the first time 795 inflateAllEntries(LayoutInflater.from(getContext())); 796 insertEntriesIntoViewGroup(); 797 updateExpandCollapseButton(getCollapseButtonText(), 798 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 799 } 800 801 private void collapse() { 802 final int startingHeight = mEntriesViewGroup.getMeasuredHeight(); 803 mIsExpanded = false; 804 updateExpandCollapseButton(getExpandButtonText(), 805 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 806 807 final ChangeBounds boundsTransition = new ChangeBounds(); 808 boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 809 810 final ChangeScroll scrollTransition = new ChangeScroll(); 811 scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 812 813 TransitionSet transitionSet = new TransitionSet(); 814 transitionSet.addTransition(boundsTransition); 815 transitionSet.addTransition(scrollTransition); 816 817 transitionSet.excludeTarget(R.id.text, /* exclude = */ true); 818 819 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 820 this : mAnimationViewGroup; 821 822 boundsTransition.addListener(new TransitionListener() { 823 @Override 824 public void onTransitionStart(Transition transition) { 825 /* 826 * onTransitionStart is called after the view hierarchy has been changed but before 827 * the animation begins. 828 */ 829 int finishingHeight = mEntriesViewGroup.getMeasuredHeight(); 830 mListener.onCollapse(startingHeight - finishingHeight); 831 } 832 833 @Override 834 public void onTransitionEnd(Transition transition) { 835 } 836 837 @Override 838 public void onTransitionCancel(Transition transition) { 839 } 840 841 @Override 842 public void onTransitionPause(Transition transition) { 843 } 844 845 @Override 846 public void onTransitionResume(Transition transition) { 847 } 848 }); 849 850 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 851 852 insertEntriesIntoViewGroup(); 853 } 854 855 /** 856 * Returns whether the view is currently in its expanded state. 857 */ 858 public boolean isExpanded() { 859 return mIsExpanded; 860 } 861 862 /** 863 * Sets the title text of this ExpandingEntryCardView. 864 * @param title The title to set. A null title will result in the title being removed. 865 */ 866 public void setTitle(String title) { 867 if (mTitleTextView == null) { 868 Log.e(TAG, "mTitleTextView is null"); 869 } 870 mTitleTextView.setText(title); 871 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); 872 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ? 873 View.GONE : View.VISIBLE); 874 // If the title is set after children have been added, reset the top entry's padding to 875 // the default. Else if the title is cleared after children have been added, set 876 // the extra top padding 877 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 878 View firstEntry = mEntriesViewGroup.getChildAt(0); 879 firstEntry.setPadding(firstEntry.getPaddingLeft(), 880 getResources().getDimensionPixelSize( 881 R.dimen.expanding_entry_card_item_padding_top), 882 firstEntry.getPaddingRight(), 883 firstEntry.getPaddingBottom()); 884 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 885 View firstEntry = mEntriesViewGroup.getChildAt(0); 886 firstEntry.setPadding(firstEntry.getPaddingLeft(), 887 getResources().getDimensionPixelSize( 888 R.dimen.expanding_entry_card_item_padding_top) + 889 getResources().getDimensionPixelSize( 890 R.dimen.expanding_entry_card_null_title_top_extra_padding), 891 firstEntry.getPaddingRight(), 892 firstEntry.getPaddingBottom()); 893 } 894 } 895 896 public boolean shouldShow() { 897 return mEntries != null && mEntries.size() > 0; 898 } 899 900 public static final class EntryView extends RelativeLayout { 901 private EntryContextMenuInfo mEntryContextMenuInfo; 902 903 public EntryView(Context context) { 904 super(context); 905 } 906 907 public EntryView(Context context, AttributeSet attrs) { 908 super(context, attrs); 909 } 910 911 public void setContextMenuInfo(EntryContextMenuInfo info) { 912 mEntryContextMenuInfo = info; 913 } 914 915 @Override 916 protected ContextMenuInfo getContextMenuInfo() { 917 return mEntryContextMenuInfo; 918 } 919 } 920 921 public static final class EntryContextMenuInfo implements ContextMenuInfo { 922 private final String mCopyText; 923 private final String mCopyLabel; 924 private final String mMimeType; 925 private final long mId; 926 private final boolean mIsSuperPrimary; 927 928 public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id, 929 boolean isSuperPrimary) { 930 mCopyText = copyText; 931 mCopyLabel = copyLabel; 932 mMimeType = mimeType; 933 mId = id; 934 mIsSuperPrimary = isSuperPrimary; 935 } 936 937 public String getCopyText() { 938 return mCopyText; 939 } 940 941 public String getCopyLabel() { 942 return mCopyLabel; 943 } 944 945 public String getMimeType() { 946 return mMimeType; 947 } 948 949 public long getId() { 950 return mId; 951 } 952 953 public boolean isSuperPrimary() { 954 return mIsSuperPrimary; 955 } 956 } 957 958 static final class EntryTag { 959 private final int mId; 960 private final Intent mIntent; 961 962 public EntryTag(int id, Intent intent) { 963 mId = id; 964 mIntent = intent; 965 } 966 967 public int getId() { 968 return mId; 969 } 970 971 public Intent getIntent() { 972 return mIntent; 973 } 974 } 975 976 /** 977 * This custom touch listener increases the touch area for the second and third icons, if 978 * they are present. This is necessary to maintain other properties on an entry view, like 979 * using a top padding on entry. Based off of {@link android.view.TouchDelegate} 980 */ 981 private static final class EntryTouchListener implements View.OnTouchListener { 982 private final View mEntry; 983 private final ImageView mAlternateIcon; 984 private final ImageView mThirdIcon; 985 /** mTouchedView locks in a view on touch down */ 986 private View mTouchedView; 987 /** mSlop adds some space to account for touches that are just outside the hit area */ 988 private int mSlop; 989 990 public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) { 991 mEntry = entry; 992 mAlternateIcon = alternateIcon; 993 mThirdIcon = thirdIcon; 994 mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop(); 995 } 996 997 @Override 998 public boolean onTouch(View v, MotionEvent event) { 999 View touchedView = mTouchedView; 1000 boolean sendToTouched = false; 1001 boolean hit = true; 1002 boolean handled = false; 1003 1004 switch (event.getAction()) { 1005 case MotionEvent.ACTION_DOWN: 1006 if (hitThirdIcon(event)) { 1007 mTouchedView = mThirdIcon; 1008 sendToTouched = true; 1009 } else if (hitAlternateIcon(event)) { 1010 mTouchedView = mAlternateIcon; 1011 sendToTouched = true; 1012 } else { 1013 mTouchedView = mEntry; 1014 sendToTouched = false; 1015 } 1016 touchedView = mTouchedView; 1017 break; 1018 case MotionEvent.ACTION_UP: 1019 case MotionEvent.ACTION_MOVE: 1020 sendToTouched = mTouchedView != null && mTouchedView != mEntry; 1021 if (sendToTouched) { 1022 final Rect slopBounds = new Rect(); 1023 touchedView.getHitRect(slopBounds); 1024 slopBounds.inset(-mSlop, -mSlop); 1025 if (!slopBounds.contains((int) event.getX(), (int) event.getY())) { 1026 hit = false; 1027 } 1028 } 1029 break; 1030 case MotionEvent.ACTION_CANCEL: 1031 sendToTouched = mTouchedView != null && mTouchedView != mEntry; 1032 mTouchedView = null; 1033 break; 1034 } 1035 if (sendToTouched) { 1036 if (hit) { 1037 event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2); 1038 } else { 1039 // Offset event coordinates to be outside the target view (in case it does 1040 // something like tracking pressed state) 1041 event.setLocation(-(mSlop * 2), -(mSlop * 2)); 1042 } 1043 handled = touchedView.dispatchTouchEvent(event); 1044 } 1045 return handled; 1046 } 1047 1048 private boolean hitThirdIcon(MotionEvent event) { 1049 if (mEntry.isLayoutRtl()) { 1050 return mThirdIcon.getVisibility() == View.VISIBLE && 1051 event.getX() < mThirdIcon.getRight(); 1052 } else { 1053 return mThirdIcon.getVisibility() == View.VISIBLE && 1054 event.getX() > mThirdIcon.getLeft(); 1055 } 1056 } 1057 1058 /** 1059 * Should be used after checking if third icon was hit 1060 */ 1061 private boolean hitAlternateIcon(MotionEvent event) { 1062 // LayoutParams used to add the start margin to the touch area 1063 final RelativeLayout.LayoutParams alternateIconParams = 1064 (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams(); 1065 if (mEntry.isLayoutRtl()) { 1066 return mAlternateIcon.getVisibility() == View.VISIBLE && 1067 event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin; 1068 } else { 1069 return mAlternateIcon.getVisibility() == View.VISIBLE && 1070 event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin; 1071 } 1072 } 1073 } 1074} 1075