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