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.common.list; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.TypedArray; 22 import android.database.CharArrayBuffer; 23 import android.database.Cursor; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.Rect; 27 import android.graphics.Typeface; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.provider.ContactsContract; 31 import android.provider.ContactsContract.Contacts; 32 import android.telephony.PhoneNumberUtils; 33 import android.text.Spannable; 34 import android.text.SpannableString; 35 import android.text.TextUtils; 36 import android.text.TextUtils.TruncateAt; 37 import android.util.AttributeSet; 38 import android.util.TypedValue; 39 import android.view.Gravity; 40 import android.view.MotionEvent; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.widget.AbsListView.SelectionBoundsAdjuster; 44 import android.widget.CheckBox; 45 import android.widget.ImageView; 46 import android.widget.ImageView.ScaleType; 47 import android.widget.QuickContactBadge; 48 import android.widget.TextView; 49 50 import com.android.contacts.common.ContactPresenceIconUtil; 51 import com.android.contacts.common.ContactStatusUtil; 52 import com.android.contacts.common.R; 53 import com.android.contacts.common.format.TextHighlighter; 54 import com.android.contacts.common.util.ContactDisplayUtils; 55 import com.android.contacts.common.util.SearchUtil; 56 import com.android.contacts.common.util.ViewUtil; 57 58 import com.google.common.collect.Lists; 59 60 import java.util.ArrayList; 61 import java.util.List; 62 import java.util.Locale; 63 import java.util.regex.Matcher; 64 import java.util.regex.Pattern; 65 66 /** 67 * A custom view for an item in the contact list. 68 * The view contains the contact's photo, a set of text views (for name, status, etc...) and 69 * icons for presence and call. 70 * The view uses no XML file for layout and all the measurements and layouts are done 71 * in the onMeasure and onLayout methods. 72 * 73 * The layout puts the contact's photo on the right side of the view, the call icon (if present) 74 * to the left of the photo, the text lines are aligned to the left and the presence icon (if 75 * present) is set to the left of the status line. 76 * 77 * The layout also supports a header (used as a header of a group of contacts) that is above the 78 * contact's data and a divider between contact view. 79 */ 80 81 public class ContactListItemView extends ViewGroup 82 implements SelectionBoundsAdjuster { 83 84 // Style values for layout and appearance 85 // The initialized values are defaults if none is provided through xml. 86 private int mPreferredHeight = 0; 87 private int mGapBetweenImageAndText = 0; 88 private int mGapBetweenLabelAndData = 0; 89 private int mPresenceIconMargin = 4; 90 private int mPresenceIconSize = 16; 91 private int mTextIndent = 0; 92 private int mTextOffsetTop; 93 private int mNameTextViewTextSize; 94 private int mHeaderWidth; 95 private Drawable mActivatedBackgroundDrawable; 96 97 // Set in onLayout. Represent left and right position of the View on the screen. 98 private int mLeftOffset; 99 private int mRightOffset; 100 101 /** 102 * Used with {@link #mLabelView}, specifying the width ratio between label and data. 103 */ 104 private int mLabelViewWidthWeight = 3; 105 /** 106 * Used with {@link #mDataView}, specifying the width ratio between label and data. 107 */ 108 private int mDataViewWidthWeight = 5; 109 110 protected static class HighlightSequence { 111 private final int start; 112 private final int end; 113 HighlightSequence(int start, int end)114 HighlightSequence(int start, int end) { 115 this.start = start; 116 this.end = end; 117 } 118 } 119 120 private ArrayList<HighlightSequence> mNameHighlightSequence; 121 private ArrayList<HighlightSequence> mNumberHighlightSequence; 122 123 // Highlighting prefix for names. 124 private String mHighlightedPrefix; 125 126 /** 127 * Where to put contact photo. This affects the other Views' layout or look-and-feel. 128 * 129 * TODO: replace enum with int constants 130 */ 131 public enum PhotoPosition { 132 LEFT, 133 RIGHT 134 } 135 getDefaultPhotoPosition(boolean opposite)136 static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) { 137 final Locale locale = Locale.getDefault(); 138 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 139 switch (layoutDirection) { 140 case View.LAYOUT_DIRECTION_RTL: 141 return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT); 142 case View.LAYOUT_DIRECTION_LTR: 143 default: 144 return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT); 145 } 146 } 147 148 private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */); 149 150 // Header layout data 151 private TextView mHeaderTextView; 152 private boolean mIsSectionHeaderEnabled; 153 154 // The views inside the contact view 155 private boolean mQuickContactEnabled = true; 156 private QuickContactBadge mQuickContact; 157 private ImageView mPhotoView; 158 private TextView mNameTextView; 159 private TextView mPhoneticNameTextView; 160 private TextView mLabelView; 161 private TextView mDataView; 162 private TextView mSnippetView; 163 private TextView mStatusView; 164 private ImageView mPresenceIcon; 165 private CheckBox mCheckBox; 166 167 private ColorStateList mSecondaryTextColor; 168 169 170 171 private int mDefaultPhotoViewSize = 0; 172 /** 173 * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding 174 * to align other data in this View. 175 */ 176 private int mPhotoViewWidth; 177 /** 178 * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. 179 */ 180 private int mPhotoViewHeight; 181 182 /** 183 * Only effective when {@link #mPhotoView} is null. 184 * When true all the Views on the right side of the photo should have horizontal padding on 185 * those left assuming there is a photo. 186 */ 187 private boolean mKeepHorizontalPaddingForPhotoView; 188 /** 189 * Only effective when {@link #mPhotoView} is null. 190 */ 191 private boolean mKeepVerticalPaddingForPhotoView; 192 193 /** 194 * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. 195 * False indicates those values should be updated before being used in position calculation. 196 */ 197 private boolean mPhotoViewWidthAndHeightAreReady = false; 198 199 private int mNameTextViewHeight; 200 private int mNameTextViewTextColor = Color.BLACK; 201 private int mPhoneticNameTextViewHeight; 202 private int mLabelViewHeight; 203 private int mDataViewHeight; 204 private int mSnippetTextViewHeight; 205 private int mStatusTextViewHeight; 206 private int mCheckBoxHeight; 207 private int mCheckBoxWidth; 208 209 // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the 210 // same row. 211 private int mLabelAndDataViewMaxHeight; 212 213 // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is 214 // more efficient for each case or in general, and simplify the whole implementation. 215 // Note: if we're sure MARQUEE will be used every time, there's no reason to use 216 // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the 217 // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to 218 // TextView without any modification. 219 private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); 220 private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); 221 222 private boolean mActivatedStateSupported; 223 private boolean mAdjustSelectionBoundsEnabled = true; 224 225 private Rect mBoundsWithoutHeader = new Rect(); 226 227 /** A helper used to highlight a prefix in a text field. */ 228 private final TextHighlighter mTextHighlighter; 229 private CharSequence mUnknownNameText; 230 ContactListItemView(Context context)231 public ContactListItemView(Context context) { 232 super(context); 233 234 mTextHighlighter = new TextHighlighter(Typeface.BOLD); 235 mNameHighlightSequence = new ArrayList<HighlightSequence>(); 236 mNumberHighlightSequence = new ArrayList<HighlightSequence>(); 237 } 238 ContactListItemView(Context context, AttributeSet attrs)239 public ContactListItemView(Context context, AttributeSet attrs) { 240 super(context, attrs); 241 242 // Read all style values 243 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); 244 mPreferredHeight = a.getDimensionPixelSize( 245 R.styleable.ContactListItemView_list_item_height, mPreferredHeight); 246 mActivatedBackgroundDrawable = a.getDrawable( 247 R.styleable.ContactListItemView_activated_background); 248 249 mGapBetweenImageAndText = a.getDimensionPixelOffset( 250 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 251 mGapBetweenImageAndText); 252 mGapBetweenLabelAndData = a.getDimensionPixelOffset( 253 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 254 mGapBetweenLabelAndData); 255 mPresenceIconMargin = a.getDimensionPixelOffset( 256 R.styleable.ContactListItemView_list_item_presence_icon_margin, 257 mPresenceIconMargin); 258 mPresenceIconSize = a.getDimensionPixelOffset( 259 R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize); 260 mDefaultPhotoViewSize = a.getDimensionPixelOffset( 261 R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize); 262 mTextIndent = a.getDimensionPixelOffset( 263 R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); 264 mTextOffsetTop = a.getDimensionPixelOffset( 265 R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); 266 mDataViewWidthWeight = a.getInteger( 267 R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight); 268 mLabelViewWidthWeight = a.getInteger( 269 R.styleable.ContactListItemView_list_item_label_width_weight, 270 mLabelViewWidthWeight); 271 mNameTextViewTextColor = a.getColor( 272 R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor); 273 mNameTextViewTextSize = (int) a.getDimension( 274 R.styleable.ContactListItemView_list_item_name_text_size, 275 (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); 276 277 setPaddingRelative( 278 a.getDimensionPixelOffset( 279 R.styleable.ContactListItemView_list_item_padding_left, 0), 280 a.getDimensionPixelOffset( 281 R.styleable.ContactListItemView_list_item_padding_top, 0), 282 a.getDimensionPixelOffset( 283 R.styleable.ContactListItemView_list_item_padding_right, 0), 284 a.getDimensionPixelOffset( 285 R.styleable.ContactListItemView_list_item_padding_bottom, 0)); 286 287 mTextHighlighter = new TextHighlighter(Typeface.BOLD); 288 289 a.recycle(); 290 291 a = getContext().obtainStyledAttributes(R.styleable.Theme); 292 mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); 293 a.recycle(); 294 295 mHeaderWidth = 296 getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); 297 298 if (mActivatedBackgroundDrawable != null) { 299 mActivatedBackgroundDrawable.setCallback(this); 300 } 301 302 mNameHighlightSequence = new ArrayList<HighlightSequence>(); 303 mNumberHighlightSequence = new ArrayList<HighlightSequence>(); 304 305 setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); 306 } 307 setUnknownNameText(CharSequence unknownNameText)308 public void setUnknownNameText(CharSequence unknownNameText) { 309 mUnknownNameText = unknownNameText; 310 } 311 setQuickContactEnabled(boolean flag)312 public void setQuickContactEnabled(boolean flag) { 313 mQuickContactEnabled = flag; 314 } 315 316 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)317 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 318 // We will match parent's width and wrap content vertically, but make sure 319 // height is no less than listPreferredItemHeight. 320 final int specWidth = resolveSize(0, widthMeasureSpec); 321 final int preferredHeight = mPreferredHeight; 322 323 mNameTextViewHeight = 0; 324 mPhoneticNameTextViewHeight = 0; 325 mLabelViewHeight = 0; 326 mDataViewHeight = 0; 327 mLabelAndDataViewMaxHeight = 0; 328 mSnippetTextViewHeight = 0; 329 mStatusTextViewHeight = 0; 330 mCheckBoxWidth = 0; 331 mCheckBoxHeight = 0; 332 333 ensurePhotoViewSize(); 334 335 // Width each TextView is able to use. 336 int effectiveWidth; 337 // All the other Views will honor the photo, so available width for them may be shrunk. 338 if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) { 339 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight() 340 - (mPhotoViewWidth + mGapBetweenImageAndText); 341 } else { 342 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); 343 } 344 345 if (mIsSectionHeaderEnabled) { 346 effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText; 347 } 348 349 // Go over all visible text views and measure actual width of each of them. 350 // Also calculate their heights to get the total height for this entire view. 351 352 if (isVisible(mCheckBox)) { 353 mCheckBox.measure( 354 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 355 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 356 mCheckBoxWidth = mCheckBox.getMeasuredWidth(); 357 mCheckBoxHeight = mCheckBox.getMeasuredHeight(); 358 effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText; 359 } 360 361 if (isVisible(mNameTextView)) { 362 // Calculate width for name text - this parallels similar measurement in onLayout. 363 int nameTextWidth = effectiveWidth; 364 if (mPhotoPosition != PhotoPosition.LEFT) { 365 nameTextWidth -= mTextIndent; 366 } 367 mNameTextView.measure( 368 MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY), 369 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 370 mNameTextViewHeight = mNameTextView.getMeasuredHeight(); 371 } 372 373 if (isVisible(mPhoneticNameTextView)) { 374 mPhoneticNameTextView.measure( 375 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), 376 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 377 mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight(); 378 } 379 380 // If both data (phone number/email address) and label (type like "MOBILE") are quite long, 381 // we should ellipsize both using appropriate ratio. 382 final int dataWidth; 383 final int labelWidth; 384 if (isVisible(mDataView)) { 385 if (isVisible(mLabelView)) { 386 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData; 387 dataWidth = ((totalWidth * mDataViewWidthWeight) 388 / (mDataViewWidthWeight + mLabelViewWidthWeight)); 389 labelWidth = ((totalWidth * mLabelViewWidthWeight) / 390 (mDataViewWidthWeight + mLabelViewWidthWeight)); 391 } else { 392 dataWidth = effectiveWidth; 393 labelWidth = 0; 394 } 395 } else { 396 dataWidth = 0; 397 if (isVisible(mLabelView)) { 398 labelWidth = effectiveWidth; 399 } else { 400 labelWidth = 0; 401 } 402 } 403 404 if (isVisible(mDataView)) { 405 mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY), 406 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 407 mDataViewHeight = mDataView.getMeasuredHeight(); 408 } 409 410 if (isVisible(mLabelView)) { 411 // For performance reason we don't want AT_MOST usually, but when the picture is 412 // on right, we need to use it anyway because mDataView is next to mLabelView. 413 final int mode = (mPhotoPosition == PhotoPosition.LEFT 414 ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); 415 mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode), 416 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 417 mLabelViewHeight = mLabelView.getMeasuredHeight(); 418 } 419 mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight); 420 421 if (isVisible(mSnippetView)) { 422 mSnippetView.measure( 423 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), 424 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 425 mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); 426 } 427 428 // Status view height is the biggest of the text view and the presence icon 429 if (isVisible(mPresenceIcon)) { 430 mPresenceIcon.measure( 431 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY), 432 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY)); 433 mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); 434 } 435 436 if (isVisible(mStatusView)) { 437 // Presence and status are in a same row, so status will be affected by icon size. 438 final int statusWidth; 439 if (isVisible(mPresenceIcon)) { 440 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() 441 - mPresenceIconMargin); 442 } else { 443 statusWidth = effectiveWidth; 444 } 445 mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY), 446 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 447 mStatusTextViewHeight = 448 Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight()); 449 } 450 451 // Calculate height including padding. 452 int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight + 453 mLabelAndDataViewMaxHeight + 454 mSnippetTextViewHeight + mStatusTextViewHeight); 455 456 // Make sure the height is at least as high as the photo 457 height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); 458 459 // Make sure height is at least the preferred height 460 height = Math.max(height, preferredHeight); 461 462 // Measure the header if it is visible. 463 if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) { 464 mHeaderTextView.measure( 465 MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), 466 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 467 } 468 469 setMeasuredDimension(specWidth, height); 470 } 471 472 @Override onLayout(boolean changed, int left, int top, int right, int bottom)473 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 474 final int height = bottom - top; 475 final int width = right - left; 476 477 // Determine the vertical bounds by laying out the header first. 478 int topBound = 0; 479 int bottomBound = height; 480 int leftBound = getPaddingLeft(); 481 int rightBound = width - getPaddingRight(); 482 483 final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); 484 485 // Put the section header on the left side of the contact view. 486 if (mIsSectionHeaderEnabled) { 487 if (mHeaderTextView != null) { 488 int headerHeight = mHeaderTextView.getMeasuredHeight(); 489 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; 490 491 mHeaderTextView.layout( 492 isLayoutRtl ? rightBound - mHeaderWidth : leftBound, 493 headerTopBound, 494 isLayoutRtl ? rightBound : leftBound + mHeaderWidth, 495 headerTopBound + headerHeight); 496 } 497 if (isLayoutRtl) { 498 rightBound -= mHeaderWidth; 499 } else { 500 leftBound += mHeaderWidth; 501 } 502 } 503 504 mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); 505 mLeftOffset = left + leftBound; 506 mRightOffset = left + rightBound; 507 if (mIsSectionHeaderEnabled) { 508 if (isLayoutRtl) { 509 rightBound -= mGapBetweenImageAndText; 510 } else { 511 leftBound += mGapBetweenImageAndText; 512 } 513 } 514 515 if (mActivatedStateSupported && isActivated()) { 516 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 517 } 518 519 if (isVisible(mCheckBox)) { 520 final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2; 521 if (mPhotoPosition == PhotoPosition.LEFT) { 522 mCheckBox.layout(rightBound - mCheckBoxWidth, 523 photoTop, 524 rightBound, 525 photoTop + mCheckBoxHeight); 526 } else { 527 mCheckBox.layout(leftBound, 528 photoTop, 529 leftBound + mCheckBoxWidth, 530 photoTop + mCheckBoxHeight); 531 } 532 } 533 534 final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 535 if (mPhotoPosition == PhotoPosition.LEFT) { 536 // Photo is the left most view. All the other Views should on the right of the photo. 537 if (photoView != null) { 538 // Center the photo vertically 539 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 540 photoView.layout( 541 leftBound, 542 photoTop, 543 leftBound + mPhotoViewWidth, 544 photoTop + mPhotoViewHeight); 545 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 546 } else if (mKeepHorizontalPaddingForPhotoView) { 547 // Draw nothing but keep the padding. 548 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 549 } 550 } else { 551 // Photo is the right most view. Right bound should be adjusted that way. 552 if (photoView != null) { 553 // Center the photo vertically 554 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 555 photoView.layout( 556 rightBound - mPhotoViewWidth, 557 photoTop, 558 rightBound, 559 photoTop + mPhotoViewHeight); 560 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 561 } else if (mKeepHorizontalPaddingForPhotoView) { 562 // Draw nothing but keep the padding. 563 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 564 } 565 566 // Add indent between left-most padding and texts. 567 leftBound += mTextIndent; 568 } 569 570 // Center text vertically, then apply the top offset. 571 final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + 572 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight; 573 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; 574 575 // Layout all text view and presence icon 576 // Put name TextView first 577 if (isVisible(mNameTextView)) { 578 final int distanceFromEnd = mCheckBoxWidth > 0 579 ? mCheckBoxWidth + mGapBetweenImageAndText : 0; 580 if (mPhotoPosition == PhotoPosition.LEFT) { 581 mNameTextView.layout(leftBound, 582 textTopBound, 583 rightBound - distanceFromEnd, 584 textTopBound + mNameTextViewHeight); 585 } else { 586 mNameTextView.layout(leftBound + distanceFromEnd, 587 textTopBound, 588 rightBound, 589 textTopBound + mNameTextViewHeight); 590 } 591 textTopBound += mNameTextViewHeight; 592 } 593 594 // Presence and status 595 if (isLayoutRtl) { 596 int statusRightBound = rightBound; 597 if (isVisible(mPresenceIcon)) { 598 int iconWidth = mPresenceIcon.getMeasuredWidth(); 599 mPresenceIcon.layout( 600 rightBound - iconWidth, 601 textTopBound, 602 rightBound, 603 textTopBound + mStatusTextViewHeight); 604 statusRightBound -= (iconWidth + mPresenceIconMargin); 605 } 606 607 if (isVisible(mStatusView)) { 608 mStatusView.layout(leftBound, 609 textTopBound, 610 statusRightBound, 611 textTopBound + mStatusTextViewHeight); 612 } 613 } else { 614 int statusLeftBound = leftBound; 615 if (isVisible(mPresenceIcon)) { 616 int iconWidth = mPresenceIcon.getMeasuredWidth(); 617 mPresenceIcon.layout( 618 leftBound, 619 textTopBound, 620 leftBound + iconWidth, 621 textTopBound + mStatusTextViewHeight); 622 statusLeftBound += (iconWidth + mPresenceIconMargin); 623 } 624 625 if (isVisible(mStatusView)) { 626 mStatusView.layout(statusLeftBound, 627 textTopBound, 628 rightBound, 629 textTopBound + mStatusTextViewHeight); 630 } 631 } 632 633 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { 634 textTopBound += mStatusTextViewHeight; 635 } 636 637 // Rest of text views 638 int dataLeftBound = leftBound; 639 if (isVisible(mPhoneticNameTextView)) { 640 mPhoneticNameTextView.layout(leftBound, 641 textTopBound, 642 rightBound, 643 textTopBound + mPhoneticNameTextViewHeight); 644 textTopBound += mPhoneticNameTextViewHeight; 645 } 646 647 // Label and Data align bottom. 648 if (isVisible(mLabelView)) { 649 if (mPhotoPosition == PhotoPosition.LEFT) { 650 // When photo is on left, label is placed on the right edge of the list item. 651 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(), 652 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 653 rightBound, 654 textTopBound + mLabelAndDataViewMaxHeight); 655 rightBound -= mLabelView.getMeasuredWidth(); 656 } else { 657 // When photo is on right, label is placed on the left of data view. 658 dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); 659 mLabelView.layout(leftBound, 660 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 661 dataLeftBound, 662 textTopBound + mLabelAndDataViewMaxHeight); 663 dataLeftBound += mGapBetweenLabelAndData; 664 } 665 } 666 667 if (isVisible(mDataView)) { 668 mDataView.layout(dataLeftBound, 669 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, 670 rightBound, 671 textTopBound + mLabelAndDataViewMaxHeight); 672 } 673 if (isVisible(mLabelView) || isVisible(mDataView)) { 674 textTopBound += mLabelAndDataViewMaxHeight; 675 } 676 677 if (isVisible(mSnippetView)) { 678 mSnippetView.layout(leftBound, 679 textTopBound, 680 rightBound, 681 textTopBound + mSnippetTextViewHeight); 682 } 683 } 684 685 @Override adjustListItemSelectionBounds(Rect bounds)686 public void adjustListItemSelectionBounds(Rect bounds) { 687 if (mAdjustSelectionBoundsEnabled) { 688 bounds.top += mBoundsWithoutHeader.top; 689 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 690 bounds.left = mBoundsWithoutHeader.left; 691 bounds.right = mBoundsWithoutHeader.right; 692 } 693 } 694 isVisible(View view)695 protected boolean isVisible(View view) { 696 return view != null && view.getVisibility() == View.VISIBLE; 697 } 698 699 /** 700 * Extracts width and height from the style 701 */ ensurePhotoViewSize()702 private void ensurePhotoViewSize() { 703 if (!mPhotoViewWidthAndHeightAreReady) { 704 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); 705 if (!mQuickContactEnabled && mPhotoView == null) { 706 if (!mKeepHorizontalPaddingForPhotoView) { 707 mPhotoViewWidth = 0; 708 } 709 if (!mKeepVerticalPaddingForPhotoView) { 710 mPhotoViewHeight = 0; 711 } 712 } 713 714 mPhotoViewWidthAndHeightAreReady = true; 715 } 716 } 717 getDefaultPhotoViewSize()718 protected int getDefaultPhotoViewSize() { 719 return mDefaultPhotoViewSize; 720 } 721 722 /** 723 * Gets a LayoutParam that corresponds to the default photo size. 724 * 725 * @return A new LayoutParam. 726 */ getDefaultPhotoLayoutParams()727 private LayoutParams getDefaultPhotoLayoutParams() { 728 LayoutParams params = generateDefaultLayoutParams(); 729 params.width = getDefaultPhotoViewSize(); 730 params.height = params.width; 731 return params; 732 } 733 734 @Override drawableStateChanged()735 protected void drawableStateChanged() { 736 super.drawableStateChanged(); 737 if (mActivatedStateSupported) { 738 mActivatedBackgroundDrawable.setState(getDrawableState()); 739 } 740 } 741 742 @Override verifyDrawable(Drawable who)743 protected boolean verifyDrawable(Drawable who) { 744 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 745 } 746 747 @Override jumpDrawablesToCurrentState()748 public void jumpDrawablesToCurrentState() { 749 super.jumpDrawablesToCurrentState(); 750 if (mActivatedStateSupported) { 751 mActivatedBackgroundDrawable.jumpToCurrentState(); 752 } 753 } 754 755 @Override dispatchDraw(Canvas canvas)756 public void dispatchDraw(Canvas canvas) { 757 if (mActivatedStateSupported && isActivated()) { 758 mActivatedBackgroundDrawable.draw(canvas); 759 } 760 761 super.dispatchDraw(canvas); 762 } 763 764 /** 765 * Sets section header or makes it invisible if the title is null. 766 */ setSectionHeader(String title)767 public void setSectionHeader(String title) { 768 if (!TextUtils.isEmpty(title)) { 769 if (mHeaderTextView == null) { 770 mHeaderTextView = new TextView(getContext()); 771 mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); 772 mHeaderTextView.setGravity( 773 ViewUtil.isViewLayoutRtl(this) ? Gravity.RIGHT : Gravity.LEFT); 774 addView(mHeaderTextView); 775 } 776 setMarqueeText(mHeaderTextView, title); 777 mHeaderTextView.setVisibility(View.VISIBLE); 778 mHeaderTextView.setAllCaps(true); 779 } else if (mHeaderTextView != null) { 780 mHeaderTextView.setVisibility(View.GONE); 781 } 782 } 783 setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)784 public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { 785 mIsSectionHeaderEnabled = isSectionHeaderEnabled; 786 } 787 788 /** 789 * Returns the quick contact badge, creating it if necessary. 790 */ getQuickContact()791 public QuickContactBadge getQuickContact() { 792 if (!mQuickContactEnabled) { 793 throw new IllegalStateException("QuickContact is disabled for this view"); 794 } 795 if (mQuickContact == null) { 796 mQuickContact = new QuickContactBadge(getContext()); 797 mQuickContact.setOverlay(null); 798 mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); 799 if (mNameTextView != null) { 800 mQuickContact.setContentDescription(getContext().getString( 801 R.string.description_quick_contact_for, mNameTextView.getText())); 802 } 803 804 addView(mQuickContact); 805 mPhotoViewWidthAndHeightAreReady = false; 806 } 807 return mQuickContact; 808 } 809 810 /** 811 * Returns the photo view, creating it if necessary. 812 */ getPhotoView()813 public ImageView getPhotoView() { 814 if (mPhotoView == null) { 815 mPhotoView = new ImageView(getContext()); 816 mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); 817 // Quick contact style used above will set a background - remove it 818 mPhotoView.setBackground(null); 819 addView(mPhotoView); 820 mPhotoViewWidthAndHeightAreReady = false; 821 } 822 return mPhotoView; 823 } 824 825 /** 826 * Removes the photo view. 827 */ removePhotoView()828 public void removePhotoView() { 829 removePhotoView(false, true); 830 } 831 832 /** 833 * Removes the photo view. 834 * 835 * @param keepHorizontalPadding True means data on the right side will have 836 * padding on left, pretending there is still a photo view. 837 * @param keepVerticalPadding True means the View will have some height 838 * enough for accommodating a photo view. 839 */ removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)840 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { 841 mPhotoViewWidthAndHeightAreReady = false; 842 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; 843 mKeepVerticalPaddingForPhotoView = keepVerticalPadding; 844 if (mPhotoView != null) { 845 removeView(mPhotoView); 846 mPhotoView = null; 847 } 848 if (mQuickContact != null) { 849 removeView(mQuickContact); 850 mQuickContact = null; 851 } 852 } 853 854 /** 855 * Sets a word prefix that will be highlighted if encountered in fields like 856 * name and search snippet. This will disable the mask highlighting for names. 857 * <p> 858 * NOTE: must be all upper-case 859 */ setHighlightedPrefix(String upperCasePrefix)860 public void setHighlightedPrefix(String upperCasePrefix) { 861 mHighlightedPrefix = upperCasePrefix; 862 } 863 864 /** 865 * Clears previously set highlight sequences for the view. 866 */ clearHighlightSequences()867 public void clearHighlightSequences() { 868 mNameHighlightSequence.clear(); 869 mNumberHighlightSequence.clear(); 870 mHighlightedPrefix = null; 871 } 872 873 /** 874 * Adds a highlight sequence to the name highlighter. 875 * @param start The start position of the highlight sequence. 876 * @param end The end position of the highlight sequence. 877 */ addNameHighlightSequence(int start, int end)878 public void addNameHighlightSequence(int start, int end) { 879 mNameHighlightSequence.add(new HighlightSequence(start, end)); 880 } 881 882 /** 883 * Adds a highlight sequence to the number highlighter. 884 * @param start The start position of the highlight sequence. 885 * @param end The end position of the highlight sequence. 886 */ addNumberHighlightSequence(int start, int end)887 public void addNumberHighlightSequence(int start, int end) { 888 mNumberHighlightSequence.add(new HighlightSequence(start, end)); 889 } 890 891 /** 892 * Returns the text view for the contact name, creating it if necessary. 893 */ getNameTextView()894 public TextView getNameTextView() { 895 if (mNameTextView == null) { 896 mNameTextView = new TextView(getContext()); 897 mNameTextView.setSingleLine(true); 898 mNameTextView.setEllipsize(getTextEllipsis()); 899 mNameTextView.setTextColor(mNameTextViewTextColor); 900 mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 901 mNameTextViewTextSize); 902 // Manually call setActivated() since this view may be added after the first 903 // setActivated() call toward this whole item view. 904 mNameTextView.setActivated(isActivated()); 905 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 906 mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 907 mNameTextView.setId(R.id.cliv_name_textview); 908 mNameTextView.setElegantTextHeight(false); 909 addView(mNameTextView); 910 } 911 return mNameTextView; 912 } 913 914 /** 915 * Adds or updates a text view for the phonetic name. 916 */ setPhoneticName(char[] text, int size)917 public void setPhoneticName(char[] text, int size) { 918 if (text == null || size == 0) { 919 if (mPhoneticNameTextView != null) { 920 mPhoneticNameTextView.setVisibility(View.GONE); 921 } 922 } else { 923 getPhoneticNameTextView(); 924 setMarqueeText(mPhoneticNameTextView, text, size); 925 mPhoneticNameTextView.setVisibility(VISIBLE); 926 } 927 } 928 929 /** 930 * Returns the text view for the phonetic name, creating it if necessary. 931 */ getPhoneticNameTextView()932 public TextView getPhoneticNameTextView() { 933 if (mPhoneticNameTextView == null) { 934 mPhoneticNameTextView = new TextView(getContext()); 935 mPhoneticNameTextView.setSingleLine(true); 936 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 937 mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 938 mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 939 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 940 mPhoneticNameTextView.setActivated(isActivated()); 941 mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); 942 addView(mPhoneticNameTextView); 943 } 944 return mPhoneticNameTextView; 945 } 946 947 /** 948 * Adds or updates a text view for the data label. 949 */ setLabel(CharSequence text)950 public void setLabel(CharSequence text) { 951 if (TextUtils.isEmpty(text)) { 952 if (mLabelView != null) { 953 mLabelView.setVisibility(View.GONE); 954 } 955 } else { 956 getLabelView(); 957 setMarqueeText(mLabelView, text); 958 mLabelView.setVisibility(VISIBLE); 959 } 960 } 961 962 /** 963 * Returns the text view for the data label, creating it if necessary. 964 */ getLabelView()965 public TextView getLabelView() { 966 if (mLabelView == null) { 967 mLabelView = new TextView(getContext()); 968 mLabelView.setSingleLine(true); 969 mLabelView.setEllipsize(getTextEllipsis()); 970 mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 971 if (mPhotoPosition == PhotoPosition.LEFT) { 972 mLabelView.setAllCaps(true); 973 mLabelView.setGravity(Gravity.END); 974 } else { 975 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 976 } 977 mLabelView.setActivated(isActivated()); 978 mLabelView.setId(R.id.cliv_label_textview); 979 addView(mLabelView); 980 } 981 return mLabelView; 982 } 983 984 /** 985 * Adds or updates a text view for the data element. 986 */ setData(char[] text, int size)987 public void setData(char[] text, int size) { 988 if (text == null || size == 0) { 989 if (mDataView != null) { 990 mDataView.setVisibility(View.GONE); 991 } 992 } else { 993 getDataView(); 994 setMarqueeText(mDataView, text, size); 995 mDataView.setVisibility(VISIBLE); 996 } 997 } 998 999 /** 1000 * Sets phone number for a list item. This takes care of number highlighting if the highlight 1001 * mask exists. 1002 */ setPhoneNumber(String text, String countryIso)1003 public void setPhoneNumber(String text, String countryIso) { 1004 if (text == null) { 1005 if (mDataView != null) { 1006 mDataView.setVisibility(View.GONE); 1007 } 1008 } else { 1009 getDataView(); 1010 1011 // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to 1012 // mDataView. Make sure that determination of the highlight sequences are done only 1013 // after number formatting. 1014 1015 // Sets phone number texts for display after highlighting it, if applicable. 1016 // CharSequence textToSet = text; 1017 final SpannableString textToSet = new SpannableString(text); 1018 1019 if (mNumberHighlightSequence.size() != 0) { 1020 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); 1021 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start, 1022 highlightSequence.end); 1023 } 1024 1025 setMarqueeText(mDataView, textToSet); 1026 mDataView.setVisibility(VISIBLE); 1027 1028 // We have a phone number as "mDataView" so make it always LTR and VIEW_START 1029 mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); 1030 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1031 } 1032 } 1033 setMarqueeText(TextView textView, char[] text, int size)1034 private void setMarqueeText(TextView textView, char[] text, int size) { 1035 if (getTextEllipsis() == TruncateAt.MARQUEE) { 1036 setMarqueeText(textView, new String(text, 0, size)); 1037 } else { 1038 textView.setText(text, 0, size); 1039 } 1040 } 1041 setMarqueeText(TextView textView, CharSequence text)1042 private void setMarqueeText(TextView textView, CharSequence text) { 1043 if (getTextEllipsis() == TruncateAt.MARQUEE) { 1044 // To show MARQUEE correctly (with END effect during non-active state), we need 1045 // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. 1046 final SpannableString spannable = new SpannableString(text); 1047 spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(), 1048 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1049 textView.setText(spannable); 1050 } else { 1051 textView.setText(text); 1052 } 1053 } 1054 1055 /** 1056 * Returns the {@link CheckBox} view, creating it if necessary. 1057 */ getCheckBox()1058 public CheckBox getCheckBox() { 1059 if (mCheckBox == null) { 1060 mCheckBox = new CheckBox(getContext()); 1061 // Make non-focusable, so the rest of the ContactListItemView can be clicked. 1062 mCheckBox.setFocusable(false); 1063 addView(mCheckBox); 1064 } 1065 return mCheckBox; 1066 } 1067 1068 /** 1069 * Returns the text view for the data text, creating it if necessary. 1070 */ getDataView()1071 public TextView getDataView() { 1072 if (mDataView == null) { 1073 mDataView = new TextView(getContext()); 1074 mDataView.setSingleLine(true); 1075 mDataView.setEllipsize(getTextEllipsis()); 1076 mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 1077 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1078 mDataView.setActivated(isActivated()); 1079 mDataView.setId(R.id.cliv_data_view); 1080 mDataView.setElegantTextHeight(false); 1081 addView(mDataView); 1082 } 1083 return mDataView; 1084 } 1085 1086 /** 1087 * Adds or updates a text view for the search snippet. 1088 */ setSnippet(String text)1089 public void setSnippet(String text) { 1090 if (TextUtils.isEmpty(text)) { 1091 if (mSnippetView != null) { 1092 mSnippetView.setVisibility(View.GONE); 1093 } 1094 } else { 1095 mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); 1096 mSnippetView.setVisibility(VISIBLE); 1097 if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { 1098 // Give the text-to-speech engine a hint that it's a phone number 1099 mSnippetView.setContentDescription(PhoneNumberUtils.createTtsSpannable(text)); 1100 } else { 1101 mSnippetView.setContentDescription(null); 1102 } 1103 } 1104 } 1105 1106 /** 1107 * Returns the text view for the search snippet, creating it if necessary. 1108 */ getSnippetView()1109 public TextView getSnippetView() { 1110 if (mSnippetView == null) { 1111 mSnippetView = new TextView(getContext()); 1112 mSnippetView.setSingleLine(true); 1113 mSnippetView.setEllipsize(getTextEllipsis()); 1114 mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1115 mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1116 mSnippetView.setActivated(isActivated()); 1117 addView(mSnippetView); 1118 } 1119 return mSnippetView; 1120 } 1121 1122 /** 1123 * Returns the text view for the status, creating it if necessary. 1124 */ getStatusView()1125 public TextView getStatusView() { 1126 if (mStatusView == null) { 1127 mStatusView = new TextView(getContext()); 1128 mStatusView.setSingleLine(true); 1129 mStatusView.setEllipsize(getTextEllipsis()); 1130 mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1131 mStatusView.setTextColor(mSecondaryTextColor); 1132 mStatusView.setActivated(isActivated()); 1133 mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1134 addView(mStatusView); 1135 } 1136 return mStatusView; 1137 } 1138 1139 /** 1140 * Adds or updates a text view for the status. 1141 */ setStatus(CharSequence text)1142 public void setStatus(CharSequence text) { 1143 if (TextUtils.isEmpty(text)) { 1144 if (mStatusView != null) { 1145 mStatusView.setVisibility(View.GONE); 1146 } 1147 } else { 1148 getStatusView(); 1149 setMarqueeText(mStatusView, text); 1150 mStatusView.setVisibility(VISIBLE); 1151 } 1152 } 1153 1154 /** 1155 * Adds or updates the presence icon view. 1156 */ setPresence(Drawable icon)1157 public void setPresence(Drawable icon) { 1158 if (icon != null) { 1159 if (mPresenceIcon == null) { 1160 mPresenceIcon = new ImageView(getContext()); 1161 addView(mPresenceIcon); 1162 } 1163 mPresenceIcon.setImageDrawable(icon); 1164 mPresenceIcon.setScaleType(ScaleType.CENTER); 1165 mPresenceIcon.setVisibility(View.VISIBLE); 1166 } else { 1167 if (mPresenceIcon != null) { 1168 mPresenceIcon.setVisibility(View.GONE); 1169 } 1170 } 1171 } 1172 getTextEllipsis()1173 private TruncateAt getTextEllipsis() { 1174 return TruncateAt.MARQUEE; 1175 } 1176 showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1177 public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) { 1178 CharSequence name = cursor.getString(nameColumnIndex); 1179 setDisplayName(name); 1180 1181 // Since the quick contact content description is derived from the display name and there is 1182 // no guarantee that when the quick contact is initialized the display name is already set, 1183 // do it here too. 1184 if (mQuickContact != null) { 1185 mQuickContact.setContentDescription(getContext().getString( 1186 R.string.description_quick_contact_for, mNameTextView.getText())); 1187 } 1188 } 1189 setDisplayName(CharSequence name, boolean highlight)1190 public void setDisplayName(CharSequence name, boolean highlight) { 1191 if (!TextUtils.isEmpty(name) && highlight) { 1192 clearHighlightSequences(); 1193 addNameHighlightSequence(0, name.length()); 1194 } 1195 setDisplayName(name); 1196 } 1197 setDisplayName(CharSequence name)1198 public void setDisplayName(CharSequence name) { 1199 if (!TextUtils.isEmpty(name)) { 1200 // Chooses the available highlighting method for highlighting. 1201 if (mHighlightedPrefix != null) { 1202 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); 1203 } else if (mNameHighlightSequence.size() != 0) { 1204 final SpannableString spannableName = new SpannableString(name); 1205 for (HighlightSequence highlightSequence : mNameHighlightSequence) { 1206 mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start, 1207 highlightSequence.end); 1208 } 1209 name = spannableName; 1210 } 1211 } else { 1212 name = mUnknownNameText; 1213 } 1214 setMarqueeText(getNameTextView(), name); 1215 1216 if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { 1217 // Give the text-to-speech engine a hint that it's a phone number 1218 mNameTextView.setContentDescription( 1219 PhoneNumberUtils.createTtsSpannable(name.toString())); 1220 } else { 1221 mNameTextView.setContentDescription(null); 1222 } 1223 } 1224 hideCheckBox()1225 public void hideCheckBox() { 1226 if (mCheckBox != null) { 1227 removeView(mCheckBox); 1228 mCheckBox = null; 1229 } 1230 } 1231 hideDisplayName()1232 public void hideDisplayName() { 1233 if (mNameTextView != null) { 1234 removeView(mNameTextView); 1235 mNameTextView = null; 1236 } 1237 } 1238 showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1239 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 1240 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 1241 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 1242 if (phoneticNameSize != 0) { 1243 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 1244 } else { 1245 setPhoneticName(null, 0); 1246 } 1247 } 1248 hidePhoneticName()1249 public void hidePhoneticName() { 1250 if (mPhoneticNameTextView != null) { 1251 removeView(mPhoneticNameTextView); 1252 mPhoneticNameTextView = null; 1253 } 1254 } 1255 1256 /** 1257 * Sets the proper icon (star or presence or nothing) and/or status message. 1258 */ showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1259 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, 1260 int contactStatusColumnIndex) { 1261 Drawable icon = null; 1262 int presence = 0; 1263 if (!cursor.isNull(presenceColumnIndex)) { 1264 presence = cursor.getInt(presenceColumnIndex); 1265 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); 1266 } 1267 setPresence(icon); 1268 1269 String statusMessage = null; 1270 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { 1271 statusMessage = cursor.getString(contactStatusColumnIndex); 1272 } 1273 // If there is no status message from the contact, but there was a presence value, then use 1274 // the default status message string 1275 if (statusMessage == null && presence != 0) { 1276 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); 1277 } 1278 setStatus(statusMessage); 1279 } 1280 1281 /** 1282 * Shows search snippet. 1283 */ showSnippet(Cursor cursor, int summarySnippetColumnIndex)1284 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 1285 if (cursor.getColumnCount() <= summarySnippetColumnIndex) { 1286 setSnippet(null); 1287 return; 1288 } 1289 1290 String snippet = cursor.getString(summarySnippetColumnIndex); 1291 1292 // Do client side snippeting if provider didn't do it 1293 final Bundle extras = cursor.getExtras(); 1294 if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { 1295 1296 final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); 1297 1298 String displayName = null; 1299 int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); 1300 if (displayNameIndex >= 0) { 1301 displayName = cursor.getString(displayNameIndex); 1302 } 1303 1304 snippet = updateSnippet(snippet, query, displayName); 1305 1306 } else { 1307 if (snippet != null) { 1308 int from = 0; 1309 int to = snippet.length(); 1310 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 1311 if (start == -1) { 1312 snippet = null; 1313 } else { 1314 int firstNl = snippet.lastIndexOf('\n', start); 1315 if (firstNl != -1) { 1316 from = firstNl + 1; 1317 } 1318 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 1319 if (end != -1) { 1320 int lastNl = snippet.indexOf('\n', end); 1321 if (lastNl != -1) { 1322 to = lastNl; 1323 } 1324 } 1325 1326 StringBuilder sb = new StringBuilder(); 1327 for (int i = from; i < to; i++) { 1328 char c = snippet.charAt(i); 1329 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 1330 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 1331 sb.append(c); 1332 } 1333 } 1334 snippet = sb.toString(); 1335 } 1336 } 1337 } 1338 1339 setSnippet(snippet); 1340 } 1341 1342 /** 1343 * Used for deferred snippets from the database. The contents come back as large strings which 1344 * need to be extracted for display. 1345 * 1346 * @param snippet The snippet from the database. 1347 * @param query The search query substring. 1348 * @param displayName The contact display name. 1349 * @return The proper snippet to display. 1350 */ updateSnippet(String snippet, String query, String displayName)1351 private String updateSnippet(String snippet, String query, String displayName) { 1352 1353 if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { 1354 return null; 1355 } 1356 query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); 1357 1358 // If the display name already contains the query term, return empty - snippets should 1359 // not be needed in that case. 1360 if (!TextUtils.isEmpty(displayName)) { 1361 final String lowerDisplayName = displayName.toLowerCase(); 1362 final List<String> nameTokens = split(lowerDisplayName); 1363 for (String nameToken : nameTokens) { 1364 if (nameToken.startsWith(query)) { 1365 return null; 1366 } 1367 } 1368 } 1369 1370 // The snippet may contain multiple data lines. 1371 // Show the first line that matches the query. 1372 final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); 1373 1374 if (matched != null && matched.line != null) { 1375 // Tokenize for long strings since the match may be at the end of it. 1376 // Skip this part for short strings since the whole string will be displayed. 1377 // Most contact strings are short so the snippetize method will be called infrequently. 1378 final int lengthThreshold = getResources().getInteger( 1379 R.integer.snippet_length_before_tokenize); 1380 if (matched.line.length() > lengthThreshold) { 1381 return snippetize(matched.line, matched.startIndex, lengthThreshold); 1382 } else { 1383 return matched.line; 1384 } 1385 } 1386 1387 // No match found. 1388 return null; 1389 } 1390 snippetize(String line, int matchIndex, int maxLength)1391 private String snippetize(String line, int matchIndex, int maxLength) { 1392 // Show up to maxLength characters. But we only show full tokens so show the last full token 1393 // up to maxLength characters. So as many starting tokens as possible before trying ending 1394 // tokens. 1395 int remainingLength = maxLength; 1396 int tempRemainingLength = remainingLength; 1397 1398 // Start the end token after the matched query. 1399 int index = matchIndex; 1400 int endTokenIndex = index; 1401 1402 // Find the match token first. 1403 while (index < line.length()) { 1404 if (!Character.isLetterOrDigit(line.charAt(index))) { 1405 endTokenIndex = index; 1406 remainingLength = tempRemainingLength; 1407 break; 1408 } 1409 tempRemainingLength--; 1410 index++; 1411 } 1412 1413 // Find as much content before the match. 1414 index = matchIndex - 1; 1415 tempRemainingLength = remainingLength; 1416 int startTokenIndex = matchIndex; 1417 while (index > -1 && tempRemainingLength > 0) { 1418 if (!Character.isLetterOrDigit(line.charAt(index))) { 1419 startTokenIndex = index; 1420 remainingLength = tempRemainingLength; 1421 } 1422 tempRemainingLength--; 1423 index--; 1424 } 1425 1426 index = endTokenIndex; 1427 tempRemainingLength = remainingLength; 1428 // Find remaining content at after match. 1429 while (index < line.length() && tempRemainingLength > 0) { 1430 if (!Character.isLetterOrDigit(line.charAt(index))) { 1431 endTokenIndex = index; 1432 } 1433 tempRemainingLength--; 1434 index++; 1435 } 1436 // Append ellipse if there is content before or after. 1437 final StringBuilder sb = new StringBuilder(); 1438 if (startTokenIndex > 0) { 1439 sb.append("..."); 1440 } 1441 sb.append(line.substring(startTokenIndex, endTokenIndex)); 1442 if (endTokenIndex < line.length()) { 1443 sb.append("..."); 1444 } 1445 return sb.toString(); 1446 } 1447 1448 private static final Pattern SPLIT_PATTERN = Pattern.compile( 1449 "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); 1450 1451 /** 1452 * Helper method for splitting a string into tokens. The lists passed in are populated with 1453 * the 1454 * tokens and offsets into the content of each token. The tokenization function parses e-mail 1455 * addresses as a single token; otherwise it splits on any non-alphanumeric character. 1456 * 1457 * @param content Content to split. 1458 * @return List of token strings. 1459 */ split(String content)1460 private static List<String> split(String content) { 1461 final Matcher matcher = SPLIT_PATTERN.matcher(content); 1462 final ArrayList<String> tokens = Lists.newArrayList(); 1463 while (matcher.find()) { 1464 tokens.add(matcher.group()); 1465 } 1466 return tokens; 1467 } 1468 1469 /** 1470 * Shows data element. 1471 */ showData(Cursor cursor, int dataColumnIndex)1472 public void showData(Cursor cursor, int dataColumnIndex) { 1473 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 1474 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 1475 } 1476 setActivatedStateSupported(boolean flag)1477 public void setActivatedStateSupported(boolean flag) { 1478 this.mActivatedStateSupported = flag; 1479 } 1480 setAdjustSelectionBoundsEnabled(boolean enabled)1481 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 1482 mAdjustSelectionBoundsEnabled = enabled; 1483 } 1484 1485 @Override requestLayout()1486 public void requestLayout() { 1487 // We will assume that once measured this will not need to resize 1488 // itself, so there is no need to pass the layout request to the parent 1489 // view (ListView). 1490 forceLayout(); 1491 } 1492 setPhotoPosition(PhotoPosition photoPosition)1493 public void setPhotoPosition(PhotoPosition photoPosition) { 1494 mPhotoPosition = photoPosition; 1495 } 1496 getPhotoPosition()1497 public PhotoPosition getPhotoPosition() { 1498 return mPhotoPosition; 1499 } 1500 1501 /** 1502 * Set drawable resources directly for the drawable resource of the photo view. 1503 * 1504 * @param drawableId Id of drawable resource. 1505 */ setDrawableResource(int drawableId)1506 public void setDrawableResource(int drawableId) { 1507 ImageView photo = getPhotoView(); 1508 photo.setScaleType(ImageView.ScaleType.CENTER); 1509 photo.setImageDrawable(getContext().getDrawable(drawableId)); 1510 photo.setImageTintList(ColorStateList.valueOf( 1511 getContext().getColor(R.color.search_shortcut_icon_color))); 1512 } 1513 1514 @Override onTouchEvent(MotionEvent event)1515 public boolean onTouchEvent(MotionEvent event) { 1516 final float x = event.getX(); 1517 final float y = event.getY(); 1518 // If the touch event's coordinates are not within the view's header, then delegate 1519 // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume 1520 // and ignore the touch event. 1521 if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { 1522 return super.onTouchEvent(event); 1523 } else { 1524 return true; 1525 } 1526 } 1527 pointIsInView(float localX, float localY)1528 private final boolean pointIsInView(float localX, float localY) { 1529 return localX >= mLeftOffset && localX < mRightOffset 1530 && localY >= 0 && localY < (getBottom() - getTop()); 1531 } 1532 } 1533