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