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