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