ExpandingEntryCardView.java revision 71032f3fb7038995297666602773ae023c1351c4
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.drawable.Drawable; 27import android.net.Uri; 28import android.provider.ContactsContract; 29import android.provider.ContactsContract.QuickContact; 30import android.support.v4.text.TextUtilsCompat; 31import android.support.v4.view.ViewCompat; 32import android.text.TextUtils; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.View.OnClickListener; 38import android.view.ViewGroup; 39import android.view.ViewTreeObserver; 40import android.view.ViewTreeObserver.OnPreDrawListener; 41import android.view.animation.AccelerateDecelerateInterpolator; 42import android.widget.FrameLayout; 43import android.widget.ImageView; 44import android.widget.LinearLayout; 45import android.widget.TextView; 46 47import java.util.ArrayList; 48import java.util.List; 49import java.util.Locale; 50 51/** 52 * Display entries in a LinearLayout that can be expanded to show all entries. 53 */ 54public class ExpandingEntryCardView extends LinearLayout { 55 56 private static final String TAG = "ExpandingEntryCardView"; 57 58 /** 59 * Entry data. 60 */ 61 public static final class Entry { 62 63 private final Drawable mIcon; 64 private final String mHeader; 65 private final String mSubHeader; 66 private final Drawable mSubHeaderIcon; 67 private final String mText; 68 private final Drawable mTextIcon; 69 private final Intent mIntent; 70 private final boolean mIsEditable; 71 72 public Entry(Drawable icon, String header, String subHeader, String text, 73 Intent intent, boolean isEditable) { 74 this(icon, header, subHeader, null, text, null, intent, isEditable); 75 } 76 77 public Entry(Drawable mainIcon, String header, String subHeader, 78 Drawable subHeaderIcon, String text, Drawable textIcon, Intent intent, 79 boolean isEditable) { 80 mIcon = mainIcon; 81 mHeader = header; 82 mSubHeader = subHeader; 83 mSubHeaderIcon = subHeaderIcon; 84 mText = text; 85 mTextIcon = textIcon; 86 mIntent = intent; 87 mIsEditable = isEditable; 88 } 89 90 Drawable getIcon() { 91 return mIcon; 92 } 93 94 String getHeader() { 95 return mHeader; 96 } 97 98 String getSubHeader() { 99 return mSubHeader; 100 } 101 102 Drawable getSubHeaderIcon() { 103 return mSubHeaderIcon; 104 } 105 106 public String getText() { 107 return mText; 108 } 109 110 Drawable getTextIcon() { 111 return mTextIcon; 112 } 113 114 Intent getIntent() { 115 return mIntent; 116 } 117 118 boolean isEditable() { 119 return mIsEditable; 120 } 121 } 122 123 private View mExpandCollapseButton; 124 private TextView mExpandCollapseTextView; 125 private TextView mTitleTextView; 126 private CharSequence mExpandButtonText; 127 private CharSequence mCollapseButtonText; 128 private OnClickListener mOnClickListener; 129 private boolean mIsExpanded = false; 130 private int mCollapsedEntriesCount; 131 private List<View> mEntryViews; 132 private LinearLayout mEntriesViewGroup; 133 private int mThemeColor; 134 135 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() { 136 @Override 137 public void onClick(View v) { 138 if (mIsExpanded) { 139 collapse(); 140 } else { 141 expand(); 142 } 143 } 144 }; 145 146 public ExpandingEntryCardView(Context context) { 147 super(context); 148 LayoutInflater inflater = LayoutInflater.from(context); 149 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); 150 mEntriesViewGroup = (LinearLayout) 151 expandingEntryCardView.findViewById(R.id.content_area_linear_layout); 152 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); 153 } 154 155 public ExpandingEntryCardView(Context context, AttributeSet attrs) { 156 super(context, attrs); 157 LayoutInflater inflater = LayoutInflater.from(context); 158 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); 159 mEntriesViewGroup = (LinearLayout) 160 expandingEntryCardView.findViewById(R.id.content_area_linear_layout); 161 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); 162 } 163 164 /** 165 * Sets the Entry list to display. 166 * 167 * @param entries The Entry list to display. 168 */ 169 public void initialize(List<Entry> entries, int numInitialVisibleEntries, 170 boolean isExpanded, int themeColor) { 171 LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 172 mIsExpanded = isExpanded; 173 mEntryViews = createEntryViews(layoutInflater, entries); 174 mThemeColor = themeColor; 175 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, entries.size()); 176 if (mExpandCollapseButton == null) { 177 createExpandButton(layoutInflater); 178 } 179 insertEntriesIntoViewGroup(); 180 } 181 182 /** 183 * Sets the text for the expand button. 184 * 185 * @param expandButtonText The expand button text. 186 */ 187 public void setExpandButtonText(CharSequence expandButtonText) { 188 mExpandButtonText = expandButtonText; 189 if (mExpandCollapseTextView != null && !mIsExpanded) { 190 mExpandCollapseTextView.setText(expandButtonText); 191 } 192 } 193 194 /** 195 * Sets the text for the expand button. 196 * 197 * @param expandButtonText The expand button text. 198 */ 199 public void setCollapseButtonText(CharSequence expandButtonText) { 200 mCollapseButtonText = expandButtonText; 201 if (mExpandCollapseTextView != null && mIsExpanded) { 202 mExpandCollapseTextView.setText(mCollapseButtonText); 203 } 204 } 205 206 @Override 207 public void setOnClickListener(OnClickListener listener) { 208 mOnClickListener = listener; 209 } 210 211 private void insertEntriesIntoViewGroup() { 212 mEntriesViewGroup.removeAllViews(); 213 for (int i = 0; i < mCollapsedEntriesCount; ++i) { 214 addEntry(mEntryViews.get(i)); 215 } 216 if (mIsExpanded) { 217 for (int i = mCollapsedEntriesCount; i < mEntryViews.size(); ++i) { 218 addEntry(mEntryViews.get(i)); 219 } 220 } 221 222 removeView(mExpandCollapseButton); 223 if (mCollapsedEntriesCount < mEntryViews.size() 224 && mExpandCollapseButton.getParent() == null) { 225 addView(mExpandCollapseButton, -1); 226 } 227 } 228 229 private void addEntry(View entry) { 230 if (mEntriesViewGroup.getChildCount() > 0) { 231 View separator = new View(getContext()); 232 separator.setBackgroundColor(getResources().getColor( 233 R.color.expanding_entry_card_item_separator_color)); 234 LayoutParams layoutParams = generateDefaultLayoutParams(); 235 Resources resources = getResources(); 236 layoutParams.height = resources.getDimensionPixelSize( 237 R.dimen.expanding_entry_card_item_separator_height); 238 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 239 layoutParams.rightMargin = resources.getDimensionPixelSize( 240 R.dimen.expanding_entry_card_item_padding_start); 241 layoutParams.leftMargin = resources.getDimensionPixelSize( 242 R.dimen.expanding_entry_card_item_padding_end); 243 } else { 244 layoutParams.leftMargin = resources.getDimensionPixelSize( 245 R.dimen.expanding_entry_card_item_padding_start); 246 layoutParams.rightMargin = resources.getDimensionPixelSize( 247 R.dimen.expanding_entry_card_item_padding_end); 248 } 249 separator.setLayoutParams(layoutParams); 250 mEntriesViewGroup.addView(separator); 251 } 252 mEntriesViewGroup.addView(entry); 253 } 254 255 private CharSequence getExpandButtonText() { 256 if (!TextUtils.isEmpty(mExpandButtonText)) { 257 return mExpandButtonText; 258 } else { 259 // Default to "See more". 260 return getResources().getText(R.string.expanding_entry_card_view_see_more); 261 } 262 } 263 264 private CharSequence getCollapseButtonText() { 265 if (!TextUtils.isEmpty(mCollapseButtonText)) { 266 return mCollapseButtonText; 267 } else { 268 // Default to "See less". 269 return getResources().getText(R.string.expanding_entry_card_view_see_less); 270 } 271 } 272 273 private void createExpandButton(LayoutInflater layoutInflater) { 274 mExpandCollapseButton = layoutInflater.inflate( 275 R.layout.quickcontact_expanding_entry_card_button, this, false); 276 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text); 277 if (mIsExpanded) { 278 updateExpandCollapseButton(getCollapseButtonText()); 279 } else { 280 updateExpandCollapseButton(getExpandButtonText()); 281 } 282 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener); 283 } 284 285 private List<View> createEntryViews(LayoutInflater layoutInflater, List<Entry> entries) { 286 ArrayList<View> views = new ArrayList<View>(entries.size()); 287 for (Entry entry : entries) { 288 views.add(createEntryView(layoutInflater, entry)); 289 } 290 return views; 291 } 292 293 private View createEntryView(LayoutInflater layoutInflater, Entry entry) { 294 View view = layoutInflater.inflate( 295 R.layout.expanding_entry_card_item, this, false); 296 297 ImageView icon = (ImageView) view.findViewById(R.id.icon); 298 icon.setImageDrawable(entry.getIcon()); 299 300 TextView header = (TextView) view.findViewById(R.id.header); 301 if (entry.getHeader() != null) { 302 header.setText(entry.getHeader()); 303 } else { 304 header.setVisibility(View.GONE); 305 } 306 307 TextView subHeader = (TextView) view.findViewById(R.id.sub_header); 308 if (entry.getSubHeader() != null) { 309 subHeader.setText(entry.getSubHeader()); 310 } else { 311 subHeader.setVisibility(View.GONE); 312 } 313 314 ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); 315 if (entry.getSubHeaderIcon() != null) { 316 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); 317 } else { 318 subHeaderIcon.setVisibility(View.GONE); 319 } 320 321 TextView text = (TextView) view.findViewById(R.id.text); 322 if (entry.getText() != null) { 323 text.setText(entry.getText()); 324 } else { 325 text.setVisibility(View.GONE); 326 } 327 328 ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); 329 if (entry.getTextIcon() != null) { 330 textIcon.setImageDrawable(entry.getTextIcon()); 331 } else { 332 textIcon.setVisibility(View.GONE); 333 } 334 335 if (entry.getIntent() != null) { 336 View entryLayout = view.findViewById(R.id.entry_layout); 337 entryLayout.setOnClickListener(mOnClickListener); 338 entryLayout.setTag(entry.getIntent()); 339 } 340 341 return view; 342 } 343 344 private void updateExpandCollapseButton(CharSequence buttonText) { 345 int resId = mIsExpanded ? R.drawable.expanding_entry_card_collapse_white_24 346 : R.drawable.expanding_entry_card_expand_white_24; 347 // TODO: apply color theme to the drawable 348 Drawable drawable = getResources().getDrawable(resId); 349 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 350 mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, 351 null); 352 } else { 353 mExpandCollapseTextView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, 354 null); 355 } 356 mExpandCollapseTextView.setText(buttonText); 357 } 358 359 private void expand() { 360 final int startingHeight = mEntriesViewGroup.getHeight(); 361 362 mIsExpanded = true; 363 insertEntriesIntoViewGroup(); 364 updateExpandCollapseButton(getCollapseButtonText()); 365 366 // When expanding, all the TextViews haven't been laid out yet. Therefore, 367 // calling measure() would return an incorrect result. Therefore, we need a pre draw 368 // listener. 369 final ViewTreeObserver observer = mEntriesViewGroup.getViewTreeObserver(); 370 observer.addOnPreDrawListener(new OnPreDrawListener() { 371 @Override 372 public boolean onPreDraw() { 373 if (observer.isAlive()) { 374 mEntriesViewGroup.getViewTreeObserver().removeOnPreDrawListener(this); 375 } 376 createExpandAnimator(startingHeight, mEntriesViewGroup.getHeight()).start(); 377 // Do not draw the final frame of the animation immediately. 378 return false; 379 } 380 }); 381 } 382 383 private void collapse() { 384 int startingHeight = mEntriesViewGroup.getHeight(); 385 int finishHeight = measureCollapsedViewGroupHeight(); 386 387 mIsExpanded = false; 388 updateExpandCollapseButton(getExpandButtonText()); 389 createExpandAnimator(startingHeight, finishHeight).start(); 390 } 391 392 private int measureCollapsedViewGroupHeight() { 393 if (mCollapsedEntriesCount == 0) { 394 return 0; 395 } 396 final View bottomCollapsedView = mEntryViews.get(mCollapsedEntriesCount - 1); 397 return bottomCollapsedView.getTop() + bottomCollapsedView.getHeight(); 398 } 399 400 /** 401 * Create ValueAnimator that performs an expand animation on the content LinearLayout. 402 * 403 * The animation needs to be performed manually using a ValueAnimator, since LinearLayout 404 * doesn't have a single set-able height property (ie, no setHeight()). 405 */ 406 private ValueAnimator createExpandAnimator(int start, int end) { 407 ValueAnimator animator = ValueAnimator.ofInt(start, end); 408 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 409 @Override 410 public void onAnimationUpdate(ValueAnimator valueAnimator) { 411 int value = (Integer) valueAnimator.getAnimatedValue(); 412 ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams(); 413 layoutParams.height = value; 414 mEntriesViewGroup.setLayoutParams(layoutParams); 415 } 416 }); 417 animator.addListener(new AnimatorListenerAdapter() { 418 @Override 419 public void onAnimationEnd(Animator animation) { 420 insertEntriesIntoViewGroup(); 421 // Now that the animation is done, stop using a fixed height. 422 ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams(); 423 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 424 mEntriesViewGroup.setLayoutParams(layoutParams); 425 } 426 }); 427 return animator; 428 } 429 430 /** 431 * Returns whether the view is currently in its expanded state. 432 */ 433 public boolean isExpanded() { 434 return mIsExpanded; 435 } 436 437 /** 438 * Sets the title text of this ExpandingEntryCardView. 439 * @param title The title to set. A null title will result in an empty string being set. 440 */ 441 public void setTitle(String title) { 442 if (mTitleTextView == null) { 443 Log.e(TAG, "mTitleTextView is null"); 444 } 445 if (title == null) { 446 mTitleTextView.setText(""); 447 } 448 mTitleTextView.setText(title); 449 } 450} 451