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