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