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