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