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