ContactListItemView.java revision faa49c913fedb67161dc784b91faa9a27195b0a8
1/* 2 * Copyright (C) 2010 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 */ 16 17package com.android.contacts.list; 18 19import com.android.contacts.ContactPresenceIconUtil; 20import com.android.contacts.ContactStatusUtil; 21import com.android.contacts.R; 22import com.android.contacts.format.DisplayNameFormatter; 23import com.android.contacts.format.PrefixHighlighter; 24 25import android.content.Context; 26import android.content.res.ColorStateList; 27import android.content.res.TypedArray; 28import android.database.CharArrayBuffer; 29import android.database.Cursor; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.Rect; 33import android.graphics.Typeface; 34import android.graphics.drawable.Drawable; 35import android.os.Bundle; 36import android.provider.ContactsContract; 37import android.provider.ContactsContract.Contacts; 38import android.text.TextUtils; 39import android.text.TextUtils.TruncateAt; 40import android.util.AttributeSet; 41import android.util.TypedValue; 42import android.view.Gravity; 43import android.view.View; 44import android.view.ViewGroup; 45import android.widget.AbsListView.SelectionBoundsAdjuster; 46import android.widget.ImageView; 47import android.widget.ImageView.ScaleType; 48import android.widget.QuickContactBadge; 49import android.widget.TextView; 50 51import java.util.ArrayList; 52import java.util.List; 53import java.util.regex.Matcher; 54import java.util.regex.Pattern; 55 56/** 57 * A custom view for an item in the contact list. 58 * The view contains the contact's photo, a set of text views (for name, status, etc...) and 59 * icons for presence and call. 60 * The view uses no XML file for layout and all the measurements and layouts are done 61 * in the onMeasure and onLayout methods. 62 * 63 * The layout puts the contact's photo on the right side of the view, the call icon (if present) 64 * to the left of the photo, the text lines are aligned to the left and the presence icon (if 65 * present) is set to the left of the status line. 66 * 67 * The layout also supports a header (used as a header of a group of contacts) that is above the 68 * contact's data and a divider between contact view. 69 */ 70 71public class ContactListItemView extends ViewGroup 72 implements SelectionBoundsAdjuster { 73 74 private static final int QUICK_CONTACT_BADGE_STYLE = 75 com.android.internal.R.attr.quickContactBadgeStyleWindowMedium; 76 77 protected final Context mContext; 78 79 // Style values for layout and appearance 80 private final int mPreferredHeight; 81 private final int mVerticalDividerMargin; 82 private final int mPaddingTop; 83 private final int mPaddingRight; 84 private final int mPaddingBottom; 85 private final int mPaddingLeft; 86 private final int mGapBetweenImageAndText; 87 private final int mGapBetweenLabelAndData; 88 private final int mCallButtonPadding; 89 private final int mPresenceIconMargin; 90 private final int mPresenceIconSize; 91 private final int mHeaderTextColor; 92 private final int mHeaderTextIndent; 93 private final int mHeaderTextSize; 94 private final int mHeaderUnderlineHeight; 95 private final int mHeaderUnderlineColor; 96 private final int mCountViewTextSize; 97 private final int mContactsCountTextColor; 98 private final int mTextIndent; 99 private Drawable mActivatedBackgroundDrawable; 100 101 // Horizontal divider between contact views. 102 private boolean mHorizontalDividerVisible = true; 103 private Drawable mHorizontalDividerDrawable; 104 private int mHorizontalDividerHeight; 105 106 // Vertical divider between the call icon and the text. 107 private boolean mVerticalDividerVisible; 108 private Drawable mVerticalDividerDrawable; 109 private int mVerticalDividerWidth; 110 111 // Header layout data 112 private boolean mHeaderVisible; 113 private View mHeaderDivider; 114 private int mHeaderBackgroundHeight; 115 private TextView mHeaderTextView; 116 117 // The views inside the contact view 118 private boolean mQuickContactEnabled = true; 119 private QuickContactBadge mQuickContact; 120 private ImageView mPhotoView; 121 private TextView mNameTextView; 122 private TextView mPhoneticNameTextView; 123 private DontPressWithParentImageView mCallButton; 124 private TextView mLabelView; 125 private TextView mDataView; 126 private TextView mSnippetView; 127 private TextView mStatusView; 128 private TextView mCountView; 129 private ImageView mPresenceIcon; 130 131 private ColorStateList mSecondaryTextColor; 132 133 private char[] mHighlightedPrefix; 134 135 private int mDefaultPhotoViewSize; 136 /** 137 * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding 138 * to align other data in this View. 139 */ 140 private int mPhotoViewWidth; 141 /** 142 * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. 143 */ 144 private int mPhotoViewHeight; 145 146 /** 147 * Only effective when {@link #mPhotoView} is null. 148 * When true all the Views on the right side of the photo should have horizontal padding on 149 * those left assuming there is a photo. 150 */ 151 private boolean mKeepHorizontalPaddingForPhotoView; 152 /** 153 * Only effective when {@link #mPhotoView} is null. 154 */ 155 private boolean mKeepVerticalPaddingForPhotoView; 156 157 /** 158 * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. 159 * False indicates those values should be updated before being used in position calculation. 160 */ 161 private boolean mPhotoViewWidthAndHeightAreReady = false; 162 163 private int mNameTextViewHeight; 164 private int mPhoneticNameTextViewHeight; 165 private int mLabelTextViewHeight; 166 private int mSnippetTextViewHeight; 167 private int mStatusTextViewHeight; 168 169 private OnClickListener mCallButtonClickListener; 170 private CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); 171 private CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); 172 173 private boolean mActivatedStateSupported; 174 175 private Rect mBoundsWithoutHeader = new Rect(); 176 177 /** A helper used to highlight a prefix in a text field. */ 178 private PrefixHighlighter mPrefixHighligher; 179 /** A helper used to format display names. */ 180 private DisplayNameFormatter mDisplayNameFormatter; 181 182 /** 183 * Special class to allow the parent to be pressed without being pressed itself. 184 * This way the line of a tab can be pressed, but the image itself is not. 185 */ 186 // TODO: understand this 187 private static class DontPressWithParentImageView extends ImageView { 188 189 public DontPressWithParentImageView(Context context, AttributeSet attrs) { 190 super(context, attrs); 191 } 192 193 @Override 194 public void setPressed(boolean pressed) { 195 // If the parent is pressed, do not set to pressed. 196 if (pressed && ((View) getParent()).isPressed()) { 197 return; 198 } 199 super.setPressed(pressed); 200 } 201 } 202 203 public ContactListItemView(Context context, AttributeSet attrs) { 204 super(context, attrs); 205 mContext = context; 206 207 // Read all style values 208 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); 209 mPreferredHeight = a.getDimensionPixelSize( 210 R.styleable.ContactListItemView_list_item_height, 0); 211 mActivatedBackgroundDrawable = a.getDrawable( 212 R.styleable.ContactListItemView_activated_background); 213 mHorizontalDividerDrawable = a.getDrawable( 214 R.styleable.ContactListItemView_list_item_divider); 215 mVerticalDividerMargin = a.getDimensionPixelOffset( 216 R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0); 217 mPaddingTop = a.getDimensionPixelOffset( 218 R.styleable.ContactListItemView_list_item_padding_top, 0); 219 mPaddingBottom = a.getDimensionPixelOffset( 220 R.styleable.ContactListItemView_list_item_padding_bottom, 0); 221 mPaddingLeft = a.getDimensionPixelOffset( 222 R.styleable.ContactListItemView_list_item_padding_left, 0); 223 mPaddingRight = a.getDimensionPixelOffset( 224 R.styleable.ContactListItemView_list_item_padding_right, 0); 225 mGapBetweenImageAndText = a.getDimensionPixelOffset( 226 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0); 227 mGapBetweenLabelAndData = a.getDimensionPixelOffset( 228 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0); 229 mCallButtonPadding = a.getDimensionPixelOffset( 230 R.styleable.ContactListItemView_list_item_call_button_padding, 0); 231 mPresenceIconMargin = a.getDimensionPixelOffset( 232 R.styleable.ContactListItemView_list_item_presence_icon_margin, 4); 233 mPresenceIconSize = a.getDimensionPixelOffset( 234 R.styleable.ContactListItemView_list_item_presence_icon_size, 16); 235 mDefaultPhotoViewSize = a.getDimensionPixelOffset( 236 R.styleable.ContactListItemView_list_item_photo_size, 0); 237 mHeaderTextIndent = a.getDimensionPixelOffset( 238 R.styleable.ContactListItemView_list_item_header_text_indent, 0); 239 mHeaderTextColor = a.getColor( 240 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK); 241 mHeaderTextSize = a.getDimensionPixelSize( 242 R.styleable.ContactListItemView_list_item_header_text_size, 12); 243 mHeaderBackgroundHeight = a.getDimensionPixelSize( 244 R.styleable.ContactListItemView_list_item_header_height, 30); 245 mHeaderUnderlineHeight = a.getDimensionPixelSize( 246 R.styleable.ContactListItemView_list_item_header_underline_height, 1); 247 mHeaderUnderlineColor = a.getColor( 248 R.styleable.ContactListItemView_list_item_header_underline_color, 0); 249 mTextIndent = a.getDimensionPixelOffset( 250 R.styleable.ContactListItemView_list_item_text_indent, 0); 251 mCountViewTextSize = a.getDimensionPixelSize( 252 R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12); 253 mContactsCountTextColor = a.getColor( 254 R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK); 255 256 mPrefixHighligher = new PrefixHighlighter( 257 a.getColor(R.styleable.ContactListItemView_list_item_prefix_highlight_color, 258 Color.GREEN)); 259 a.recycle(); 260 261 a = getContext().obtainStyledAttributes(android.R.styleable.Theme); 262 mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary); 263 a.recycle(); 264 265 mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight(); 266 267 if (mActivatedBackgroundDrawable != null) { 268 mActivatedBackgroundDrawable.setCallback(this); 269 } 270 271 mDisplayNameFormatter = new DisplayNameFormatter(mPrefixHighligher); 272 } 273 274 /** 275 * Installs a call button listener. 276 */ 277 public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) { 278 mCallButtonClickListener = callButtonClickListener; 279 } 280 281 public void setUnknownNameText(CharSequence unknownNameText) { 282 mDisplayNameFormatter.setUnknownNameText(unknownNameText); 283 } 284 285 public void setQuickContactEnabled(boolean flag) { 286 mQuickContactEnabled = flag; 287 } 288 289 @Override 290 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 291 // We will match parent's width and wrap content vertically, but make sure 292 // height is no less than listPreferredItemHeight. 293 int width = resolveSize(0, widthMeasureSpec); 294 int height = 0; 295 int preferredHeight = mPreferredHeight; 296 297 mNameTextViewHeight = 0; 298 mPhoneticNameTextViewHeight = 0; 299 mLabelTextViewHeight = 0; 300 mSnippetTextViewHeight = 0; 301 mStatusTextViewHeight = 0; 302 303 // Go over all visible text views and add their heights to get the total height 304 if (isVisible(mNameTextView)) { 305 mNameTextView.measure(0, 0); 306 mNameTextViewHeight = mNameTextView.getMeasuredHeight(); 307 } 308 309 if (isVisible(mPhoneticNameTextView)) { 310 mPhoneticNameTextView.measure(0, 0); 311 mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight(); 312 } 313 314 if (isVisible(mLabelView)) { 315 mLabelView.measure(0, 0); 316 mLabelTextViewHeight = mLabelView.getMeasuredHeight(); 317 } 318 319 // Label view height is the biggest of the label text view and the data text view 320 if (isVisible(mDataView)) { 321 mDataView.measure(0, 0); 322 mLabelTextViewHeight = Math.max(mLabelTextViewHeight, mDataView.getMeasuredHeight()); 323 } 324 325 if (isVisible(mSnippetView)) { 326 mSnippetView.measure(0, 0); 327 mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); 328 } 329 330 // Status view height is the biggest of the text view and the presence icon 331 if (isVisible(mPresenceIcon)) { 332 mPresenceIcon.measure(mPresenceIconSize, mPresenceIconSize); 333 mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); 334 } 335 336 if (isVisible(mStatusView)) { 337 mStatusView.measure(0, 0); 338 mStatusTextViewHeight = Math.max(mStatusTextViewHeight, 339 mStatusView.getMeasuredHeight()); 340 } 341 342 // Calculate height including padding 343 height += mNameTextViewHeight + mPhoneticNameTextViewHeight + mLabelTextViewHeight + 344 mSnippetTextViewHeight + mStatusTextViewHeight + mPaddingTop + mPaddingBottom; 345 346 if (isVisible(mCallButton)) { 347 mCallButton.measure(0, 0); 348 } 349 350 // Make sure the height is at least as high as the photo 351 ensurePhotoViewSize(); 352 height = Math.max(height, mPhotoViewHeight + mPaddingBottom + mPaddingTop); 353 354 // Add horizontal divider height 355 if (mHorizontalDividerVisible) { 356 height += mHorizontalDividerHeight; 357 preferredHeight += mHorizontalDividerHeight; 358 } 359 360 // Make sure height is at least the preferred height 361 height = Math.max(height, preferredHeight); 362 363 // Add the height of the header if visible 364 if (mHeaderVisible) { 365 mHeaderTextView.measure( 366 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 367 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 368 if (mCountView != null) { 369 mCountView.measure( 370 MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 371 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 372 } 373 mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight, 374 mHeaderTextView.getMeasuredHeight()); 375 height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight); 376 } 377 378 setMeasuredDimension(width, height); 379 } 380 381 @Override 382 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 383 int height = bottom - top; 384 int width = right - left; 385 386 // Determine the vertical bounds by laying out the header first. 387 int topBound = 0; 388 int bottomBound = height; 389 int leftBound = mPaddingLeft; 390 391 // Put the header in the top of the contact view (Text + underline view) 392 if (mHeaderVisible) { 393 mHeaderTextView.layout(leftBound + mHeaderTextIndent, 394 0, 395 width - mPaddingRight, 396 mHeaderBackgroundHeight); 397 if (mCountView != null) { 398 mCountView.layout(width - mPaddingRight - mCountView.getMeasuredWidth(), 399 0, 400 width - mPaddingRight, 401 mHeaderBackgroundHeight); 402 } 403 mHeaderDivider.layout(leftBound, 404 mHeaderBackgroundHeight, 405 width - mPaddingRight, 406 mHeaderBackgroundHeight + mHeaderUnderlineHeight); 407 topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight); 408 } 409 410 // Put horizontal divider at the bottom 411 if (mHorizontalDividerVisible) { 412 mHorizontalDividerDrawable.setBounds( 413 leftBound, 414 height - mHorizontalDividerHeight, 415 width - mPaddingRight, 416 height); 417 bottomBound -= mHorizontalDividerHeight; 418 } 419 420 mBoundsWithoutHeader.set(0, topBound, width, bottomBound); 421 422 if (mActivatedStateSupported && isActivated()) { 423 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 424 } 425 426 // Set the top/bottom paddings 427 topBound += mPaddingTop; 428 bottomBound -= mPaddingBottom; 429 430 // Set contact layout: 431 // Photo on the right, call button to the left of the photo 432 // Text aligned to the left along with the presence status. 433 434 // layout the photo and call button. 435 int rightBound = layoutRightSide(height, topBound, bottomBound, width - mPaddingRight); 436 437 // Center text vertically 438 int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + 439 mLabelTextViewHeight + mSnippetTextViewHeight + mStatusTextViewHeight; 440 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2; 441 442 // Layout all text view and presence icon 443 // Put name TextView first 444 if (isVisible(mNameTextView)) { 445 mNameTextView.layout(leftBound + mTextIndent, 446 textTopBound, 447 rightBound, 448 textTopBound + mNameTextViewHeight); 449 textTopBound += mNameTextViewHeight; 450 } 451 452 // Presence and status 453 int statusLeftBound = leftBound + mTextIndent; 454 if (isVisible(mPresenceIcon)) { 455 int iconWidth = mPresenceIcon.getMeasuredWidth(); 456 mPresenceIcon.layout( 457 leftBound + mTextIndent, 458 textTopBound, 459 leftBound + iconWidth + mTextIndent, 460 textTopBound + mStatusTextViewHeight); 461 statusLeftBound += (iconWidth + mPresenceIconMargin); 462 } 463 464 if (isVisible(mStatusView)) { 465 mStatusView.layout(statusLeftBound, 466 textTopBound, 467 rightBound, 468 textTopBound + mStatusTextViewHeight); 469 } 470 471 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { 472 textTopBound += mStatusTextViewHeight; 473 } 474 475 // Rest of text views 476 int dataLeftBound = leftBound; 477 if (isVisible(mPhoneticNameTextView)) { 478 mPhoneticNameTextView.layout(leftBound + mTextIndent, 479 textTopBound, 480 rightBound, 481 textTopBound + mPhoneticNameTextViewHeight); 482 textTopBound += mPhoneticNameTextViewHeight; 483 } 484 485 if (isVisible(mLabelView)) { 486 dataLeftBound = leftBound + mLabelView.getMeasuredWidth() + mTextIndent; 487 mLabelView.layout(leftBound + mTextIndent, 488 textTopBound, 489 dataLeftBound, 490 textTopBound + mLabelTextViewHeight); 491 dataLeftBound += mGapBetweenLabelAndData; 492 } 493 494 if (isVisible(mDataView)) { 495 mDataView.layout(dataLeftBound, 496 textTopBound, 497 rightBound, 498 textTopBound + mLabelTextViewHeight); 499 } 500 if (isVisible(mLabelView) || isVisible(mDataView)) { 501 textTopBound += mLabelTextViewHeight; 502 } 503 504 if (isVisible(mSnippetView)) { 505 mSnippetView.layout(leftBound + mTextIndent, 506 textTopBound, 507 rightBound, 508 textTopBound + mSnippetTextViewHeight); 509 } 510 } 511 512 /** 513 * Performs layout of the right side of the view 514 * 515 * @return new right boundary 516 */ 517 protected int layoutRightSide(int height, int topBound, int bottomBound, int rightBound) { 518 519 // Photo is the right most view, set it up 520 521 View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 522 if (photoView != null) { 523 // Center the photo vertically 524 int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 525 photoView.layout( 526 rightBound - mPhotoViewWidth, 527 photoTop, 528 rightBound, 529 photoTop + mPhotoViewHeight); 530 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 531 } 532 533 // Put call button and vertical divider 534 if (isVisible(mCallButton)) { 535 int buttonWidth = mCallButton.getMeasuredWidth(); 536 rightBound -= buttonWidth; 537 mCallButton.layout( 538 rightBound, 539 topBound, 540 rightBound + buttonWidth, 541 height - mHorizontalDividerHeight); 542 mVerticalDividerVisible = true; 543 ensureVerticalDivider(); 544 rightBound -= mVerticalDividerWidth; 545 mVerticalDividerDrawable.setBounds( 546 rightBound, 547 topBound + mVerticalDividerMargin, 548 rightBound + mVerticalDividerWidth, 549 height - mVerticalDividerMargin); 550 } else { 551 mVerticalDividerVisible = false; 552 } 553 554 return rightBound; 555 } 556 557 @Override 558 public void adjustListItemSelectionBounds(Rect bounds) { 559 bounds.top += mBoundsWithoutHeader.top; 560 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 561 } 562 563 protected boolean isVisible(View view) { 564 return view != null && view.getVisibility() == View.VISIBLE; 565 } 566 567 /** 568 * Loads the drawable for the vertical divider if it has not yet been loaded. 569 */ 570 private void ensureVerticalDivider() { 571 if (mVerticalDividerDrawable == null) { 572 mVerticalDividerDrawable = mContext.getResources().getDrawable( 573 R.drawable.divider_vertical_dark); 574 mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth(); 575 } 576 } 577 578 /** 579 * Extracts width and height from the style 580 */ 581 private void ensurePhotoViewSize() { 582 if (!mPhotoViewWidthAndHeightAreReady) { 583 if (mQuickContactEnabled) { 584 TypedArray a = mContext.obtainStyledAttributes(null, 585 com.android.internal.R.styleable.ViewGroup_Layout, 586 QUICK_CONTACT_BADGE_STYLE, 0); 587 mPhotoViewWidth = a.getLayoutDimension( 588 android.R.styleable.ViewGroup_Layout_layout_width, 589 ViewGroup.LayoutParams.WRAP_CONTENT); 590 mPhotoViewHeight = a.getLayoutDimension( 591 android.R.styleable.ViewGroup_Layout_layout_height, 592 ViewGroup.LayoutParams.WRAP_CONTENT); 593 a.recycle(); 594 } else if (mPhotoView != null) { 595 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); 596 } else { 597 final int defaultPhotoViewSize = getDefaultPhotoViewSize(); 598 mPhotoViewWidth = mKeepHorizontalPaddingForPhotoView ? defaultPhotoViewSize : 0; 599 mPhotoViewHeight = mKeepVerticalPaddingForPhotoView ? defaultPhotoViewSize : 0; 600 } 601 602 mPhotoViewWidthAndHeightAreReady = true; 603 } 604 } 605 606 protected void setDefaultPhotoViewSize(int pixels) { 607 mDefaultPhotoViewSize = pixels; 608 } 609 610 protected int getDefaultPhotoViewSize() { 611 return mDefaultPhotoViewSize; 612 } 613 614 @Override 615 protected void drawableStateChanged() { 616 super.drawableStateChanged(); 617 if (mActivatedStateSupported) { 618 mActivatedBackgroundDrawable.setState(getDrawableState()); 619 } 620 } 621 622 @Override 623 protected boolean verifyDrawable(Drawable who) { 624 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 625 } 626 627 @Override 628 public void jumpDrawablesToCurrentState() { 629 super.jumpDrawablesToCurrentState(); 630 if (mActivatedStateSupported) { 631 mActivatedBackgroundDrawable.jumpToCurrentState(); 632 } 633 } 634 635 @Override 636 public void dispatchDraw(Canvas canvas) { 637 if (mActivatedStateSupported && isActivated()) { 638 mActivatedBackgroundDrawable.draw(canvas); 639 } 640 if (mHorizontalDividerVisible) { 641 mHorizontalDividerDrawable.draw(canvas); 642 } 643 if (mVerticalDividerVisible) { 644 mVerticalDividerDrawable.draw(canvas); 645 } 646 647 super.dispatchDraw(canvas); 648 } 649 650 /** 651 * Sets the flag that determines whether a divider should drawn at the bottom 652 * of the view. 653 */ 654 public void setDividerVisible(boolean visible) { 655 mHorizontalDividerVisible = visible; 656 } 657 658 /** 659 * Sets section header or makes it invisible if the title is null. 660 */ 661 public void setSectionHeader(String title) { 662 if (!TextUtils.isEmpty(title)) { 663 if (mHeaderTextView == null) { 664 mHeaderTextView = new TextView(mContext); 665 mHeaderTextView.setTextColor(mHeaderTextColor); 666 mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize); 667 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD); 668 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL); 669 addView(mHeaderTextView); 670 } 671 if (mHeaderDivider == null) { 672 mHeaderDivider = new View(mContext); 673 mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor); 674 addView(mHeaderDivider); 675 } 676 mHeaderTextView.setText(title); 677 mHeaderTextView.setVisibility(View.VISIBLE); 678 mHeaderDivider.setVisibility(View.VISIBLE); 679 mHeaderTextView.setAllCaps(true); 680 mHeaderVisible = true; 681 } else { 682 if (mHeaderTextView != null) { 683 mHeaderTextView.setVisibility(View.GONE); 684 } 685 if (mHeaderDivider != null) { 686 mHeaderDivider.setVisibility(View.GONE); 687 } 688 mHeaderVisible = false; 689 } 690 } 691 692 /** 693 * Returns the quick contact badge, creating it if necessary. 694 */ 695 public QuickContactBadge getQuickContact() { 696 if (!mQuickContactEnabled) { 697 throw new IllegalStateException("QuickContact is disabled for this view"); 698 } 699 if (mQuickContact == null) { 700 mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE); 701 if (mNameTextView != null) { 702 mQuickContact.setContentDescription(mContext.getString( 703 R.string.description_quick_contact_for, mNameTextView.getText())); 704 } 705 706 addView(mQuickContact); 707 mPhotoViewWidthAndHeightAreReady = false; 708 } 709 return mQuickContact; 710 } 711 712 /** 713 * Returns the photo view, creating it if necessary. 714 */ 715 public ImageView getPhotoView() { 716 if (mPhotoView == null) { 717 if (mQuickContactEnabled) { 718 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE); 719 } else { 720 mPhotoView = new ImageView(mContext); 721 } 722 // Quick contact style used above will set a background - remove it 723 mPhotoView.setBackgroundDrawable(null); 724 addView(mPhotoView); 725 mPhotoViewWidthAndHeightAreReady = false; 726 } 727 return mPhotoView; 728 } 729 730 /** 731 * Removes the photo view. 732 */ 733 public void removePhotoView() { 734 removePhotoView(false, true); 735 } 736 737 /** 738 * Removes the photo view. 739 * 740 * @param keepHorizontalPadding True means data on the right side will have 741 * padding on left, pretending there is still a photo view. 742 * @param keepVerticalPadding True means the View will have some height 743 * enough for accommodating a photo view. 744 */ 745 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { 746 mPhotoViewWidthAndHeightAreReady = false; 747 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; 748 mKeepVerticalPaddingForPhotoView = keepVerticalPadding; 749 if (mPhotoView != null) { 750 removeView(mPhotoView); 751 mPhotoView = null; 752 } 753 if (mQuickContact != null) { 754 removeView(mQuickContact); 755 mQuickContact = null; 756 } 757 } 758 759 /** 760 * Sets a word prefix that will be highlighted if encountered in fields like 761 * name and search snippet. 762 * <p> 763 * NOTE: must be all upper-case 764 */ 765 public void setHighlightedPrefix(char[] upperCasePrefix) { 766 mHighlightedPrefix = upperCasePrefix; 767 } 768 769 /** 770 * Returns the text view for the contact name, creating it if necessary. 771 */ 772 public TextView getNameTextView() { 773 if (mNameTextView == null) { 774 mNameTextView = new TextView(mContext); 775 mNameTextView.setSingleLine(true); 776 mNameTextView.setEllipsize(getTextEllipsis()); 777 mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); 778 // Manually call setActivated() since this view may be added after the first 779 // setActivated() call toward this whole item view. 780 mNameTextView.setActivated(isActivated()); 781 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 782 addView(mNameTextView); 783 } 784 return mNameTextView; 785 } 786 787 /** 788 * Adds a call button using the supplied arguments as an id and tag. 789 */ 790 public void showCallButton(int id, int tag) { 791 if (mCallButton == null) { 792 mCallButton = new DontPressWithParentImageView(mContext, null); 793 mCallButton.setId(id); 794 mCallButton.setOnClickListener(mCallButtonClickListener); 795 mCallButton.setBackgroundResource(R.drawable.call_background); 796 mCallButton.setImageResource(android.R.drawable.sym_action_call); 797 mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0); 798 mCallButton.setScaleType(ScaleType.CENTER); 799 addView(mCallButton); 800 } 801 802 mCallButton.setTag(tag); 803 mCallButton.setVisibility(View.VISIBLE); 804 } 805 806 public void hideCallButton() { 807 if (mCallButton != null) { 808 mCallButton.setVisibility(View.GONE); 809 } 810 } 811 812 /** 813 * Adds or updates a text view for the phonetic name. 814 */ 815 public void setPhoneticName(char[] text, int size) { 816 if (text == null || size == 0) { 817 if (mPhoneticNameTextView != null) { 818 mPhoneticNameTextView.setVisibility(View.GONE); 819 } 820 } else { 821 getPhoneticNameTextView(); 822 mPhoneticNameTextView.setText(text, 0, size); 823 mPhoneticNameTextView.setVisibility(VISIBLE); 824 } 825 } 826 827 /** 828 * Returns the text view for the phonetic name, creating it if necessary. 829 */ 830 public TextView getPhoneticNameTextView() { 831 if (mPhoneticNameTextView == null) { 832 mPhoneticNameTextView = new TextView(mContext); 833 mPhoneticNameTextView.setSingleLine(true); 834 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 835 mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 836 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 837 mPhoneticNameTextView.setActivated(isActivated()); 838 addView(mPhoneticNameTextView); 839 } 840 return mPhoneticNameTextView; 841 } 842 843 /** 844 * Adds or updates a text view for the data label. 845 */ 846 public void setLabel(CharSequence text) { 847 if (TextUtils.isEmpty(text)) { 848 if (mLabelView != null) { 849 mLabelView.setVisibility(View.GONE); 850 } 851 } else { 852 getLabelView(); 853 mLabelView.setText(text); 854 mLabelView.setVisibility(VISIBLE); 855 } 856 } 857 858 /** 859 * Adds or updates a text view for the data label. 860 */ 861 public void setLabel(char[] text, int size) { 862 if (text == null || size == 0) { 863 if (mLabelView != null) { 864 mLabelView.setVisibility(View.GONE); 865 } 866 } else { 867 getLabelView(); 868 mLabelView.setText(text, 0, size); 869 mLabelView.setVisibility(VISIBLE); 870 } 871 } 872 873 /** 874 * Returns the text view for the data label, creating it if necessary. 875 */ 876 public TextView getLabelView() { 877 if (mLabelView == null) { 878 mLabelView = new TextView(mContext); 879 mLabelView.setSingleLine(true); 880 mLabelView.setEllipsize(getTextEllipsis()); 881 mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 882 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 883 mLabelView.setActivated(isActivated()); 884 addView(mLabelView); 885 } 886 return mLabelView; 887 } 888 889 /** 890 * Adds or updates a text view for the data element. 891 */ 892 public void setData(char[] text, int size) { 893 if (text == null || size == 0) { 894 if (mDataView != null) { 895 mDataView.setVisibility(View.GONE); 896 } 897 return; 898 } else { 899 getDataView(); 900 mDataView.setText(text, 0, size); 901 mDataView.setVisibility(VISIBLE); 902 } 903 } 904 905 /** 906 * Returns the text view for the data text, creating it if necessary. 907 */ 908 public TextView getDataView() { 909 if (mDataView == null) { 910 mDataView = new TextView(mContext); 911 mDataView.setSingleLine(true); 912 mDataView.setEllipsize(getTextEllipsis()); 913 mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 914 mDataView.setActivated(isActivated()); 915 addView(mDataView); 916 } 917 return mDataView; 918 } 919 920 /** 921 * Adds or updates a text view for the search snippet. 922 */ 923 public void setSnippet(String text) { 924 if (TextUtils.isEmpty(text)) { 925 if (mSnippetView != null) { 926 mSnippetView.setVisibility(View.GONE); 927 } 928 } else { 929 mPrefixHighligher.setText(getSnippetView(), text, mHighlightedPrefix); 930 mSnippetView.setVisibility(VISIBLE); 931 } 932 } 933 934 /** 935 * Returns the text view for the search snippet, creating it if necessary. 936 */ 937 public TextView getSnippetView() { 938 if (mSnippetView == null) { 939 mSnippetView = new TextView(mContext); 940 mSnippetView.setSingleLine(true); 941 mSnippetView.setEllipsize(getTextEllipsis()); 942 mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 943 mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD); 944 mSnippetView.setActivated(isActivated()); 945 addView(mSnippetView); 946 } 947 return mSnippetView; 948 } 949 950 /** 951 * Returns the text view for the status, creating it if necessary. 952 */ 953 public TextView getStatusView() { 954 if (mStatusView == null) { 955 mStatusView = new TextView(mContext); 956 mStatusView.setSingleLine(true); 957 mStatusView.setEllipsize(getTextEllipsis()); 958 mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 959 mStatusView.setTextColor(mSecondaryTextColor); 960 mStatusView.setActivated(isActivated()); 961 addView(mStatusView); 962 } 963 return mStatusView; 964 } 965 966 /** 967 * Returns the text view for the contacts count, creating it if necessary. 968 */ 969 public TextView getCountView() { 970 if (mCountView == null) { 971 mCountView = new TextView(mContext); 972 mCountView.setSingleLine(true); 973 mCountView.setEllipsize(getTextEllipsis()); 974 mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); 975 mCountView.setTextColor(R.color.contact_count_text_color); 976 addView(mCountView); 977 } 978 return mCountView; 979 } 980 981 /** 982 * Adds or updates a text view for the contacts count. 983 */ 984 public void setCountView(CharSequence text) { 985 if (TextUtils.isEmpty(text)) { 986 if (mCountView != null) { 987 mCountView.setVisibility(View.GONE); 988 } 989 } else { 990 getCountView(); 991 mCountView.setText(text); 992 mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize); 993 mCountView.setGravity(Gravity.CENTER_VERTICAL); 994 mCountView.setTextColor(mContactsCountTextColor); 995 mCountView.setVisibility(VISIBLE); 996 } 997 } 998 999 /** 1000 * Adds or updates a text view for the status. 1001 */ 1002 public void setStatus(CharSequence text) { 1003 if (TextUtils.isEmpty(text)) { 1004 if (mStatusView != null) { 1005 mStatusView.setVisibility(View.GONE); 1006 } 1007 } else { 1008 getStatusView(); 1009 mStatusView.setText(text); 1010 mStatusView.setVisibility(VISIBLE); 1011 } 1012 } 1013 1014 /** 1015 * Adds or updates the presence icon view. 1016 */ 1017 public void setPresence(Drawable icon) { 1018 if (icon != null) { 1019 if (mPresenceIcon == null) { 1020 mPresenceIcon = new ImageView(mContext); 1021 addView(mPresenceIcon); 1022 } 1023 mPresenceIcon.setImageDrawable(icon); 1024 mPresenceIcon.setScaleType(ScaleType.CENTER); 1025 mPresenceIcon.setVisibility(View.VISIBLE); 1026 } else { 1027 if (mPresenceIcon != null) { 1028 mPresenceIcon.setVisibility(View.GONE); 1029 } 1030 } 1031 } 1032 1033 private TruncateAt getTextEllipsis() { 1034 return mActivatedStateSupported ? TruncateAt.START : TruncateAt.MARQUEE; 1035 } 1036 1037 public void showDisplayName(Cursor cursor, int nameColumnIndex, int alternativeNameColumnIndex, 1038 boolean highlightingEnabled, int displayOrder) { 1039 // Copy out the display name and alternate display name. 1040 cursor.copyStringToBuffer(nameColumnIndex, mDisplayNameFormatter.getNameBuffer()); 1041 cursor.copyStringToBuffer(alternativeNameColumnIndex, 1042 mDisplayNameFormatter.getAlternateNameBuffer()); 1043 1044 mDisplayNameFormatter.setDisplayName( 1045 getNameTextView(), displayOrder, highlightingEnabled, mHighlightedPrefix); 1046 // Since the quick contact content description is derived from the display name and there is 1047 // no guarantee that when the quick contact is initialized the display name is already set, 1048 // do it here too. 1049 if (mQuickContact != null) { 1050 mQuickContact.setContentDescription(mContext.getString( 1051 R.string.description_quick_contact_for, mNameTextView.getText())); 1052 } 1053 } 1054 1055 public void hideDisplayName() { 1056 if (mNameTextView != null) { 1057 removeView(mNameTextView); 1058 mNameTextView = null; 1059 } 1060 } 1061 1062 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 1063 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 1064 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 1065 if (phoneticNameSize != 0) { 1066 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 1067 } else { 1068 setPhoneticName(null, 0); 1069 } 1070 } 1071 1072 public void hidePhoneticName() { 1073 if (mPhoneticNameTextView != null) { 1074 removeView(mPhoneticNameTextView); 1075 mPhoneticNameTextView = null; 1076 } 1077 } 1078 1079 /** 1080 * Sets the proper icon (star or presence or nothing) and/or status message. 1081 */ 1082 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, 1083 int contactStatusColumnIndex) { 1084 Drawable icon = null; 1085 int presence = 0; 1086 if (!cursor.isNull(presenceColumnIndex)) { 1087 presence = cursor.getInt(presenceColumnIndex); 1088 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); 1089 } 1090 setPresence(icon); 1091 1092 String statusMessage = null; 1093 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { 1094 statusMessage = cursor.getString(contactStatusColumnIndex); 1095 } 1096 // If there is no status message from the contact, but there was a presence value, then use 1097 // the default status message string 1098 if (statusMessage == null && presence != 0) { 1099 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); 1100 } 1101 setStatus(statusMessage); 1102 } 1103 1104 /** 1105 * Shows search snippet. 1106 */ 1107 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 1108 if (cursor.getColumnCount() <= summarySnippetColumnIndex) { 1109 setSnippet(null); 1110 return; 1111 } 1112 String snippet; 1113 String columnContent = cursor.getString(summarySnippetColumnIndex); 1114 1115 // Do client side snippeting if provider didn't do it 1116 Bundle extras = cursor.getExtras(); 1117 if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { 1118 int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); 1119 1120 snippet = ContactsContract.snippetize(columnContent, 1121 displayNameIndex < 0 ? null : cursor.getString(displayNameIndex), 1122 extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY), 1123 DefaultContactListAdapter.SNIPPET_START_MATCH, 1124 DefaultContactListAdapter.SNIPPET_END_MATCH, 1125 DefaultContactListAdapter.SNIPPET_ELLIPSIS, 1126 DefaultContactListAdapter.SNIPPET_MAX_TOKENS); 1127 } else { 1128 snippet = columnContent; 1129 } 1130 1131 if (snippet != null) { 1132 int from = 0; 1133 int to = snippet.length(); 1134 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 1135 if (start == -1) { 1136 snippet = null; 1137 } else { 1138 int firstNl = snippet.lastIndexOf('\n', start); 1139 if (firstNl != -1) { 1140 from = firstNl + 1; 1141 } 1142 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 1143 if (end != -1) { 1144 int lastNl = snippet.indexOf('\n', end); 1145 if (lastNl != -1) { 1146 to = lastNl; 1147 } 1148 } 1149 1150 StringBuilder sb = new StringBuilder(); 1151 for (int i = from; i < to; i++) { 1152 char c = snippet.charAt(i); 1153 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 1154 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 1155 sb.append(c); 1156 } 1157 } 1158 snippet = sb.toString(); 1159 } 1160 } 1161 setSnippet(snippet); 1162 } 1163 1164 /** 1165 * Shows data element (e.g. phone number). 1166 */ 1167 public void showData(Cursor cursor, int dataColumnIndex) { 1168 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 1169 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 1170 } 1171 1172 public void setActivatedStateSupported(boolean flag) { 1173 this.mActivatedStateSupported = flag; 1174 } 1175 1176 @Override 1177 public void requestLayout() { 1178 // We will assume that once measured this will not need to resize 1179 // itself, so there is no need to pass the layout request to the parent 1180 // view (ListView). 1181 forceLayout(); 1182 } 1183} 1184