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