ExpandingEntryCardView.java revision 891bd2bad8de331a089466777cd054674261a969
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.content.Context; 19import android.content.Intent; 20import android.content.res.Resources; 21import android.graphics.ColorFilter; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.text.TextUtils; 25import android.transition.ChangeBounds; 26import android.transition.ChangeScroll; 27import android.transition.Fade; 28import android.transition.Transition; 29import android.transition.Transition.TransitionListener; 30import android.transition.TransitionManager; 31import android.transition.TransitionSet; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.LayoutInflater; 35import android.view.TouchDelegate; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.ImageView; 39import android.widget.LinearLayout; 40import android.widget.TextView; 41 42import com.android.contacts.R; 43 44import java.util.ArrayList; 45import java.util.List; 46 47/** 48 * Display entries in a LinearLayout that can be expanded to show all entries. 49 */ 50public class ExpandingEntryCardView extends LinearLayout { 51 52 private static final String TAG = "ExpandingEntryCardView"; 53 private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200; 54 private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100; 55 56 public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300; 57 public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300; 58 59 /** 60 * Entry data. 61 */ 62 public static final class Entry { 63 64 private final int mViewId; 65 private final Drawable mIcon; 66 private final String mHeader; 67 private final String mSubHeader; 68 private final Drawable mSubHeaderIcon; 69 private final String mText; 70 private final Drawable mTextIcon; 71 private final Intent mIntent; 72 private final Drawable mAlternateIcon; 73 private final Intent mAlternateIntent; 74 private final String mAlternateContentDescription; 75 private final boolean mShouldApplyColor; 76 private final boolean mIsEditable; 77 78 public Entry(int viewId, Drawable icon, String header, String subHeader, String text, 79 Intent intent, Drawable alternateIcon, Intent alternateIntent, 80 String alternateContentDescription, boolean shouldApplyColor, 81 boolean isEditable) { 82 this(viewId, icon, header, subHeader, null, text, null, intent, alternateIcon, 83 alternateIntent, alternateContentDescription, shouldApplyColor, isEditable); 84 } 85 86 public Entry(int viewId, Drawable mainIcon, String header, String subHeader, 87 Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent, 88 Drawable alternateIcon, Intent alternateIntent, String alternateContentDescription, 89 boolean shouldApplyColor, boolean isEditable) { 90 mViewId = viewId; 91 mIcon = mainIcon; 92 mHeader = header; 93 mSubHeader = subHeader; 94 mSubHeaderIcon = subHeaderIcon; 95 mText = text; 96 mTextIcon = textIcon; 97 mIntent = intent; 98 mAlternateIcon = alternateIcon; 99 mAlternateIntent = alternateIntent; 100 mAlternateContentDescription = alternateContentDescription; 101 mShouldApplyColor = shouldApplyColor; 102 mIsEditable = isEditable; 103 } 104 105 Drawable getIcon() { 106 return mIcon; 107 } 108 109 String getHeader() { 110 return mHeader; 111 } 112 113 String getSubHeader() { 114 return mSubHeader; 115 } 116 117 Drawable getSubHeaderIcon() { 118 return mSubHeaderIcon; 119 } 120 121 public String getText() { 122 return mText; 123 } 124 125 Drawable getTextIcon() { 126 return mTextIcon; 127 } 128 129 Intent getIntent() { 130 return mIntent; 131 } 132 133 Drawable getAlternateIcon() { 134 return mAlternateIcon; 135 } 136 137 Intent getAlternateIntent() { 138 return mAlternateIntent; 139 } 140 141 String getAlternateContentDescription() { 142 return mAlternateContentDescription; 143 } 144 145 boolean shouldApplyColor() { 146 return mShouldApplyColor; 147 } 148 149 boolean isEditable() { 150 return mIsEditable; 151 } 152 153 int getViewId() { 154 return mViewId; 155 } 156 } 157 158 public interface ExpandingEntryCardViewListener { 159 void onCollapse(int heightDelta); 160 void onExpand(int heightDelta); 161 } 162 163 private View mExpandCollapseButton; 164 private TextView mExpandCollapseTextView; 165 private TextView mTitleTextView; 166 private CharSequence mExpandButtonText; 167 private CharSequence mCollapseButtonText; 168 private OnClickListener mOnClickListener; 169 private boolean mIsExpanded = false; 170 private int mCollapsedEntriesCount; 171 private ExpandingEntryCardViewListener mListener; 172 private List<List<Entry>> mEntries; 173 private int mNumEntries = 0; 174 private boolean mAllEntriesInflated = false; 175 private List<List<View>> mEntryViews; 176 private LinearLayout mEntriesViewGroup; 177 private final Drawable mCollapseArrowDrawable; 178 private final Drawable mExpandArrowDrawable; 179 private int mThemeColor; 180 private ColorFilter mThemeColorFilter; 181 private boolean mIsAlwaysExpanded; 182 /** The ViewGroup to run the expand/collapse animation on */ 183 private ViewGroup mAnimationViewGroup; 184 185 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() { 186 @Override 187 public void onClick(View v) { 188 if (mIsExpanded) { 189 collapse(); 190 } else { 191 expand(); 192 } 193 } 194 }; 195 196 public ExpandingEntryCardView(Context context) { 197 this(context, null); 198 } 199 200 public ExpandingEntryCardView(Context context, AttributeSet attrs) { 201 super(context, attrs); 202 LayoutInflater inflater = LayoutInflater.from(context); 203 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); 204 mEntriesViewGroup = (LinearLayout) 205 expandingEntryCardView.findViewById(R.id.content_area_linear_layout); 206 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); 207 mCollapseArrowDrawable = 208 getResources().getDrawable(R.drawable.expanding_entry_card_collapse_white_24); 209 mExpandArrowDrawable = 210 getResources().getDrawable(R.drawable.expanding_entry_card_expand_white_24); 211 212 mExpandCollapseButton = inflater.inflate( 213 R.layout.quickcontact_expanding_entry_card_button, this, false); 214 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text); 215 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener); 216 217 218 } 219 220 /** 221 * Sets the Entry list to display. 222 * 223 * @param entries The Entry list to display. 224 */ 225 public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries, 226 boolean isExpanded, boolean isAlwaysExpanded, 227 ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) { 228 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 229 mIsExpanded = isExpanded; 230 mIsAlwaysExpanded = isAlwaysExpanded; 231 // If isAlwaysExpanded is true, mIsExpanded should be true 232 mIsExpanded |= mIsAlwaysExpanded; 233 mEntryViews = new ArrayList<List<View>>(entries.size()); 234 mEntries = entries; 235 mNumEntries = 0; 236 mAllEntriesInflated = false; 237 for (List<Entry> entryList : mEntries) { 238 mNumEntries += entryList.size(); 239 mEntryViews.add(new ArrayList<View>()); 240 } 241 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries); 242 // Only show the head of each entry list if the initial visible number falls between the 243 // number of lists and the total number of entries 244 if (mCollapsedEntriesCount > mEntries.size()) { 245 mCollapsedEntriesCount = mEntries.size(); 246 } 247 mListener = listener; 248 mAnimationViewGroup = animationViewGroup; 249 250 if (mIsExpanded) { 251 updateExpandCollapseButton(getCollapseButtonText()); 252 inflateAllEntries(layoutInflater); 253 } else { 254 updateExpandCollapseButton(getExpandButtonText()); 255 inflateInitialEntries(layoutInflater); 256 } 257 insertEntriesIntoViewGroup(); 258 applyColor(); 259 } 260 261 /** 262 * Sets the text for the expand button. 263 * 264 * @param expandButtonText The expand button text. 265 */ 266 public void setExpandButtonText(CharSequence expandButtonText) { 267 mExpandButtonText = expandButtonText; 268 if (mExpandCollapseTextView != null && !mIsExpanded) { 269 mExpandCollapseTextView.setText(expandButtonText); 270 } 271 } 272 273 /** 274 * Sets the text for the expand button. 275 * 276 * @param expandButtonText The expand button text. 277 */ 278 public void setCollapseButtonText(CharSequence expandButtonText) { 279 mCollapseButtonText = expandButtonText; 280 if (mExpandCollapseTextView != null && mIsExpanded) { 281 mExpandCollapseTextView.setText(mCollapseButtonText); 282 } 283 } 284 285 @Override 286 public void setOnClickListener(OnClickListener listener) { 287 mOnClickListener = listener; 288 } 289 290 private void insertEntriesIntoViewGroup() { 291 mEntriesViewGroup.removeAllViews(); 292 293 if (mIsExpanded) { 294 for (List<View> viewList : mEntryViews) { 295 for (View view : viewList) { 296 addEntry(view); 297 } 298 } 299 } else { 300 for (int i = 0; i < mCollapsedEntriesCount; i++) { 301 addEntry(mEntryViews.get(i).get(0)); 302 } 303 } 304 305 removeView(mExpandCollapseButton); 306 if (mCollapsedEntriesCount < mNumEntries 307 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) { 308 addView(mExpandCollapseButton, -1); 309 } 310 } 311 312 private void addEntry(View entry) { 313 if (mEntriesViewGroup.getChildCount() > 0) { 314 mEntriesViewGroup.addView(createSeparator(entry)); 315 } 316 mEntriesViewGroup.addView(entry); 317 } 318 319 private View createSeparator(View entry) { 320 View separator = new View(getContext()); 321 separator.setBackgroundColor(getResources().getColor( 322 R.color.expanding_entry_card_item_separator_color)); 323 LayoutParams layoutParams = generateDefaultLayoutParams(); 324 Resources resources = getResources(); 325 layoutParams.height = resources.getDimensionPixelSize( 326 R.dimen.expanding_entry_card_item_separator_height); 327 // The separator is aligned with the text in the entry. This is offset by a default 328 // margin. If there is an icon present, the icon's width and margin are added 329 int marginStart = resources.getDimensionPixelSize( 330 R.dimen.expanding_entry_card_item_padding_start); 331 ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon); 332 if (entryIcon.getDrawable() != null) { 333 int imageWidthAndMargin = 334 resources.getDimensionPixelSize( 335 R.dimen.expanding_entry_card_item_icon_width) + 336 resources.getDimensionPixelSize( 337 R.dimen.expanding_entry_card_item_image_spacing); 338 marginStart += imageWidthAndMargin; 339 } 340 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 341 layoutParams.rightMargin = marginStart; 342 } else { 343 layoutParams.leftMargin = marginStart; 344 } 345 separator.setLayoutParams(layoutParams); 346 return separator; 347 } 348 349 private CharSequence getExpandButtonText() { 350 if (!TextUtils.isEmpty(mExpandButtonText)) { 351 return mExpandButtonText; 352 } else { 353 // Default to "See more". 354 return getResources().getText(R.string.expanding_entry_card_view_see_more); 355 } 356 } 357 358 private CharSequence getCollapseButtonText() { 359 if (!TextUtils.isEmpty(mCollapseButtonText)) { 360 return mCollapseButtonText; 361 } else { 362 // Default to "See less". 363 return getResources().getText(R.string.expanding_entry_card_view_see_less); 364 } 365 } 366 367 /** 368 * Inflates the initial entries to be shown. 369 */ 370 private void inflateInitialEntries(LayoutInflater layoutInflater) { 371 // If the number of collapsed entries equals total entries, inflate all 372 if (mCollapsedEntriesCount == mNumEntries) { 373 inflateAllEntries(layoutInflater); 374 } else { 375 // Otherwise inflate the top entry from each list 376 for (int i = 0; i < mCollapsedEntriesCount; i++) { 377 mEntryViews.get(i).add(createEntryView(layoutInflater, mEntries.get(i).get(0))); 378 } 379 } 380 } 381 382 /** 383 * Inflates all entries. 384 */ 385 private void inflateAllEntries(LayoutInflater layoutInflater) { 386 if (mAllEntriesInflated) { 387 return; 388 } 389 for (int i = 0; i < mEntries.size(); i++) { 390 List<Entry> entryList = mEntries.get(i); 391 List<View> viewList = mEntryViews.get(i); 392 for (int j = viewList.size(); j < entryList.size(); j++) { 393 viewList.add(createEntryView(layoutInflater, entryList.get(j))); 394 } 395 } 396 mAllEntriesInflated = true; 397 } 398 399 public void setColorAndFilter(int color, ColorFilter colorFilter) { 400 mThemeColor = color; 401 mThemeColorFilter = colorFilter; 402 applyColor(); 403 } 404 405 public void setEntryHeaderColor(int color) { 406 if (mEntries != null) { 407 for (List<View> entryList : mEntryViews) { 408 for (View entryView : entryList) { 409 TextView header = (TextView) entryView.findViewById(R.id.header); 410 if (header != null) { 411 header.setTextColor(color); 412 } 413 } 414 } 415 } 416 } 417 418 /** 419 * The ColorFilter is passed in along with the color so that a new one only needs to be created 420 * once for the entire activity. 421 * 1. Title 422 * 2. Entry icons 423 * 3. Expand/Collapse Text 424 * 4. Expand/Collapse Button 425 */ 426 public void applyColor() { 427 if (mThemeColor != 0 && mThemeColorFilter != null) { 428 // Title 429 if (mTitleTextView != null) { 430 mTitleTextView.setTextColor(mThemeColor); 431 } 432 433 // Entry icons 434 if (mEntries != null) { 435 for (List<Entry> entryList : mEntries) { 436 for (Entry entry : entryList) { 437 if (entry.shouldApplyColor()) { 438 Drawable icon = entry.getIcon(); 439 if (icon != null) { 440 icon.setColorFilter(mThemeColorFilter); 441 } 442 } 443 Drawable alternateIcon = entry.getAlternateIcon(); 444 if (alternateIcon != null) { 445 alternateIcon.setColorFilter(mThemeColorFilter); 446 } 447 } 448 } 449 } 450 451 // Expand/Collapse 452 mExpandCollapseTextView.setTextColor(mThemeColor); 453 mCollapseArrowDrawable.setColorFilter(mThemeColorFilter); 454 mExpandArrowDrawable.setColorFilter(mThemeColorFilter); 455 } 456 } 457 458 private View createEntryView(LayoutInflater layoutInflater, Entry entry) { 459 final View view = layoutInflater.inflate( 460 R.layout.expanding_entry_card_item, this, false); 461 462 view.setId(entry.getViewId()); 463 464 final ImageView icon = (ImageView) view.findViewById(R.id.icon); 465 if (entry.getIcon() != null) { 466 icon.setImageDrawable(entry.getIcon()); 467 } else { 468 icon.setVisibility(View.GONE); 469 } 470 471 final TextView header = (TextView) view.findViewById(R.id.header); 472 if (!TextUtils.isEmpty(entry.getHeader())) { 473 header.setText(entry.getHeader()); 474 } else { 475 header.setVisibility(View.GONE); 476 } 477 478 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header); 479 if (!TextUtils.isEmpty(entry.getSubHeader())) { 480 subHeader.setText(entry.getSubHeader()); 481 } else { 482 subHeader.setVisibility(View.GONE); 483 } 484 485 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); 486 if (entry.getSubHeaderIcon() != null) { 487 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); 488 } else { 489 subHeaderIcon.setVisibility(View.GONE); 490 } 491 492 final TextView text = (TextView) view.findViewById(R.id.text); 493 if (!TextUtils.isEmpty(entry.getText())) { 494 text.setText(entry.getText()); 495 } else { 496 text.setVisibility(View.GONE); 497 } 498 499 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); 500 if (entry.getTextIcon() != null) { 501 textIcon.setImageDrawable(entry.getTextIcon()); 502 } else { 503 textIcon.setVisibility(View.GONE); 504 } 505 506 if (entry.getIntent() != null) { 507 view.setOnClickListener(mOnClickListener); 508 view.setTag(entry.getIntent()); 509 } 510 511 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); 512 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { 513 alternateIcon.setImageDrawable(entry.getAlternateIcon()); 514 alternateIcon.setOnClickListener(mOnClickListener); 515 alternateIcon.setTag(entry.getAlternateIntent()); 516 alternateIcon.setId(entry.getViewId()); 517 alternateIcon.setVisibility(View.VISIBLE); 518 alternateIcon.setContentDescription(entry.getAlternateContentDescription()); 519 520 // Expand the clickable area for alternate icon to be top to bottom and to end edge 521 // of the entry view 522 view.post(new Runnable() { 523 @Override 524 public void run() { 525 final Rect alternateIconRect = new Rect(); 526 alternateIcon.getHitRect(alternateIconRect); 527 528 alternateIconRect.bottom = view.getHeight(); 529 alternateIconRect.top = 0; 530 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 531 alternateIconRect.left = 0; 532 } else { 533 alternateIconRect.right = view.getWidth(); 534 } 535 final TouchDelegate touchDelegate = 536 new TouchDelegate(alternateIconRect, alternateIcon); 537 view.setTouchDelegate(touchDelegate); 538 } 539 }); 540 } 541 542 return view; 543 } 544 545 private void updateExpandCollapseButton(CharSequence buttonText) { 546 final Drawable arrow = mIsExpanded ? mCollapseArrowDrawable : mExpandArrowDrawable; 547 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 548 mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, arrow, 549 null); 550 } else { 551 mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(arrow, null, null, 552 null); 553 } 554 mExpandCollapseTextView.setText(buttonText); 555 } 556 557 private void expand() { 558 ChangeBounds boundsTransition = new ChangeBounds(); 559 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); 560 561 Fade fadeIn = new Fade(Fade.IN); 562 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); 563 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); 564 565 TransitionSet transitionSet = new TransitionSet(); 566 transitionSet.addTransition(boundsTransition); 567 transitionSet.addTransition(fadeIn); 568 569 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 570 this : mAnimationViewGroup; 571 572 transitionSet.addListener(new TransitionListener() { 573 @Override 574 public void onTransitionStart(Transition transition) { 575 // The listener is used to turn off suppressing, the proper delta is not necessary 576 mListener.onExpand(0); 577 } 578 579 @Override 580 public void onTransitionEnd(Transition transition) { 581 } 582 583 @Override 584 public void onTransitionCancel(Transition transition) { 585 } 586 587 @Override 588 public void onTransitionPause(Transition transition) { 589 } 590 591 @Override 592 public void onTransitionResume(Transition transition) { 593 } 594 }); 595 596 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 597 598 mIsExpanded = true; 599 // In order to insert new entries, we may need to inflate them for the first time 600 inflateAllEntries(LayoutInflater.from(getContext())); 601 insertEntriesIntoViewGroup(); 602 updateExpandCollapseButton(getCollapseButtonText()); 603 } 604 605 private void collapse() { 606 final int startingHeight = mEntriesViewGroup.getMeasuredHeight(); 607 mIsExpanded = false; 608 updateExpandCollapseButton(getExpandButtonText()); 609 610 final ChangeBounds boundsTransition = new ChangeBounds(); 611 boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 612 613 final ChangeScroll scrollTransition = new ChangeScroll(); 614 scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); 615 616 TransitionSet transitionSet = new TransitionSet(); 617 transitionSet.addTransition(boundsTransition); 618 transitionSet.addTransition(scrollTransition); 619 620 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? 621 this : mAnimationViewGroup; 622 623 boundsTransition.addListener(new TransitionListener() { 624 @Override 625 public void onTransitionStart(Transition transition) { 626 /* 627 * onTransitionStart is called after the view hierarchy has been changed but before 628 * the animation begins. 629 */ 630 int finishingHeight = mEntriesViewGroup.getMeasuredHeight(); 631 mListener.onCollapse(startingHeight - finishingHeight); 632 } 633 634 @Override 635 public void onTransitionEnd(Transition transition) { 636 } 637 638 @Override 639 public void onTransitionCancel(Transition transition) { 640 } 641 642 @Override 643 public void onTransitionPause(Transition transition) { 644 } 645 646 @Override 647 public void onTransitionResume(Transition transition) { 648 } 649 }); 650 651 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); 652 653 insertEntriesIntoViewGroup(); 654 } 655 656 /** 657 * Returns whether the view is currently in its expanded state. 658 */ 659 public boolean isExpanded() { 660 return mIsExpanded; 661 } 662 663 /** 664 * Sets the title text of this ExpandingEntryCardView. 665 * @param title The title to set. A null title will result in the title being removed. 666 */ 667 public void setTitle(String title) { 668 if (mTitleTextView == null) { 669 Log.e(TAG, "mTitleTextView is null"); 670 } 671 if (title == null) { 672 mTitleTextView.setVisibility(View.GONE); 673 findViewById(R.id.title_separator).setVisibility(View.GONE); 674 } 675 mTitleTextView.setText(title); 676 mTitleTextView.setVisibility(View.VISIBLE); 677 findViewById(R.id.title_separator).setVisibility(View.VISIBLE); 678 } 679 680 public boolean shouldShow() { 681 return mEntries != null && mEntries.size() > 0; 682 } 683} 684