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