1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.contacts.quickcontact; 17 18 import android.animation.ObjectAnimator; 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.res.Resources; 22 import android.graphics.ColorFilter; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.support.v7.widget.CardView; 26 import android.text.Spannable; 27 import android.text.TextUtils; 28 import android.transition.ChangeBounds; 29 import android.transition.ChangeScroll; 30 import android.transition.Fade; 31 import android.transition.Transition; 32 import android.transition.Transition.TransitionListener; 33 import android.transition.TransitionManager; 34 import android.transition.TransitionSet; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.view.ContextMenu.ContextMenuInfo; 38 import android.view.LayoutInflater; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.ViewConfiguration; 42 import android.view.ViewGroup; 43 import android.widget.ImageView; 44 import android.widget.LinearLayout; 45 import android.widget.RelativeLayout; 46 import android.widget.TextView; 47 48 import com.android.contacts.R; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 53 /** 54 * Display entries in a LinearLayout that can be expanded to show all entries. 55 */ 56 public 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 Entry(int id, Drawable mainIcon, String header, String subHeader, Drawable subHeaderIcon, String text, Drawable textIcon, Spannable primaryContentDescription, Intent intent, Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription, boolean shouldApplyColor, boolean isEditable, EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent, String thirdContentDescription, int iconResourceId)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 getIcon()118 Drawable getIcon() { 119 return mIcon; 120 } 121 getHeader()122 String getHeader() { 123 return mHeader; 124 } 125 getSubHeader()126 String getSubHeader() { 127 return mSubHeader; 128 } 129 getSubHeaderIcon()130 Drawable getSubHeaderIcon() { 131 return mSubHeaderIcon; 132 } 133 getText()134 public String getText() { 135 return mText; 136 } 137 getTextIcon()138 Drawable getTextIcon() { 139 return mTextIcon; 140 } 141 getPrimaryContentDescription()142 Spannable getPrimaryContentDescription() { 143 return mPrimaryContentDescription; 144 } 145 getIntent()146 Intent getIntent() { 147 return mIntent; 148 } 149 getAlternateIcon()150 Drawable getAlternateIcon() { 151 return mAlternateIcon; 152 } 153 getAlternateIntent()154 Intent getAlternateIntent() { 155 return mAlternateIntent; 156 } 157 getAlternateContentDescription()158 String getAlternateContentDescription() { 159 return mAlternateContentDescription; 160 } 161 shouldApplyColor()162 boolean shouldApplyColor() { 163 return mShouldApplyColor; 164 } 165 isEditable()166 boolean isEditable() { 167 return mIsEditable; 168 } 169 getId()170 int getId() { 171 return mId; 172 } 173 getEntryContextMenuInfo()174 EntryContextMenuInfo getEntryContextMenuInfo() { 175 return mEntryContextMenuInfo; 176 } 177 getThirdIcon()178 Drawable getThirdIcon() { 179 return mThirdIcon; 180 } 181 getThirdIntent()182 Intent getThirdIntent() { 183 return mThirdIntent; 184 } 185 getThirdContentDescription()186 String getThirdContentDescription() { 187 return mThirdContentDescription; 188 } 189 getIconResourceId()190 int getIconResourceId() { 191 return mIconResourceId; 192 } 193 } 194 195 public interface ExpandingEntryCardViewListener { onCollapse(int heightDelta)196 void onCollapse(int heightDelta); onExpand(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 ExpandingEntryCardView(Context context)246 public ExpandingEntryCardView(Context context) { 247 this(context, null); 248 } 249 ExpandingEntryCardView(Context context, AttributeSet attrs)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 */ initialize(List<List<Entry>> entries, int numInitialVisibleEntries, boolean isExpanded, boolean isAlwaysExpanded, ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup)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 */ setExpandButtonText(CharSequence expandButtonText)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 */ setCollapseButtonText(CharSequence expandButtonText)327 public void setCollapseButtonText(CharSequence expandButtonText) { 328 mCollapseButtonText = expandButtonText; 329 if (mExpandCollapseTextView != null && mIsExpanded) { 330 mExpandCollapseTextView.setText(mCollapseButtonText); 331 } 332 } 333 334 @Override setOnClickListener(OnClickListener listener)335 public void setOnClickListener(OnClickListener listener) { 336 mOnClickListener = listener; 337 } 338 339 @Override setOnCreateContextMenuListener(OnCreateContextMenuListener listener)340 public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) { 341 mOnCreateContextMenuListener = listener; 342 } 343 insertEntriesIntoViewGroup()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 addEntry(View entry)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 generateSeparator(View entry)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.divider_line_color_light)); 425 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 426 ViewGroup.LayoutParams.MATCH_PARENT, 427 res.getDimensionPixelSize(R.dimen.divider_line_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 getExpandButtonText()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 getCollapseButtonText()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 */ inflateInitialEntries(LayoutInflater layoutInflater)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 */ inflateAllEntries(LayoutInflater layoutInflater)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 setColorAndFilter(int color, ColorFilter colorFilter)521 public void setColorAndFilter(int color, ColorFilter colorFilter) { 522 mThemeColor = color; 523 mThemeColorFilter = colorFilter; 524 applyColor(); 525 } 526 setEntryHeaderColor(int color)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 */ applyColor()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.mutate(); 563 icon.setColorFilter(mThemeColorFilter); 564 } 565 } 566 Drawable alternateIcon = entry.getAlternateIcon(); 567 if (alternateIcon != null) { 568 alternateIcon.mutate(); 569 alternateIcon.setColorFilter(mThemeColorFilter); 570 } 571 Drawable thirdIcon = entry.getThirdIcon(); 572 if (thirdIcon != null) { 573 thirdIcon.mutate(); 574 thirdIcon.setColorFilter(mThemeColorFilter); 575 } 576 } 577 } 578 } 579 580 // Expand/Collapse 581 mExpandCollapseTextView.setTextColor(mThemeColor); 582 mExpandCollapseArrow.setColorFilter(mThemeColorFilter); 583 } 584 } 585 createEntryView(LayoutInflater layoutInflater, final Entry entry, int iconVisibility)586 private View createEntryView(LayoutInflater layoutInflater, final Entry entry, 587 int iconVisibility) { 588 final EntryView view = (EntryView) layoutInflater.inflate( 589 R.layout.expanding_entry_card_item, this, false); 590 591 view.setContextMenuInfo(entry.getEntryContextMenuInfo()); 592 if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) { 593 view.setContentDescription(entry.getPrimaryContentDescription()); 594 } 595 596 final ImageView icon = (ImageView) view.findViewById(R.id.icon); 597 icon.setVisibility(iconVisibility); 598 if (entry.getIcon() != null) { 599 icon.setImageDrawable(entry.getIcon()); 600 } 601 final TextView header = (TextView) view.findViewById(R.id.header); 602 if (!TextUtils.isEmpty(entry.getHeader())) { 603 header.setText(entry.getHeader()); 604 } else { 605 header.setVisibility(View.GONE); 606 } 607 608 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header); 609 if (!TextUtils.isEmpty(entry.getSubHeader())) { 610 subHeader.setText(entry.getSubHeader()); 611 } else { 612 subHeader.setVisibility(View.GONE); 613 } 614 615 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); 616 if (entry.getSubHeaderIcon() != null) { 617 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); 618 } else { 619 subHeaderIcon.setVisibility(View.GONE); 620 } 621 622 final TextView text = (TextView) view.findViewById(R.id.text); 623 if (!TextUtils.isEmpty(entry.getText())) { 624 text.setText(entry.getText()); 625 } else { 626 text.setVisibility(View.GONE); 627 } 628 629 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); 630 if (entry.getTextIcon() != null) { 631 textIcon.setImageDrawable(entry.getTextIcon()); 632 } else { 633 textIcon.setVisibility(View.GONE); 634 } 635 636 if (entry.getIntent() != null) { 637 view.setOnClickListener(mOnClickListener); 638 view.setTag(new EntryTag(entry.getId(), entry.getIntent())); 639 } 640 641 if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) { 642 // Remove the click effect 643 view.setBackground(null); 644 } 645 646 // If only the header is visible, add a top margin to match icon's top margin. 647 // Also increase the space below the header for visual comfort. 648 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE && 649 text.getVisibility() == View.GONE) { 650 RelativeLayout.LayoutParams headerLayoutParams = 651 (RelativeLayout.LayoutParams) header.getLayoutParams(); 652 headerLayoutParams.topMargin = (int) (getResources().getDimension( 653 R.dimen.expanding_entry_card_item_header_only_margin_top)); 654 headerLayoutParams.bottomMargin += (int) (getResources().getDimension( 655 R.dimen.expanding_entry_card_item_header_only_margin_bottom)); 656 header.setLayoutParams(headerLayoutParams); 657 } 658 659 // Adjust the top padding size for entries with an invisible icon. The padding depends on 660 // if there is a sub header or text section 661 if (iconVisibility == View.INVISIBLE && 662 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) { 663 view.setPaddingRelative(view.getPaddingStart(), 664 getResources().getDimensionPixelSize( 665 R.dimen.expanding_entry_card_item_no_icon_margin_top), 666 view.getPaddingEnd(), 667 view.getPaddingBottom()); 668 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader()) 669 && TextUtils.isEmpty(entry.getText())) { 670 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(), 671 view.getPaddingBottom()); 672 } 673 674 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); 675 final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon); 676 677 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { 678 alternateIcon.setImageDrawable(entry.getAlternateIcon()); 679 alternateIcon.setOnClickListener(mOnClickListener); 680 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent())); 681 alternateIcon.setVisibility(View.VISIBLE); 682 alternateIcon.setContentDescription(entry.getAlternateContentDescription()); 683 } 684 685 if (entry.getThirdIcon() != null && entry.getThirdIntent() != null) { 686 thirdIcon.setImageDrawable(entry.getThirdIcon()); 687 thirdIcon.setOnClickListener(mOnClickListener); 688 thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent())); 689 thirdIcon.setVisibility(View.VISIBLE); 690 thirdIcon.setContentDescription(entry.getThirdContentDescription()); 691 } 692 693 // Set a custom touch listener for expanding the extra icon touch areas 694 view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon)); 695 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 696 697 return view; 698 } 699 updateExpandCollapseButton(CharSequence buttonText, long duration)700 private void updateExpandCollapseButton(CharSequence buttonText, long duration) { 701 if (mIsExpanded) { 702 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 703 "rotation", 180); 704 animator.setDuration(duration); 705 animator.start(); 706 } else { 707 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, 708 "rotation", 0); 709 animator.setDuration(duration); 710 animator.start(); 711 } 712 updateBadges(); 713 714 mExpandCollapseTextView.setText(buttonText); 715 } 716 updateBadges()717 private void updateBadges() { 718 if (mIsExpanded) { 719 mBadgeContainer.removeAllViews(); 720 } else { 721 // Inflate badges if not yet created 722 if (mBadges.size() < mEntries.size() - mCollapsedEntriesCount) { 723 for (int i = mCollapsedEntriesCount; i < mEntries.size(); i++) { 724 Drawable badgeDrawable = mEntries.get(i).get(0).getIcon(); 725 int badgeResourceId = mEntries.get(i).get(0).getIconResourceId(); 726 // Do not add the same badge twice 727 if (badgeResourceId != 0 && mBadgeIds.contains(badgeResourceId)) { 728 continue; 729 } 730 if (badgeDrawable != null) { 731 ImageView badgeView = new ImageView(getContext()); 732 LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams( 733 (int) getResources().getDimension( 734 R.dimen.expanding_entry_card_item_icon_width), 735 (int) getResources().getDimension( 736 R.dimen.expanding_entry_card_item_icon_height)); 737 badgeViewParams.setMarginEnd((int) getResources().getDimension( 738 R.dimen.expanding_entry_card_badge_separator_margin)); 739 badgeView.setLayoutParams(badgeViewParams); 740 badgeView.setImageDrawable(badgeDrawable); 741 mBadges.add(badgeView); 742 mBadgeIds.add(badgeResourceId); 743 } 744 } 745 } 746 mBadgeContainer.removeAllViews(); 747 for (ImageView badge : mBadges) { 748 mBadgeContainer.addView(badge); 749 } 750 } 751 } 752 expand()753 private void expand() { 754 ChangeBounds boundsTransition = new ChangeBounds(); 755 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 756 757 Fade fadeIn = new Fade(Fade.IN); 758 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); 759 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); 760 761 TransitionSet transitionSet = new TransitionSet(); 762 transitionSet.addTransition(boundsTransition); 763 transitionSet.addTransition(fadeIn); 764 765 transitionSet.excludeTarget(R.id.text, /* exclude = */ true); 766 767 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 768 this : mAnimationViewGroup; 769 770 transitionSet.addListener(new TransitionListener() { 771 @Override 772 public void onTransitionStart(Transition transition) { 773 // The listener is used to turn off suppressing, the proper delta is not necessary 774 mListener.onExpand(0); 775 } 776 777 @Override 778 public void onTransitionEnd(Transition transition) { 779 } 780 781 @Override 782 public void onTransitionCancel(Transition transition) { 783 } 784 785 @Override 786 public void onTransitionPause(Transition transition) { 787 } 788 789 @Override 790 public void onTransitionResume(Transition transition) { 791 } 792 }); 793 794 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 795 796 mIsExpanded = true; 797 // In order to insert new entries, we may need to inflate them for the first time 798 inflateAllEntries(LayoutInflater.from(getContext())); 799 insertEntriesIntoViewGroup(); 800 updateExpandCollapseButton(getCollapseButtonText(), 801 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 802 } 803 collapse()804 private void collapse() { 805 final int startingHeight = mEntriesViewGroup.getMeasuredHeight(); 806 mIsExpanded = false; 807 updateExpandCollapseButton(getExpandButtonText(), 808 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 809 810 final ChangeBounds boundsTransition = new ChangeBounds(); 811 boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 812 813 final ChangeScroll scrollTransition = new ChangeScroll(); 814 scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 815 816 TransitionSet transitionSet = new TransitionSet(); 817 transitionSet.addTransition(boundsTransition); 818 transitionSet.addTransition(scrollTransition); 819 820 transitionSet.excludeTarget(R.id.text, /* exclude = */ true); 821 822 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 823 this : mAnimationViewGroup; 824 825 boundsTransition.addListener(new TransitionListener() { 826 @Override 827 public void onTransitionStart(Transition transition) { 828 /* 829 * onTransitionStart is called after the view hierarchy has been changed but before 830 * the animation begins. 831 */ 832 int finishingHeight = mEntriesViewGroup.getMeasuredHeight(); 833 mListener.onCollapse(startingHeight - finishingHeight); 834 } 835 836 @Override 837 public void onTransitionEnd(Transition transition) { 838 } 839 840 @Override 841 public void onTransitionCancel(Transition transition) { 842 } 843 844 @Override 845 public void onTransitionPause(Transition transition) { 846 } 847 848 @Override 849 public void onTransitionResume(Transition transition) { 850 } 851 }); 852 853 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 854 855 insertEntriesIntoViewGroup(); 856 } 857 858 /** 859 * Returns whether the view is currently in its expanded state. 860 */ isExpanded()861 public boolean isExpanded() { 862 return mIsExpanded; 863 } 864 865 /** 866 * Sets the title text of this ExpandingEntryCardView. 867 * @param title The title to set. A null title will result in the title being removed. 868 */ setTitle(String title)869 public void setTitle(String title) { 870 if (mTitleTextView == null) { 871 Log.e(TAG, "mTitleTextView is null"); 872 } 873 mTitleTextView.setText(title); 874 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); 875 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ? 876 View.GONE : View.VISIBLE); 877 // If the title is set after children have been added, reset the top entry's padding to 878 // the default. Else if the title is cleared after children have been added, set 879 // the extra top padding 880 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 881 View firstEntry = mEntriesViewGroup.getChildAt(0); 882 firstEntry.setPadding(firstEntry.getPaddingLeft(), 883 getResources().getDimensionPixelSize( 884 R.dimen.expanding_entry_card_item_padding_top), 885 firstEntry.getPaddingRight(), 886 firstEntry.getPaddingBottom()); 887 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { 888 View firstEntry = mEntriesViewGroup.getChildAt(0); 889 firstEntry.setPadding(firstEntry.getPaddingLeft(), 890 getResources().getDimensionPixelSize( 891 R.dimen.expanding_entry_card_item_padding_top) + 892 getResources().getDimensionPixelSize( 893 R.dimen.expanding_entry_card_null_title_top_extra_padding), 894 firstEntry.getPaddingRight(), 895 firstEntry.getPaddingBottom()); 896 } 897 } 898 shouldShow()899 public boolean shouldShow() { 900 return mEntries != null && mEntries.size() > 0; 901 } 902 903 public static final class EntryView extends RelativeLayout { 904 private EntryContextMenuInfo mEntryContextMenuInfo; 905 EntryView(Context context)906 public EntryView(Context context) { 907 super(context); 908 } 909 EntryView(Context context, AttributeSet attrs)910 public EntryView(Context context, AttributeSet attrs) { 911 super(context, attrs); 912 } 913 setContextMenuInfo(EntryContextMenuInfo info)914 public void setContextMenuInfo(EntryContextMenuInfo info) { 915 mEntryContextMenuInfo = info; 916 } 917 918 @Override getContextMenuInfo()919 protected ContextMenuInfo getContextMenuInfo() { 920 return mEntryContextMenuInfo; 921 } 922 } 923 924 public static final class EntryContextMenuInfo implements ContextMenuInfo { 925 private final String mCopyText; 926 private final String mCopyLabel; 927 private final String mMimeType; 928 private final long mId; 929 private final boolean mIsSuperPrimary; 930 EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id, boolean isSuperPrimary)931 public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id, 932 boolean isSuperPrimary) { 933 mCopyText = copyText; 934 mCopyLabel = copyLabel; 935 mMimeType = mimeType; 936 mId = id; 937 mIsSuperPrimary = isSuperPrimary; 938 } 939 getCopyText()940 public String getCopyText() { 941 return mCopyText; 942 } 943 getCopyLabel()944 public String getCopyLabel() { 945 return mCopyLabel; 946 } 947 getMimeType()948 public String getMimeType() { 949 return mMimeType; 950 } 951 getId()952 public long getId() { 953 return mId; 954 } 955 isSuperPrimary()956 public boolean isSuperPrimary() { 957 return mIsSuperPrimary; 958 } 959 } 960 961 static final class EntryTag { 962 private final int mId; 963 private final Intent mIntent; 964 EntryTag(int id, Intent intent)965 public EntryTag(int id, Intent intent) { 966 mId = id; 967 mIntent = intent; 968 } 969 getId()970 public int getId() { 971 return mId; 972 } 973 getIntent()974 public Intent getIntent() { 975 return mIntent; 976 } 977 } 978 979 /** 980 * This custom touch listener increases the touch area for the second and third icons, if 981 * they are present. This is necessary to maintain other properties on an entry view, like 982 * using a top padding on entry. Based off of {@link android.view.TouchDelegate} 983 */ 984 private static final class EntryTouchListener implements View.OnTouchListener { 985 private final View mEntry; 986 private final ImageView mAlternateIcon; 987 private final ImageView mThirdIcon; 988 /** mTouchedView locks in a view on touch down */ 989 private View mTouchedView; 990 /** mSlop adds some space to account for touches that are just outside the hit area */ 991 private int mSlop; 992 EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon)993 public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) { 994 mEntry = entry; 995 mAlternateIcon = alternateIcon; 996 mThirdIcon = thirdIcon; 997 mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop(); 998 } 999 1000 @Override onTouch(View v, MotionEvent event)1001 public boolean onTouch(View v, MotionEvent event) { 1002 View touchedView = mTouchedView; 1003 boolean sendToTouched = false; 1004 boolean hit = true; 1005 boolean handled = false; 1006 1007 switch (event.getAction()) { 1008 case MotionEvent.ACTION_DOWN: 1009 if (hitThirdIcon(event)) { 1010 mTouchedView = mThirdIcon; 1011 sendToTouched = true; 1012 } else if (hitAlternateIcon(event)) { 1013 mTouchedView = mAlternateIcon; 1014 sendToTouched = true; 1015 } else { 1016 mTouchedView = mEntry; 1017 sendToTouched = false; 1018 } 1019 touchedView = mTouchedView; 1020 break; 1021 case MotionEvent.ACTION_UP: 1022 case MotionEvent.ACTION_MOVE: 1023 sendToTouched = mTouchedView != null && mTouchedView != mEntry; 1024 if (sendToTouched) { 1025 final Rect slopBounds = new Rect(); 1026 touchedView.getHitRect(slopBounds); 1027 slopBounds.inset(-mSlop, -mSlop); 1028 if (!slopBounds.contains((int) event.getX(), (int) event.getY())) { 1029 hit = false; 1030 } 1031 } 1032 break; 1033 case MotionEvent.ACTION_CANCEL: 1034 sendToTouched = mTouchedView != null && mTouchedView != mEntry; 1035 mTouchedView = null; 1036 break; 1037 } 1038 if (sendToTouched) { 1039 if (hit) { 1040 event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2); 1041 } else { 1042 // Offset event coordinates to be outside the target view (in case it does 1043 // something like tracking pressed state) 1044 event.setLocation(-(mSlop * 2), -(mSlop * 2)); 1045 } 1046 handled = touchedView.dispatchTouchEvent(event); 1047 } 1048 return handled; 1049 } 1050 hitThirdIcon(MotionEvent event)1051 private boolean hitThirdIcon(MotionEvent event) { 1052 if (mEntry.isLayoutRtl()) { 1053 return mThirdIcon.getVisibility() == View.VISIBLE && 1054 event.getX() < mThirdIcon.getRight(); 1055 } else { 1056 return mThirdIcon.getVisibility() == View.VISIBLE && 1057 event.getX() > mThirdIcon.getLeft(); 1058 } 1059 } 1060 1061 /** 1062 * Should be used after checking if third icon was hit 1063 */ hitAlternateIcon(MotionEvent event)1064 private boolean hitAlternateIcon(MotionEvent event) { 1065 // LayoutParams used to add the start margin to the touch area 1066 final RelativeLayout.LayoutParams alternateIconParams = 1067 (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams(); 1068 if (mEntry.isLayoutRtl()) { 1069 return mAlternateIcon.getVisibility() == View.VISIBLE && 1070 event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin; 1071 } else { 1072 return mAlternateIcon.getVisibility() == View.VISIBLE && 1073 event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin; 1074 } 1075 } 1076 } 1077 } 1078