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