ContactListItemView.java revision 5181dff5240a207fd04e0c065d978535c3e8744f
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 com.android.contacts.ContactPresenceIconUtil; 20import com.android.contacts.R; 21import com.android.contacts.widget.TextWithHighlighting; 22import com.android.contacts.widget.TextWithHighlightingFactory; 23 24import android.content.Context; 25import android.content.res.TypedArray; 26import android.database.CharArrayBuffer; 27import android.database.Cursor; 28import android.graphics.Canvas; 29import android.graphics.Color; 30import android.graphics.Rect; 31import android.graphics.Typeface; 32import android.graphics.drawable.Drawable; 33import android.provider.ContactsContract.Contacts; 34import android.text.SpannableString; 35import android.text.TextUtils; 36import android.text.TextUtils.TruncateAt; 37import android.text.style.ForegroundColorSpan; 38import android.util.AttributeSet; 39import android.view.Gravity; 40import android.view.View; 41import android.view.ViewGroup; 42import android.widget.AbsListView.SelectionBoundsAdjuster; 43import android.widget.ImageView; 44import android.widget.ImageView.ScaleType; 45import android.widget.QuickContactBadge; 46import android.widget.TextView; 47 48/** 49 * A custom view for an item in the contact list. 50 */ 51public class ContactListItemView extends ViewGroup 52 implements SelectionBoundsAdjuster 53{ 54 55 private static final int QUICK_CONTACT_BADGE_STYLE = 56 com.android.internal.R.attr.quickContactBadgeStyleWindowMedium; 57 58 protected final Context mContext; 59 60 private final int mPreferredHeight; 61 private final int mVerticalDividerMargin; 62 private final int mPaddingTop; 63 private final int mPaddingRight; 64 private final int mPaddingBottom; 65 private final int mPaddingLeft; 66 private final int mGapBetweenImageAndText; 67 private final int mGapBetweenLabelAndData; 68 private final int mCallButtonPadding; 69 private final int mPresenceIconMargin; 70 private final int mPrefixHightlightColor; 71 private final int mHeaderTextColor; 72 private final int mHeaderTextIndent; 73 private final int mHeaderTextSize; 74 75 private Drawable mActivatedBackgroundDrawable; 76 77 private boolean mHorizontalDividerVisible = true; 78 private Drawable mHorizontalDividerDrawable; 79 private int mHorizontalDividerHeight; 80 81 private boolean mVerticalDividerVisible; 82 private Drawable mVerticalDividerDrawable; 83 private int mVerticalDividerWidth; 84 85 private boolean mHeaderVisible; 86 private Drawable mHeaderBackgroundDrawable; 87 private int mHeaderBackgroundHeight; 88 private TextView mHeaderTextView; 89 90 private boolean mQuickContactEnabled = true; 91 private QuickContactBadge mQuickContact; 92 private ImageView mPhotoView; 93 private TextView mNameTextView; 94 private TextView mPhoneticNameTextView; 95 private DontPressWithParentImageView mCallButton; 96 private TextView mLabelView; 97 private TextView mDataView; 98 private TextView mSnippetView; 99 private ImageView mPresenceIcon; 100 101 private char[] mHighlightedPrefix; 102 103 private int mDefaultPhotoViewSize; 104 private int mPhotoViewWidth; 105 private int mPhotoViewHeight; 106 private int mLine1Height; 107 private int mLine2Height; 108 private int mLine3Height; 109 private int mLine4Height; 110 111 private OnClickListener mCallButtonClickListener; 112 private TextWithHighlightingFactory mTextWithHighlightingFactory; 113 private CharArrayBuffer mNameBuffer = new CharArrayBuffer(128); 114 private CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); 115 private CharArrayBuffer mHighlightedTextBuffer = new CharArrayBuffer(128); 116 private TextWithHighlighting mTextWithHighlighting; 117 private CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); 118 119 private CharSequence mUnknownNameText; 120 121 private boolean mActivatedStateSupported; 122 123 private ForegroundColorSpan mPrefixColorSpan; 124 125 private Rect mBoundsWithoutHeader = new Rect(); 126 127 /** 128 * Special class to allow the parent to be pressed without being pressed itself. 129 * This way the line of a tab can be pressed, but the image itself is not. 130 */ 131 // TODO: understand this 132 private static class DontPressWithParentImageView extends ImageView { 133 134 public DontPressWithParentImageView(Context context, AttributeSet attrs) { 135 super(context, attrs); 136 } 137 138 @Override 139 public void setPressed(boolean pressed) { 140 // If the parent is pressed, do not set to pressed. 141 if (pressed && ((View) getParent()).isPressed()) { 142 return; 143 } 144 super.setPressed(pressed); 145 } 146 } 147 148 public ContactListItemView(Context context, AttributeSet attrs) { 149 super(context, attrs); 150 mContext = context; 151 152 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); 153 mPreferredHeight = a.getDimensionPixelSize( 154 R.styleable.ContactListItemView_list_item_height, 0); 155 mActivatedBackgroundDrawable = a.getDrawable( 156 R.styleable.ContactListItemView_activated_background); 157 mHeaderBackgroundDrawable = a.getDrawable( 158 R.styleable.ContactListItemView_section_header_background); 159 mHorizontalDividerDrawable = a.getDrawable( 160 R.styleable.ContactListItemView_list_item_divider); 161 mVerticalDividerMargin = a.getDimensionPixelOffset( 162 R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0); 163 mPaddingTop = a.getDimensionPixelOffset( 164 R.styleable.ContactListItemView_list_item_padding_top, 0); 165 mPaddingBottom = a.getDimensionPixelOffset( 166 R.styleable.ContactListItemView_list_item_padding_bottom, 0); 167 mPaddingLeft = a.getDimensionPixelOffset( 168 R.styleable.ContactListItemView_list_item_padding_left, 0); 169 mPaddingRight = a.getDimensionPixelOffset( 170 R.styleable.ContactListItemView_list_item_padding_right, 0); 171 mGapBetweenImageAndText = a.getDimensionPixelOffset( 172 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0); 173 mGapBetweenLabelAndData = a.getDimensionPixelOffset( 174 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0); 175 mCallButtonPadding = a.getDimensionPixelOffset( 176 R.styleable.ContactListItemView_list_item_call_button_padding, 0); 177 mPresenceIconMargin = a.getDimensionPixelOffset( 178 R.styleable.ContactListItemView_list_item_presence_icon_margin, 0); 179 mDefaultPhotoViewSize = a.getDimensionPixelOffset( 180 R.styleable.ContactListItemView_list_item_photo_size, 0); 181 mPrefixHightlightColor = a.getColor( 182 R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN); 183 184 mHeaderTextIndent = a.getDimensionPixelOffset( 185 R.styleable.ContactListItemView_list_item_header_text_indent, 0); 186 mHeaderTextColor = a.getColor( 187 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK); 188 mHeaderTextSize = a.getDimensionPixelSize( 189 R.styleable.ContactListItemView_list_item_header_text_size, 12); 190 191 a.recycle(); 192 193 mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight(); 194 mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight(); 195 196 if (mActivatedBackgroundDrawable != null) { 197 mActivatedBackgroundDrawable.setCallback(this); 198 } 199 } 200 201 /** 202 * Installs a call button listener. 203 */ 204 public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) { 205 mCallButtonClickListener = callButtonClickListener; 206 } 207 208 public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) { 209 mTextWithHighlightingFactory = factory; 210 } 211 212 public void setUnknownNameText(CharSequence unknownNameText) { 213 mUnknownNameText = unknownNameText; 214 } 215 216 public void setQuickContactEnabled(boolean flag) { 217 mQuickContactEnabled = flag; 218 } 219 220 @Override 221 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 222 // We will match parent's width and wrap content vertically, but make sure 223 // height is no less than listPreferredItemHeight. 224 int width = resolveSize(0, widthMeasureSpec); 225 int height = 0; 226 int preferredHeight = mPreferredHeight; 227 228 mLine1Height = 0; 229 mLine2Height = 0; 230 mLine3Height = 0; 231 mLine4Height = 0; 232 233 // Obtain the natural dimensions of the name text (we only care about height) 234 if (isVisible(mNameTextView)) { 235 mNameTextView.measure(0, 0); 236 mLine1Height = mNameTextView.getMeasuredHeight(); 237 } 238 239 if (isVisible(mPhoneticNameTextView)) { 240 mPhoneticNameTextView.measure(0, 0); 241 mLine2Height = mPhoneticNameTextView.getMeasuredHeight(); 242 } 243 244 if (isVisible(mLabelView)) { 245 mLabelView.measure(0, 0); 246 mLine3Height = mLabelView.getMeasuredHeight(); 247 } 248 249 if (isVisible(mDataView)) { 250 mDataView.measure(0, 0); 251 mLine3Height = Math.max(mLine3Height, mDataView.getMeasuredHeight()); 252 } 253 254 if (isVisible(mSnippetView)) { 255 mSnippetView.measure(0, 0); 256 mLine4Height = mSnippetView.getMeasuredHeight(); 257 } 258 259 height += mLine1Height + mLine2Height + mLine3Height + mLine4Height 260 + mPaddingTop + mPaddingBottom; 261 262 if (isVisible(mCallButton)) { 263 mCallButton.measure(0, 0); 264 } 265 if (isVisible(mPresenceIcon)) { 266 mPresenceIcon.measure(0, 0); 267 } 268 269 ensurePhotoViewSize(); 270 271 height = Math.max(height, mPhotoViewHeight + mPaddingBottom + mPaddingTop); 272 273 if (mHorizontalDividerVisible) { 274 height += mHorizontalDividerHeight; 275 preferredHeight += mHorizontalDividerHeight; 276 } 277 278 height = Math.max(height, preferredHeight); 279 280 if (mHeaderVisible) { 281 mHeaderTextView.measure( 282 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 283 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 284 height += mHeaderBackgroundHeight; 285 } 286 287 setMeasuredDimension(width, height); 288 } 289 290 @Override 291 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 292 int height = bottom - top; 293 int width = right - left; 294 295 // Determine the vertical bounds by laying out the header first. 296 int topBound = 0; 297 int bottomBound = height; 298 299 if (mHeaderVisible) { 300 mHeaderBackgroundDrawable.setBounds( 301 0, 302 0, 303 width, 304 mHeaderBackgroundHeight); 305 mHeaderTextView.layout(mHeaderTextIndent, 0, width, mHeaderBackgroundHeight); 306 topBound += mHeaderBackgroundHeight; 307 } 308 309 if (mHorizontalDividerVisible) { 310 mHorizontalDividerDrawable.setBounds( 311 0, 312 height - mHorizontalDividerHeight, 313 width, 314 height); 315 bottomBound -= mHorizontalDividerHeight; 316 } 317 318 mBoundsWithoutHeader.set(0, topBound, width, bottomBound); 319 320 if (mActivatedStateSupported) { 321 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 322 } 323 324 topBound += mPaddingTop; 325 bottomBound -= mPaddingBottom; 326 327 // Positions of views on the left are fixed and so are those on the right side. 328 // The stretchable part of the layout is in the middle. So, we will start off 329 // by laying out the left and right sides. Then we will allocate the remainder 330 // to the text fields in the middle. 331 332 int leftBound = layoutLeftSide(height, topBound, bottomBound, mPaddingLeft); 333 int rightBound = layoutRightSide(height, topBound, width); 334 335 // Text lines, centered vertically 336 rightBound -= mPaddingRight; 337 338 // Center text vertically 339 int totalTextHeight = mLine1Height + mLine2Height + mLine3Height + mLine4Height; 340 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2; 341 342 if (isVisible(mNameTextView)) { 343 mNameTextView.layout(leftBound, 344 textTopBound, 345 rightBound, 346 textTopBound + mLine1Height); 347 } 348 349 int dataLeftBound = leftBound; 350 if (isVisible(mPhoneticNameTextView)) { 351 mPhoneticNameTextView.layout(leftBound, 352 textTopBound + mLine1Height, 353 rightBound, 354 textTopBound + mLine1Height + mLine2Height); 355 } 356 357 if (isVisible(mLabelView)) { 358 dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); 359 mLabelView.layout(leftBound, 360 textTopBound + mLine1Height + mLine2Height, 361 dataLeftBound, 362 textTopBound + mLine1Height + mLine2Height + mLine3Height); 363 dataLeftBound += mGapBetweenLabelAndData; 364 } 365 366 if (isVisible(mDataView)) { 367 mDataView.layout(dataLeftBound, 368 textTopBound + mLine1Height + mLine2Height, 369 rightBound, 370 textTopBound + mLine1Height + mLine2Height + mLine3Height); 371 } 372 373 if (isVisible(mSnippetView)) { 374 mSnippetView.layout(leftBound, 375 textTopBound + mLine1Height + mLine2Height + mLine3Height, 376 rightBound, 377 textTopBound + mLine1Height + mLine2Height + mLine3Height + mLine4Height); 378 } 379 } 380 381 /** 382 * Performs layout of the left side of the view 383 * 384 * @return new left boundary 385 */ 386 protected int layoutLeftSide(int height, int topBound, int bottomBound, int leftBound) { 387 View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 388 if (photoView != null) { 389 // Center the photo vertically 390 int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 391 photoView.layout( 392 leftBound, 393 photoTop, 394 leftBound + mPhotoViewWidth, 395 photoTop + mPhotoViewHeight); 396 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 397 } 398 return leftBound; 399 } 400 401 /** 402 * Performs layout of the right side of the view 403 * 404 * @return new right boundary 405 */ 406 protected int layoutRightSide(int height, int topBound, int rightBound) { 407 if (isVisible(mCallButton)) { 408 int buttonWidth = mCallButton.getMeasuredWidth(); 409 rightBound -= buttonWidth; 410 mCallButton.layout( 411 rightBound, 412 topBound, 413 rightBound + buttonWidth, 414 height - mHorizontalDividerHeight); 415 mVerticalDividerVisible = true; 416 ensureVerticalDivider(); 417 rightBound -= mVerticalDividerWidth; 418 mVerticalDividerDrawable.setBounds( 419 rightBound, 420 topBound + mVerticalDividerMargin, 421 rightBound + mVerticalDividerWidth, 422 height - mVerticalDividerMargin); 423 } else { 424 mVerticalDividerVisible = false; 425 } 426 427 if (isVisible(mPresenceIcon)) { 428 int iconWidth = mPresenceIcon.getMeasuredWidth(); 429 rightBound -= mPresenceIconMargin + iconWidth; 430 mPresenceIcon.layout( 431 rightBound, 432 topBound, 433 rightBound + iconWidth, 434 height); 435 } 436 return rightBound; 437 } 438 439 @Override 440 public void adjustListItemSelectionBounds(Rect bounds) { 441 bounds.top += mBoundsWithoutHeader.top; 442 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 443 } 444 445 protected boolean isVisible(View view) { 446 return view != null && view.getVisibility() == View.VISIBLE; 447 } 448 449 /** 450 * Loads the drawable for the vertical divider if it has not yet been loaded. 451 */ 452 private void ensureVerticalDivider() { 453 if (mVerticalDividerDrawable == null) { 454 mVerticalDividerDrawable = mContext.getResources().getDrawable( 455 R.drawable.divider_vertical_dark); 456 mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth(); 457 } 458 } 459 460 /** 461 * Extracts width and height from the style 462 */ 463 private void ensurePhotoViewSize() { 464 if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) { 465 if (mQuickContactEnabled) { 466 TypedArray a = mContext.obtainStyledAttributes(null, 467 com.android.internal.R.styleable.ViewGroup_Layout, 468 QUICK_CONTACT_BADGE_STYLE, 0); 469 mPhotoViewWidth = a.getLayoutDimension( 470 android.R.styleable.ViewGroup_Layout_layout_width, 471 ViewGroup.LayoutParams.WRAP_CONTENT); 472 mPhotoViewHeight = a.getLayoutDimension( 473 android.R.styleable.ViewGroup_Layout_layout_height, 474 ViewGroup.LayoutParams.WRAP_CONTENT); 475 a.recycle(); 476 } else { 477 mPhotoViewWidth = mPhotoViewHeight = mDefaultPhotoViewSize; 478 } 479 } 480 } 481 482 @Override 483 protected void drawableStateChanged() { 484 super.drawableStateChanged(); 485 if (mActivatedStateSupported) { 486 mActivatedBackgroundDrawable.setState(getDrawableState()); 487 } 488 } 489 490 @Override 491 protected boolean verifyDrawable(Drawable who) { 492 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 493 } 494 495 @Override 496 public void jumpDrawablesToCurrentState() { 497 super.jumpDrawablesToCurrentState(); 498 if (mActivatedStateSupported) { 499 mActivatedBackgroundDrawable.jumpToCurrentState(); 500 } 501 } 502 503 @Override 504 public void dispatchDraw(Canvas canvas) { 505 if (mActivatedStateSupported) { 506 mActivatedBackgroundDrawable.draw(canvas); 507 } 508 if (mHeaderVisible) { 509 mHeaderBackgroundDrawable.draw(canvas); 510 } 511 if (mHorizontalDividerVisible) { 512 mHorizontalDividerDrawable.draw(canvas); 513 } 514 if (mVerticalDividerVisible) { 515 mVerticalDividerDrawable.draw(canvas); 516 } 517 518 super.dispatchDraw(canvas); 519 } 520 521 /** 522 * Sets the flag that determines whether a divider should drawn at the bottom 523 * of the view. 524 */ 525 public void setDividerVisible(boolean visible) { 526 mHorizontalDividerVisible = visible; 527 } 528 529 /** 530 * Sets section header or makes it invisible if the title is null. 531 */ 532 public void setSectionHeader(String title) { 533 if (!TextUtils.isEmpty(title)) { 534 if (mHeaderTextView == null) { 535 mHeaderTextView = new TextView(mContext); 536 mHeaderTextView.setTextColor(mHeaderTextColor); 537 mHeaderTextView.setTextSize(mHeaderTextSize); 538 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD); 539 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL); 540 addView(mHeaderTextView); 541 } 542 mHeaderTextView.setText(title); 543 mHeaderTextView.setVisibility(View.VISIBLE); 544 mHeaderVisible = true; 545 } else { 546 if (mHeaderTextView != null) { 547 mHeaderTextView.setVisibility(View.GONE); 548 } 549 mHeaderVisible = false; 550 } 551 } 552 553 /** 554 * Returns the quick contact badge, creating it if necessary. 555 */ 556 public QuickContactBadge getQuickContact() { 557 if (!mQuickContactEnabled) { 558 throw new IllegalStateException("QuickContact is disabled for this view"); 559 } 560 if (mQuickContact == null) { 561 mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE); 562 mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE }); 563 addView(mQuickContact); 564 } 565 return mQuickContact; 566 } 567 568 /** 569 * Returns the photo view, creating it if necessary. 570 */ 571 public ImageView getPhotoView() { 572 if (mPhotoView == null) { 573 if (mQuickContactEnabled) { 574 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE); 575 } else { 576 mPhotoView = new ImageView(mContext); 577 } 578 // Quick contact style used above will set a background - remove it 579 mPhotoView.setBackgroundDrawable(null); 580 addView(mPhotoView); 581 } 582 return mPhotoView; 583 } 584 585 /** 586 * Removes the photo view. Should not be needed once we start handling different 587 * types of views as different types of views from the List's perspective. 588 */ 589 public void removePhotoView() { 590 if (mPhotoView != null) { 591 removeView(mPhotoView); 592 mPhotoView = null; 593 } 594 if (mQuickContact != null) { 595 removeView(mQuickContact); 596 mQuickContact = null; 597 } 598 } 599 600 /** 601 * Sets a word prefix that will be highlighted if encountered in fields like 602 * name and search snippet. 603 * <p> 604 * NOTE: must be all upper-case 605 */ 606 public void setHighlightedPrefix(char[] upperCasePrefix) { 607 mHighlightedPrefix = upperCasePrefix; 608 } 609 610 /** 611 * Returns the text view for the contact name, creating it if necessary. 612 */ 613 public TextView getNameTextView() { 614 if (mNameTextView == null) { 615 mNameTextView = new TextView(mContext); 616 mNameTextView.setSingleLine(true); 617 mNameTextView.setEllipsize(getTextEllipsis()); 618 mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); 619 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 620 addView(mNameTextView); 621 } 622 return mNameTextView; 623 } 624 625 /** 626 * Adds a call button using the supplied arguments as an id and tag. 627 */ 628 public void showCallButton(int id, int tag) { 629 if (mCallButton == null) { 630 mCallButton = new DontPressWithParentImageView(mContext, null); 631 mCallButton.setId(id); 632 mCallButton.setOnClickListener(mCallButtonClickListener); 633 mCallButton.setBackgroundResource(R.drawable.call_background); 634 mCallButton.setImageResource(android.R.drawable.sym_action_call); 635 mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0); 636 mCallButton.setScaleType(ScaleType.CENTER); 637 addView(mCallButton); 638 } 639 640 mCallButton.setTag(tag); 641 mCallButton.setVisibility(View.VISIBLE); 642 } 643 644 public void hideCallButton() { 645 if (mCallButton != null) { 646 mCallButton.setVisibility(View.GONE); 647 } 648 } 649 650 /** 651 * Adds or updates a text view for the phonetic name. 652 */ 653 public void setPhoneticName(char[] text, int size) { 654 if (text == null || size == 0) { 655 if (mPhoneticNameTextView != null) { 656 mPhoneticNameTextView.setVisibility(View.GONE); 657 } 658 } else { 659 getPhoneticNameTextView(); 660 mPhoneticNameTextView.setText(text, 0, size); 661 mPhoneticNameTextView.setVisibility(VISIBLE); 662 } 663 } 664 665 /** 666 * Returns the text view for the phonetic name, creating it if necessary. 667 */ 668 public TextView getPhoneticNameTextView() { 669 if (mPhoneticNameTextView == null) { 670 mPhoneticNameTextView = new TextView(mContext); 671 mPhoneticNameTextView.setSingleLine(true); 672 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 673 mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 674 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 675 addView(mPhoneticNameTextView); 676 } 677 return mPhoneticNameTextView; 678 } 679 680 /** 681 * Adds or updates a text view for the data label. 682 */ 683 public void setLabel(CharSequence text) { 684 if (TextUtils.isEmpty(text)) { 685 if (mLabelView != null) { 686 mLabelView.setVisibility(View.GONE); 687 } 688 } else { 689 getLabelView(); 690 mLabelView.setText(text); 691 mLabelView.setVisibility(VISIBLE); 692 } 693 } 694 695 /** 696 * Adds or updates a text view for the data label. 697 */ 698 public void setLabel(char[] text, int size) { 699 if (text == null || size == 0) { 700 if (mLabelView != null) { 701 mLabelView.setVisibility(View.GONE); 702 } 703 } else { 704 getLabelView(); 705 mLabelView.setText(text, 0, size); 706 mLabelView.setVisibility(VISIBLE); 707 } 708 } 709 710 /** 711 * Returns the text view for the data label, creating it if necessary. 712 */ 713 public TextView getLabelView() { 714 if (mLabelView == null) { 715 mLabelView = new TextView(mContext); 716 mLabelView.setSingleLine(true); 717 mLabelView.setEllipsize(getTextEllipsis()); 718 mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 719 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 720 addView(mLabelView); 721 } 722 return mLabelView; 723 } 724 725 /** 726 * Adds or updates a text view for the data element. 727 */ 728 public void setData(char[] text, int size) { 729 if (text == null || size == 0) { 730 if (mDataView != null) { 731 mDataView.setVisibility(View.GONE); 732 } 733 return; 734 } else { 735 getDataView(); 736 mDataView.setText(text, 0, size); 737 mDataView.setVisibility(VISIBLE); 738 } 739 } 740 741 /** 742 * Returns the text view for the data text, creating it if necessary. 743 */ 744 public TextView getDataView() { 745 if (mDataView == null) { 746 mDataView = new TextView(mContext); 747 mDataView.setSingleLine(true); 748 mDataView.setEllipsize(getTextEllipsis()); 749 mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 750 addView(mDataView); 751 } 752 return mDataView; 753 } 754 755 /** 756 * Adds or updates a text view for the search snippet. 757 */ 758 public void setSnippet(String text) { 759 if (TextUtils.isEmpty(text)) { 760 if (mSnippetView != null) { 761 mSnippetView.setVisibility(View.GONE); 762 } 763 } else { 764 getSnippetView(); 765 setTextWithPrefixHighlighting(mSnippetView, text); 766 mSnippetView.setVisibility(VISIBLE); 767 } 768 } 769 770 /** 771 * Returns the text view for the search snippet, creating it if necessary. 772 */ 773 public TextView getSnippetView() { 774 if (mSnippetView == null) { 775 mSnippetView = new TextView(mContext); 776 mSnippetView.setSingleLine(true); 777 mSnippetView.setEllipsize(getTextEllipsis()); 778 mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 779 mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD); 780 addView(mSnippetView); 781 } 782 return mSnippetView; 783 } 784 785 /** 786 * Adds or updates the presence icon view. 787 */ 788 public void setPresence(Drawable icon) { 789 if (icon != null) { 790 if (mPresenceIcon == null) { 791 mPresenceIcon = new ImageView(mContext); 792 addView(mPresenceIcon); 793 } 794 mPresenceIcon.setImageDrawable(icon); 795 mPresenceIcon.setScaleType(ScaleType.CENTER); 796 mPresenceIcon.setVisibility(View.VISIBLE); 797 } else { 798 if (mPresenceIcon != null) { 799 mPresenceIcon.setVisibility(View.GONE); 800 } 801 } 802 } 803 804 private TruncateAt getTextEllipsis() { 805 return mActivatedStateSupported ? TruncateAt.START : TruncateAt.MARQUEE; 806 } 807 808 public void showDisplayName(Cursor cursor, int nameColumnIndex, boolean highlightingEnabled, 809 int alternativeNameColumnIndex) { 810 cursor.copyStringToBuffer(nameColumnIndex, mNameBuffer); 811 TextView nameView = getNameTextView(); 812 int size = mNameBuffer.sizeCopied; 813 if (size != 0) { 814 if (mHighlightedPrefix != null) { 815 setTextWithPrefixHighlighting(nameView, mNameBuffer); 816 } else if (highlightingEnabled) { 817 if (mTextWithHighlighting == null) { 818 mTextWithHighlighting = 819 mTextWithHighlightingFactory.createTextWithHighlighting(); 820 } 821 cursor.copyStringToBuffer(alternativeNameColumnIndex, mHighlightedTextBuffer); 822 mTextWithHighlighting.setText(mNameBuffer, mHighlightedTextBuffer); 823 nameView.setText(mTextWithHighlighting); 824 } else { 825 nameView.setText(mNameBuffer.data, 0, size); 826 } 827 } else { 828 nameView.setText(mUnknownNameText); 829 } 830 } 831 832 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 833 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 834 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 835 if (phoneticNameSize != 0) { 836 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 837 } else { 838 setPhoneticName(null, 0); 839 } 840 } 841 842 /** 843 * Sets the proper icon (star or presence or nothing) 844 */ 845 public void showPresence(Cursor cursor, int presenceColumnIndex, int capabilityColumnIndex) { 846 Drawable icon = null; 847 if (!cursor.isNull(presenceColumnIndex)) { 848 int status = cursor.getInt(presenceColumnIndex); 849 int chatCapability = 0; 850 if (capabilityColumnIndex != 0 && !cursor.isNull(presenceColumnIndex)) { 851 chatCapability = cursor.getInt(capabilityColumnIndex); 852 } 853 icon = ContactPresenceIconUtil.getChatCapabilityIcon( 854 getContext(), status, chatCapability); 855 } 856 setPresence(icon); 857 } 858 859 /** 860 * Shows search snippet. 861 */ 862 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 863 if (cursor.getColumnCount() <= summarySnippetColumnIndex) { 864 setSnippet(null); 865 return; 866 } 867 868 String snippet = cursor.getString(summarySnippetColumnIndex); 869 if (snippet != null) { 870 int from = 0; 871 int to = snippet.length(); 872 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 873 if (start != -1) { 874 int firstNl = snippet.lastIndexOf('\n', start); 875 if (firstNl != -1) { 876 from = firstNl + 1; 877 } 878 } 879 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 880 if (end != -1) { 881 int lastNl = snippet.indexOf('\n', end); 882 if (lastNl != -1) { 883 to = lastNl; 884 } 885 } 886 887 StringBuilder sb = new StringBuilder(); 888 for (int i = from; i < to; i++) { 889 char c = snippet.charAt(i); 890 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 891 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 892 sb.append(c); 893 } 894 } 895 snippet = sb.toString(); 896 } 897 setSnippet(snippet); 898 } 899 900 /** 901 * Shows data element (e.g. phone number). 902 */ 903 public void showData(Cursor cursor, int dataColumnIndex) { 904 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 905 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 906 } 907 908 public void setActivatedStateSupported(boolean flag) { 909 this.mActivatedStateSupported = flag; 910 } 911 912 /** 913 * Sets text on the given text view, highlighting the word that matches 914 * the given prefix (see {@link #setHighlightedPrefix}). 915 */ 916 private void setTextWithPrefixHighlighting(TextView textView, String text) { 917 mHighlightedTextBuffer.sizeCopied = 918 Math.min(text.length(), mHighlightedTextBuffer.data.length); 919 text.getChars(0, mHighlightedTextBuffer.sizeCopied, mHighlightedTextBuffer.data, 0); 920 setTextWithPrefixHighlighting(textView, mHighlightedTextBuffer); 921 } 922 923 /** 924 * Sets text on the given text view, highlighting the word that matches 925 * the given prefix (see {@link #setHighlightedPrefix}). 926 */ 927 private void setTextWithPrefixHighlighting(TextView textView, CharArrayBuffer text) { 928 int index = indexOfWordPrefix(text, mHighlightedPrefix); 929 if (index != -1) { 930 if (mPrefixColorSpan == null) { 931 mPrefixColorSpan = new ForegroundColorSpan(mPrefixHightlightColor); 932 } 933 934 String string = new String(text.data, 0, text.sizeCopied); 935 SpannableString name = new SpannableString(string); 936 name.setSpan(mPrefixColorSpan, index, index + mHighlightedPrefix.length, 0 /* flags */); 937 textView.setText(name); 938 } else { 939 textView.setText(text.data, 0, text.sizeCopied); 940 } 941 } 942 943 /** 944 * Finds the index of the word that starts with the given prefix. If not found, 945 * returns -1. 946 */ 947 private int indexOfWordPrefix(CharArrayBuffer buffer, char[] prefix) { 948 if (prefix == null || prefix.length == 0) { 949 return -1; 950 } 951 952 char[] string1 = buffer.data; 953 int count1 = buffer.sizeCopied; 954 int count2 = prefix.length; 955 956 int size = count2; 957 int i = 0; 958 while (i < count1) { 959 960 // Skip non-word characters 961 while (i < count1 && !Character.isLetterOrDigit(string1[i])) { 962 i++; 963 } 964 965 if (i + size > count1) { 966 return -1; 967 } 968 969 // Compare the prefixes 970 int j; 971 for (j = 0; j < size; j++) { 972 if (Character.toUpperCase(string1[i+j]) != prefix[j]) { 973 break; 974 } 975 } 976 if (j == size) { 977 return i; 978 } 979 980 // Skip this word 981 while (i < count1 && Character.isLetterOrDigit(string1[i])) { 982 i++; 983 } 984 } 985 986 return -1; 987 } 988 989 @Override 990 public void requestLayout() { 991 // We will assume that once measured this will not need to resize 992 // itself, so there is no need to pass the layout request to the parent 993 // view (ListView). 994 forceLayout(); 995 } 996} 997