ContactListItemView.java revision 64ef8131aab057fa37fa5f6d63b7296d2fda7fb6
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.ContactStatusUtil; 21import com.android.contacts.R; 22import com.android.contacts.format.DisplayNameFormatter; 23import com.android.contacts.format.PrefixHighlighter; 24import com.android.contacts.widget.TextWithHighlightingFactory; 25 26import android.content.Context; 27import android.content.res.ColorStateList; 28import android.content.res.TypedArray; 29import android.database.CharArrayBuffer; 30import android.database.Cursor; 31import android.graphics.Canvas; 32import android.graphics.Color; 33import android.graphics.Rect; 34import android.graphics.Typeface; 35import android.graphics.drawable.Drawable; 36import android.text.TextUtils; 37import android.text.TextUtils.TruncateAt; 38import android.util.AttributeSet; 39import android.util.TypedValue; 40import android.view.Gravity; 41import android.view.View; 42import android.view.ViewGroup; 43import android.widget.AbsListView.SelectionBoundsAdjuster; 44import android.widget.ImageView; 45import android.widget.ImageView.ScaleType; 46import android.widget.QuickContactBadge; 47import android.widget.TextView; 48 49/** 50 * A custom view for an item in the contact list. 51 * The view contains the contact's photo, a set of text views (for name, status, etc...) and 52 * icons for presence and call. 53 * The view uses no XML file for layout and all the measurements and layouts are done 54 * in the onMeasure and onLayout methods. 55 * 56 * The layout puts the contact's photo on the right side of the view, the call icon (if present) 57 * to the left of the photo, the text lines are aligned to the left and the presence icon (if 58 * present) is set to the left of the status line. 59 * 60 * The layout also supports a header (used as a header of a group of contacts) that is above the 61 * contact's data and a divider between contact view. 62 */ 63 64public class ContactListItemView extends ViewGroup 65 implements SelectionBoundsAdjuster { 66 67 private static final int QUICK_CONTACT_BADGE_STYLE = 68 com.android.internal.R.attr.quickContactBadgeStyleWindowMedium; 69 70 protected final Context mContext; 71 72 // Style values for layout and appearance 73 private final int mPreferredHeight; 74 private final int mVerticalDividerMargin; 75 private final int mPaddingTop; 76 private final int mPaddingRight; 77 private final int mPaddingBottom; 78 private final int mPaddingLeft; 79 private final int mGapBetweenImageAndText; 80 private final int mGapBetweenLabelAndData; 81 private final int mCallButtonPadding; 82 private final int mPresenceIconMargin; 83 private final int mPresenceIconSize; 84 private final int mHeaderTextColor; 85 private final int mHeaderTextIndent; 86 private final int mHeaderTextSize; 87 private final int mHeaderUnderlineHeight; 88 private final int mHeaderUnderlineColor; 89 private final int mCountViewTextSize; 90 private final int mContactsCountTextColor; 91 private final int mTextIndent; 92 private Drawable mActivatedBackgroundDrawable; 93 94 // Horizontal divider between contact views. 95 private boolean mHorizontalDividerVisible = true; 96 private Drawable mHorizontalDividerDrawable; 97 private int mHorizontalDividerHeight; 98 99 // Vertical divider between the call icon and the text. 100 private boolean mVerticalDividerVisible; 101 private Drawable mVerticalDividerDrawable; 102 private int mVerticalDividerWidth; 103 104 // Header layout data 105 private boolean mHeaderVisible; 106 private View mHeaderDivider; 107 private int mHeaderBackgroundHeight; 108 private TextView mHeaderTextView; 109 110 // The views inside the contact view 111 private boolean mQuickContactEnabled = true; 112 private QuickContactBadge mQuickContact; 113 private ImageView mPhotoView; 114 private TextView mNameTextView; 115 private TextView mPhoneticNameTextView; 116 private DontPressWithParentImageView mCallButton; 117 private TextView mLabelView; 118 private TextView mDataView; 119 private TextView mSnippetView; 120 private TextView mStatusView; 121 private TextView mCountView; 122 private ImageView mPresenceIcon; 123 124 private ColorStateList mPrimaryTextColor; 125 private ColorStateList mSecondaryTextColor; 126 127 private char[] mHighlightedPrefix; 128 129 private int mDefaultPhotoViewSize; 130 /** 131 * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding 132 * to align other data in this View. 133 */ 134 private int mPhotoViewWidth; 135 /** 136 * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. 137 */ 138 private int mPhotoViewHeight; 139 140 /** 141 * Only effective when {@link #mPhotoView} is null. 142 * When true all the Views on the right side of the photo should have horizontal padding on 143 * those left assuming there is a photo. 144 */ 145 private boolean mKeepHorizontalPaddingForPhotoView; 146 /** 147 * Only effective when {@link #mPhotoView} is null. 148 */ 149 private boolean mKeepVerticalPaddingForPhotoView; 150 151 /** 152 * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. 153 * False indicates those values should be updated before being used in position calculation. 154 */ 155 private boolean mPhotoViewWidthAndHeightAreReady = false; 156 157 private int mNameTextViewHeight; 158 private int mPhoneticNameTextViewHeight; 159 private int mLabelTextViewHeight; 160 private int mSnippetTextViewHeight; 161 private int mStatusTextViewHeight; 162 163 private OnClickListener mCallButtonClickListener; 164 private CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); 165 private CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); 166 167 private boolean mActivatedStateSupported; 168 169 private Rect mBoundsWithoutHeader = new Rect(); 170 171 /** A helper used to highlight a prefix in a text field. */ 172 private PrefixHighlighter mPrefixHighligher; 173 /** A helper used to format display names. */ 174 private DisplayNameFormatter mDisplayNameFormatter; 175 176 /** 177 * Special class to allow the parent to be pressed without being pressed itself. 178 * This way the line of a tab can be pressed, but the image itself is not. 179 */ 180 // TODO: understand this 181 private static class DontPressWithParentImageView extends ImageView { 182 183 public DontPressWithParentImageView(Context context, AttributeSet attrs) { 184 super(context, attrs); 185 } 186 187 @Override 188 public void setPressed(boolean pressed) { 189 // If the parent is pressed, do not set to pressed. 190 if (pressed && ((View) getParent()).isPressed()) { 191 return; 192 } 193 super.setPressed(pressed); 194 } 195 } 196 197 public ContactListItemView(Context context, AttributeSet attrs) { 198 super(context, attrs); 199 mContext = context; 200 201 // Read all style values 202 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); 203 mPreferredHeight = a.getDimensionPixelSize( 204 R.styleable.ContactListItemView_list_item_height, 0); 205 mActivatedBackgroundDrawable = a.getDrawable( 206 R.styleable.ContactListItemView_activated_background); 207 mHorizontalDividerDrawable = a.getDrawable( 208 R.styleable.ContactListItemView_list_item_divider); 209 mVerticalDividerMargin = a.getDimensionPixelOffset( 210 R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0); 211 mPaddingTop = a.getDimensionPixelOffset( 212 R.styleable.ContactListItemView_list_item_padding_top, 0); 213 mPaddingBottom = a.getDimensionPixelOffset( 214 R.styleable.ContactListItemView_list_item_padding_bottom, 0); 215 mPaddingLeft = a.getDimensionPixelOffset( 216 R.styleable.ContactListItemView_list_item_padding_left, 0); 217 mPaddingRight = a.getDimensionPixelOffset( 218 R.styleable.ContactListItemView_list_item_padding_right, 0); 219 mGapBetweenImageAndText = a.getDimensionPixelOffset( 220 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0); 221 mGapBetweenLabelAndData = a.getDimensionPixelOffset( 222 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0); 223 mCallButtonPadding = a.getDimensionPixelOffset( 224 R.styleable.ContactListItemView_list_item_call_button_padding, 0); 225 mPresenceIconMargin = a.getDimensionPixelOffset( 226 R.styleable.ContactListItemView_list_item_presence_icon_margin, 4); 227 mPresenceIconSize = a.getDimensionPixelOffset( 228 R.styleable.ContactListItemView_list_item_presence_icon_size, 16); 229 mDefaultPhotoViewSize = a.getDimensionPixelOffset( 230 R.styleable.ContactListItemView_list_item_photo_size, 0); 231 mHeaderTextIndent = a.getDimensionPixelOffset( 232 R.styleable.ContactListItemView_list_item_header_text_indent, 0); 233 mHeaderTextColor = a.getColor( 234 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK); 235 mHeaderTextSize = a.getDimensionPixelSize( 236 R.styleable.ContactListItemView_list_item_header_text_size, 12); 237 mHeaderBackgroundHeight = a.getDimensionPixelSize( 238 R.styleable.ContactListItemView_list_item_header_height, 30); 239 mHeaderUnderlineHeight = a.getDimensionPixelSize( 240 R.styleable.ContactListItemView_list_item_header_underline_height, 1); 241 mHeaderUnderlineColor = a.getColor( 242 R.styleable.ContactListItemView_list_item_header_underline_color, 0); 243 mTextIndent = a.getDimensionPixelOffset( 244 R.styleable.ContactListItemView_list_item_text_indent, 0); 245 mCountViewTextSize = a.getDimensionPixelSize( 246 R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12); 247 mContactsCountTextColor = a.getColor( 248 R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK); 249 250 mPrefixHighligher = new PrefixHighlighter( 251 a.getColor(R.styleable.ContactListItemView_list_item_prefix_highlight_color, 252 Color.GREEN)); 253 a.recycle(); 254 255 mPrimaryTextColor = getResources().getColorStateList(R.color.list_primary_text_color); 256 mSecondaryTextColor = getResources().getColorStateList(R.color.list_secondary_text_color); 257 258 mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight(); 259 260 if (mActivatedBackgroundDrawable != null) { 261 mActivatedBackgroundDrawable.setCallback(this); 262 } 263 264 mDisplayNameFormatter = new DisplayNameFormatter(mPrefixHighligher); 265 } 266 267 /** 268 * Installs a call button listener. 269 */ 270 public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) { 271 mCallButtonClickListener = callButtonClickListener; 272 } 273 274 public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) { 275 mDisplayNameFormatter.setTextWithHighlightingFactory(factory); 276 } 277 278 public void setUnknownNameText(CharSequence unknownNameText) { 279 mDisplayNameFormatter.setUnknownNameText(unknownNameText); 280 } 281 282 public void setQuickContactEnabled(boolean flag) { 283 mQuickContactEnabled = flag; 284 } 285 286 @Override 287 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 288 // We will match parent's width and wrap content vertically, but make sure 289 // height is no less than listPreferredItemHeight. 290 int width = resolveSize(0, widthMeasureSpec); 291 int height = 0; 292 int preferredHeight = mPreferredHeight; 293 294 mNameTextViewHeight = 0; 295 mPhoneticNameTextViewHeight = 0; 296 mLabelTextViewHeight = 0; 297 mSnippetTextViewHeight = 0; 298 mStatusTextViewHeight = 0; 299 300 // Go over all visible text views and add their heights to get the total height 301 if (isVisible(mNameTextView)) { 302 mNameTextView.measure(0, 0); 303 mNameTextViewHeight = mNameTextView.getMeasuredHeight(); 304 } 305 306 if (isVisible(mPhoneticNameTextView)) { 307 mPhoneticNameTextView.measure(0, 0); 308 mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight(); 309 } 310 311 if (isVisible(mLabelView)) { 312 mLabelView.measure(0, 0); 313 mLabelTextViewHeight = mLabelView.getMeasuredHeight(); 314 } 315 316 // Label view height is the biggest of the label text view and the data text view 317 if (isVisible(mDataView)) { 318 mDataView.measure(0, 0); 319 mLabelTextViewHeight = Math.max(mLabelTextViewHeight, mDataView.getMeasuredHeight()); 320 } 321 322 if (isVisible(mSnippetView)) { 323 mSnippetView.measure(0, 0); 324 mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); 325 } 326 327 // Status view height is the biggest of the text view and the presence icon 328 if (isVisible(mPresenceIcon)) { 329 mPresenceIcon.measure(mPresenceIconSize, mPresenceIconSize); 330 mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); 331 } 332 333 if (isVisible(mStatusView)) { 334 mStatusView.measure(0, 0); 335 mStatusTextViewHeight = Math.max(mStatusTextViewHeight, 336 mStatusView.getMeasuredHeight()); 337 } 338 339 // Calculate height including padding 340 height += mNameTextViewHeight + mPhoneticNameTextViewHeight + mLabelTextViewHeight + 341 mSnippetTextViewHeight + mStatusTextViewHeight + mPaddingTop + mPaddingBottom; 342 343 if (isVisible(mCallButton)) { 344 mCallButton.measure(0, 0); 345 } 346 347 // Make sure the height is at least as high as the photo 348 ensurePhotoViewSize(); 349 height = Math.max(height, mPhotoViewHeight + mPaddingBottom + mPaddingTop); 350 351 // Add horizontal divider height 352 if (mHorizontalDividerVisible) { 353 height += mHorizontalDividerHeight; 354 preferredHeight += mHorizontalDividerHeight; 355 } 356 357 // Make sure height is at least the preferred height 358 height = Math.max(height, preferredHeight); 359 360 // Add the height of the header if visible 361 if (mHeaderVisible) { 362 mHeaderTextView.measure( 363 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 364 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 365 if (mCountView != null) { 366 mCountView.measure( 367 MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 368 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 369 } 370 mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight, 371 mHeaderTextView.getMeasuredHeight()); 372 height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight); 373 } 374 375 setMeasuredDimension(width, height); 376 } 377 378 @Override 379 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 380 int height = bottom - top; 381 int width = right - left; 382 383 // Determine the vertical bounds by laying out the header first. 384 int topBound = 0; 385 int bottomBound = height; 386 int leftBound = mPaddingLeft; 387 388 // Put the header in the top of the contact view (Text + underline view) 389 if (mHeaderVisible) { 390 mHeaderTextView.layout(leftBound + mHeaderTextIndent, 391 0, 392 width - mPaddingRight, 393 mHeaderBackgroundHeight); 394 if (mCountView != null) { 395 mCountView.layout(width - mPaddingRight - mCountView.getMeasuredWidth(), 396 0, 397 width - mPaddingRight, 398 mHeaderBackgroundHeight); 399 } 400 mHeaderDivider.layout(leftBound, 401 mHeaderBackgroundHeight, 402 width - mPaddingRight, 403 mHeaderBackgroundHeight + mHeaderUnderlineHeight); 404 topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight); 405 } 406 407 // Put horizontal divider at the bottom 408 if (mHorizontalDividerVisible) { 409 mHorizontalDividerDrawable.setBounds( 410 leftBound, 411 height - mHorizontalDividerHeight, 412 width - mPaddingRight, 413 height); 414 bottomBound -= mHorizontalDividerHeight; 415 } 416 417 mBoundsWithoutHeader.set(0, topBound, width, bottomBound); 418 419 if (mActivatedStateSupported && isActivated()) { 420 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 421 } 422 423 // Set the top/bottom paddings 424 topBound += mPaddingTop; 425 bottomBound -= mPaddingBottom; 426 427 // Set contact layout: 428 // Photo on the right, call button to the left of the photo 429 // Text aligned to the left along with the presence status. 430 431 // layout the photo and call button. 432 int rightBound = layoutRightSide(height, topBound, bottomBound, width - mPaddingRight); 433 434 // Center text vertically 435 int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + 436 mLabelTextViewHeight + mSnippetTextViewHeight + mStatusTextViewHeight; 437 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2; 438 439 // Layout all text view and presence icon 440 // Put name TextView first 441 if (isVisible(mNameTextView)) { 442 mNameTextView.layout(leftBound + mTextIndent, 443 textTopBound, 444 rightBound, 445 textTopBound + mNameTextViewHeight); 446 textTopBound += mNameTextViewHeight; 447 } 448 449 // Presence and status 450 int statusLeftBound = leftBound + mTextIndent; 451 if (isVisible(mPresenceIcon)) { 452 int iconWidth = mPresenceIcon.getMeasuredWidth(); 453 mPresenceIcon.layout( 454 leftBound + mTextIndent, 455 textTopBound, 456 leftBound + iconWidth + mTextIndent, 457 textTopBound + mStatusTextViewHeight); 458 statusLeftBound += (iconWidth + mPresenceIconMargin); 459 } 460 461 if (isVisible(mStatusView)) { 462 mStatusView.layout(statusLeftBound, 463 textTopBound, 464 rightBound, 465 textTopBound + mStatusTextViewHeight); 466 } 467 468 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { 469 textTopBound += mStatusTextViewHeight; 470 } 471 472 // Rest of text views 473 int dataLeftBound = leftBound; 474 if (isVisible(mPhoneticNameTextView)) { 475 mPhoneticNameTextView.layout(leftBound + mTextIndent, 476 textTopBound, 477 rightBound, 478 textTopBound + mPhoneticNameTextViewHeight); 479 textTopBound += mPhoneticNameTextViewHeight; 480 } 481 482 if (isVisible(mLabelView)) { 483 dataLeftBound = leftBound + mLabelView.getMeasuredWidth() + mTextIndent; 484 mLabelView.layout(leftBound + mTextIndent, 485 textTopBound, 486 dataLeftBound, 487 textTopBound + mLabelTextViewHeight); 488 dataLeftBound += mGapBetweenLabelAndData; 489 } 490 491 if (isVisible(mDataView)) { 492 mDataView.layout(dataLeftBound, 493 textTopBound, 494 rightBound, 495 textTopBound + mLabelTextViewHeight); 496 } 497 if (isVisible(mLabelView) || isVisible(mDataView)) { 498 textTopBound += mLabelTextViewHeight; 499 } 500 501 if (isVisible(mSnippetView)) { 502 mSnippetView.layout(leftBound + mTextIndent, 503 textTopBound, 504 rightBound, 505 textTopBound + mSnippetTextViewHeight); 506 } 507 } 508 509 /** 510 * Performs layout of the right side of the view 511 * 512 * @return new right boundary 513 */ 514 protected int layoutRightSide(int height, int topBound, int bottomBound, int rightBound) { 515 516 // Photo is the right most view, set it up 517 518 View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 519 if (photoView != null) { 520 // Center the photo vertically 521 int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 522 photoView.layout( 523 rightBound - mPhotoViewWidth, 524 photoTop, 525 rightBound, 526 photoTop + mPhotoViewHeight); 527 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 528 } 529 530 // Put call button and vertical divider 531 if (isVisible(mCallButton)) { 532 int buttonWidth = mCallButton.getMeasuredWidth(); 533 rightBound -= buttonWidth; 534 mCallButton.layout( 535 rightBound, 536 topBound, 537 rightBound + buttonWidth, 538 height - mHorizontalDividerHeight); 539 mVerticalDividerVisible = true; 540 ensureVerticalDivider(); 541 rightBound -= mVerticalDividerWidth; 542 mVerticalDividerDrawable.setBounds( 543 rightBound, 544 topBound + mVerticalDividerMargin, 545 rightBound + mVerticalDividerWidth, 546 height - mVerticalDividerMargin); 547 } else { 548 mVerticalDividerVisible = false; 549 } 550 551 return rightBound; 552 } 553 554 @Override 555 public void adjustListItemSelectionBounds(Rect bounds) { 556 bounds.top += mBoundsWithoutHeader.top; 557 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 558 } 559 560 protected boolean isVisible(View view) { 561 return view != null && view.getVisibility() == View.VISIBLE; 562 } 563 564 /** 565 * Loads the drawable for the vertical divider if it has not yet been loaded. 566 */ 567 private void ensureVerticalDivider() { 568 if (mVerticalDividerDrawable == null) { 569 mVerticalDividerDrawable = mContext.getResources().getDrawable( 570 R.drawable.divider_vertical_dark); 571 mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth(); 572 } 573 } 574 575 /** 576 * Extracts width and height from the style 577 */ 578 private void ensurePhotoViewSize() { 579 if (!mPhotoViewWidthAndHeightAreReady) { 580 if (mQuickContactEnabled) { 581 TypedArray a = mContext.obtainStyledAttributes(null, 582 com.android.internal.R.styleable.ViewGroup_Layout, 583 QUICK_CONTACT_BADGE_STYLE, 0); 584 mPhotoViewWidth = a.getLayoutDimension( 585 android.R.styleable.ViewGroup_Layout_layout_width, 586 ViewGroup.LayoutParams.WRAP_CONTENT); 587 mPhotoViewHeight = a.getLayoutDimension( 588 android.R.styleable.ViewGroup_Layout_layout_height, 589 ViewGroup.LayoutParams.WRAP_CONTENT); 590 a.recycle(); 591 } else if (mPhotoView != null) { 592 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); 593 } else { 594 final int defaultPhotoViewSize = getDefaultPhotoViewSize(); 595 mPhotoViewWidth = mKeepHorizontalPaddingForPhotoView ? defaultPhotoViewSize : 0; 596 mPhotoViewHeight = mKeepVerticalPaddingForPhotoView ? defaultPhotoViewSize : 0; 597 } 598 599 mPhotoViewWidthAndHeightAreReady = true; 600 } 601 } 602 603 protected void setDefaultPhotoViewSize(int pixels) { 604 mDefaultPhotoViewSize = pixels; 605 } 606 607 protected int getDefaultPhotoViewSize() { 608 return mDefaultPhotoViewSize; 609 } 610 611 @Override 612 protected void drawableStateChanged() { 613 super.drawableStateChanged(); 614 if (mActivatedStateSupported) { 615 mActivatedBackgroundDrawable.setState(getDrawableState()); 616 } 617 } 618 619 @Override 620 protected boolean verifyDrawable(Drawable who) { 621 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 622 } 623 624 @Override 625 public void jumpDrawablesToCurrentState() { 626 super.jumpDrawablesToCurrentState(); 627 if (mActivatedStateSupported) { 628 mActivatedBackgroundDrawable.jumpToCurrentState(); 629 } 630 } 631 632 @Override 633 public void dispatchDraw(Canvas canvas) { 634 if (mActivatedStateSupported && isActivated()) { 635 mActivatedBackgroundDrawable.draw(canvas); 636 } 637 if (mHorizontalDividerVisible) { 638 mHorizontalDividerDrawable.draw(canvas); 639 } 640 if (mVerticalDividerVisible) { 641 mVerticalDividerDrawable.draw(canvas); 642 } 643 644 super.dispatchDraw(canvas); 645 } 646 647 /** 648 * Sets the flag that determines whether a divider should drawn at the bottom 649 * of the view. 650 */ 651 public void setDividerVisible(boolean visible) { 652 mHorizontalDividerVisible = visible; 653 } 654 655 /** 656 * Sets section header or makes it invisible if the title is null. 657 */ 658 public void setSectionHeader(String title) { 659 if (!TextUtils.isEmpty(title)) { 660 if (mHeaderTextView == null) { 661 mHeaderTextView = new TextView(mContext); 662 mHeaderTextView.setTextColor(mHeaderTextColor); 663 mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize); 664 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD); 665 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL); 666 addView(mHeaderTextView); 667 } 668 if (mHeaderDivider == null) { 669 mHeaderDivider = new View(mContext); 670 mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor); 671 addView(mHeaderDivider); 672 } 673 mHeaderTextView.setText(title); 674 mHeaderTextView.setVisibility(View.VISIBLE); 675 mHeaderDivider.setVisibility(View.VISIBLE); 676 mHeaderVisible = true; 677 } else { 678 if (mHeaderTextView != null) { 679 mHeaderTextView.setVisibility(View.GONE); 680 } 681 if (mHeaderDivider != null) { 682 mHeaderDivider.setVisibility(View.GONE); 683 } 684 mHeaderVisible = false; 685 } 686 } 687 688 /** 689 * Returns the quick contact badge, creating it if necessary. 690 */ 691 public QuickContactBadge getQuickContact() { 692 if (!mQuickContactEnabled) { 693 throw new IllegalStateException("QuickContact is disabled for this view"); 694 } 695 if (mQuickContact == null) { 696 mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE); 697 addView(mQuickContact); 698 mPhotoViewWidthAndHeightAreReady = false; 699 } 700 return mQuickContact; 701 } 702 703 /** 704 * Returns the photo view, creating it if necessary. 705 */ 706 public ImageView getPhotoView() { 707 if (mPhotoView == null) { 708 if (mQuickContactEnabled) { 709 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE); 710 } else { 711 mPhotoView = new ImageView(mContext); 712 } 713 // Quick contact style used above will set a background - remove it 714 mPhotoView.setBackgroundDrawable(null); 715 addView(mPhotoView); 716 mPhotoViewWidthAndHeightAreReady = false; 717 } 718 return mPhotoView; 719 } 720 721 /** 722 * Removes the photo view. 723 */ 724 public void removePhotoView() { 725 removePhotoView(false, true); 726 } 727 728 /** 729 * Removes the photo view. 730 * 731 * @param keepHorizontalPadding True means data on the right side will have 732 * padding on left, pretending there is still a photo view. 733 * @param keepVerticalPadding True means the View will have some height 734 * enough for accommodating a photo view. 735 */ 736 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { 737 mPhotoViewWidthAndHeightAreReady = false; 738 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; 739 mKeepVerticalPaddingForPhotoView = keepVerticalPadding; 740 if (mPhotoView != null) { 741 removeView(mPhotoView); 742 mPhotoView = null; 743 } 744 if (mQuickContact != null) { 745 removeView(mQuickContact); 746 mQuickContact = null; 747 } 748 } 749 750 /** 751 * Sets a word prefix that will be highlighted if encountered in fields like 752 * name and search snippet. 753 * <p> 754 * NOTE: must be all upper-case 755 */ 756 public void setHighlightedPrefix(char[] upperCasePrefix) { 757 mHighlightedPrefix = upperCasePrefix; 758 } 759 760 /** 761 * Returns the text view for the contact name, creating it if necessary. 762 */ 763 public TextView getNameTextView() { 764 if (mNameTextView == null) { 765 mNameTextView = new TextView(mContext); 766 mNameTextView.setSingleLine(true); 767 mNameTextView.setEllipsize(getTextEllipsis()); 768 mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); 769 mNameTextView.setTextColor(mPrimaryTextColor); 770 // Manually call setActivated() since this view may be added after the first 771 // setActivated() call toward this whole item view. 772 mNameTextView.setActivated(isActivated()); 773 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 774 addView(mNameTextView); 775 } 776 return mNameTextView; 777 } 778 779 /** 780 * Adds a call button using the supplied arguments as an id and tag. 781 */ 782 public void showCallButton(int id, int tag) { 783 if (mCallButton == null) { 784 mCallButton = new DontPressWithParentImageView(mContext, null); 785 mCallButton.setId(id); 786 mCallButton.setOnClickListener(mCallButtonClickListener); 787 mCallButton.setBackgroundResource(R.drawable.call_background); 788 mCallButton.setImageResource(android.R.drawable.sym_action_call); 789 mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0); 790 mCallButton.setScaleType(ScaleType.CENTER); 791 addView(mCallButton); 792 } 793 794 mCallButton.setTag(tag); 795 mCallButton.setVisibility(View.VISIBLE); 796 } 797 798 public void hideCallButton() { 799 if (mCallButton != null) { 800 mCallButton.setVisibility(View.GONE); 801 } 802 } 803 804 /** 805 * Adds or updates a text view for the phonetic name. 806 */ 807 public void setPhoneticName(char[] text, int size) { 808 if (text == null || size == 0) { 809 if (mPhoneticNameTextView != null) { 810 mPhoneticNameTextView.setVisibility(View.GONE); 811 } 812 } else { 813 getPhoneticNameTextView(); 814 mPhoneticNameTextView.setText(text, 0, size); 815 mPhoneticNameTextView.setVisibility(VISIBLE); 816 } 817 } 818 819 /** 820 * Returns the text view for the phonetic name, creating it if necessary. 821 */ 822 public TextView getPhoneticNameTextView() { 823 if (mPhoneticNameTextView == null) { 824 mPhoneticNameTextView = new TextView(mContext); 825 mPhoneticNameTextView.setSingleLine(true); 826 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 827 mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 828 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 829 mPhoneticNameTextView.setTextColor(mPrimaryTextColor); 830 mPhoneticNameTextView.setActivated(isActivated()); 831 addView(mPhoneticNameTextView); 832 } 833 return mPhoneticNameTextView; 834 } 835 836 /** 837 * Adds or updates a text view for the data label. 838 */ 839 public void setLabel(CharSequence text) { 840 if (TextUtils.isEmpty(text)) { 841 if (mLabelView != null) { 842 mLabelView.setVisibility(View.GONE); 843 } 844 } else { 845 getLabelView(); 846 mLabelView.setText(text); 847 mLabelView.setVisibility(VISIBLE); 848 } 849 } 850 851 /** 852 * Adds or updates a text view for the data label. 853 */ 854 public void setLabel(char[] text, int size) { 855 if (text == null || size == 0) { 856 if (mLabelView != null) { 857 mLabelView.setVisibility(View.GONE); 858 } 859 } else { 860 getLabelView(); 861 mLabelView.setText(text, 0, size); 862 mLabelView.setVisibility(VISIBLE); 863 } 864 } 865 866 /** 867 * Returns the text view for the data label, creating it if necessary. 868 */ 869 public TextView getLabelView() { 870 if (mLabelView == null) { 871 mLabelView = new TextView(mContext); 872 mLabelView.setSingleLine(true); 873 mLabelView.setEllipsize(getTextEllipsis()); 874 mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 875 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 876 mLabelView.setTextColor(mPrimaryTextColor); 877 mLabelView.setActivated(isActivated()); 878 addView(mLabelView); 879 } 880 return mLabelView; 881 } 882 883 /** 884 * Adds or updates a text view for the data element. 885 */ 886 public void setData(char[] text, int size) { 887 if (text == null || size == 0) { 888 if (mDataView != null) { 889 mDataView.setVisibility(View.GONE); 890 } 891 return; 892 } else { 893 getDataView(); 894 mDataView.setText(text, 0, size); 895 mDataView.setVisibility(VISIBLE); 896 } 897 } 898 899 /** 900 * Returns the text view for the data text, creating it if necessary. 901 */ 902 public TextView getDataView() { 903 if (mDataView == null) { 904 mDataView = new TextView(mContext); 905 mDataView.setSingleLine(true); 906 mDataView.setEllipsize(getTextEllipsis()); 907 mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 908 mDataView.setTextColor(mPrimaryTextColor); 909 mDataView.setActivated(isActivated()); 910 addView(mDataView); 911 } 912 return mDataView; 913 } 914 915 /** 916 * Adds or updates a text view for the search snippet. 917 */ 918 public void setSnippet(String text) { 919 if (TextUtils.isEmpty(text)) { 920 if (mSnippetView != null) { 921 mSnippetView.setVisibility(View.GONE); 922 } 923 } else { 924 mPrefixHighligher.setText(getSnippetView(), text, mHighlightedPrefix); 925 mSnippetView.setVisibility(VISIBLE); 926 } 927 } 928 929 /** 930 * Returns the text view for the search snippet, creating it if necessary. 931 */ 932 public TextView getSnippetView() { 933 if (mSnippetView == null) { 934 mSnippetView = new TextView(mContext); 935 mSnippetView.setSingleLine(true); 936 mSnippetView.setEllipsize(getTextEllipsis()); 937 mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 938 mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD); 939 mSnippetView.setTextColor(mPrimaryTextColor); 940 mSnippetView.setActivated(isActivated()); 941 addView(mSnippetView); 942 } 943 return mSnippetView; 944 } 945 946 /** 947 * Returns the text view for the status, creating it if necessary. 948 */ 949 public TextView getStatusView() { 950 if (mStatusView == null) { 951 mStatusView = new TextView(mContext); 952 mStatusView.setSingleLine(true); 953 mStatusView.setEllipsize(getTextEllipsis()); 954 mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 955 mStatusView.setTextColor(mSecondaryTextColor); 956 mStatusView.setActivated(isActivated()); 957 addView(mStatusView); 958 } 959 return mStatusView; 960 } 961 962 /** 963 * Returns the text view for the contacts count, creating it if necessary. 964 */ 965 public TextView getCountView() { 966 if (mCountView == null) { 967 mCountView = new TextView(mContext); 968 mCountView.setSingleLine(true); 969 mCountView.setEllipsize(getTextEllipsis()); 970 mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium); 971 mCountView.setTextColor(R.color.contact_count_text_color); 972 addView(mCountView); 973 } 974 return mCountView; 975 } 976 977 /** 978 * Adds or updates a text view for the contacts count. 979 */ 980 public void setCountView(CharSequence text) { 981 if (TextUtils.isEmpty(text)) { 982 if (mCountView != null) { 983 mCountView.setVisibility(View.GONE); 984 } 985 } else { 986 getCountView(); 987 mCountView.setText(text); 988 mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize); 989 mCountView.setGravity(Gravity.CENTER_VERTICAL); 990 mCountView.setTextColor(mContactsCountTextColor); 991 mCountView.setVisibility(VISIBLE); 992 } 993 } 994 995 /** 996 * Adds or updates a text view for the status. 997 */ 998 public void setStatus(CharSequence text) { 999 if (TextUtils.isEmpty(text)) { 1000 if (mStatusView != null) { 1001 mStatusView.setVisibility(View.GONE); 1002 } 1003 } else { 1004 getStatusView(); 1005 mStatusView.setText(text); 1006 mStatusView.setVisibility(VISIBLE); 1007 } 1008 } 1009 1010 /** 1011 * Adds or updates the presence icon view. 1012 */ 1013 public void setPresence(Drawable icon) { 1014 if (icon != null) { 1015 if (mPresenceIcon == null) { 1016 mPresenceIcon = new ImageView(mContext); 1017 addView(mPresenceIcon); 1018 } 1019 mPresenceIcon.setImageDrawable(icon); 1020 mPresenceIcon.setScaleType(ScaleType.CENTER); 1021 mPresenceIcon.setVisibility(View.VISIBLE); 1022 } else { 1023 if (mPresenceIcon != null) { 1024 mPresenceIcon.setVisibility(View.GONE); 1025 } 1026 } 1027 } 1028 1029 private TruncateAt getTextEllipsis() { 1030 return mActivatedStateSupported ? TruncateAt.START : TruncateAt.MARQUEE; 1031 } 1032 1033 public void showDisplayName(Cursor cursor, int nameColumnIndex, int alternativeNameColumnIndex, 1034 boolean highlightingEnabled, int displayOrder) { 1035 // Copy out the display name and alternate display name. 1036 cursor.copyStringToBuffer(nameColumnIndex, mDisplayNameFormatter.getNameBuffer()); 1037 cursor.copyStringToBuffer(alternativeNameColumnIndex, 1038 mDisplayNameFormatter.getAlternateNameBuffer()); 1039 1040 mDisplayNameFormatter.setDisplayName( 1041 getNameTextView(), displayOrder, highlightingEnabled, mHighlightedPrefix); 1042 } 1043 1044 public void hideDisplayName() { 1045 if (mNameTextView != null) { 1046 removeView(mNameTextView); 1047 mNameTextView = null; 1048 } 1049 } 1050 1051 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 1052 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 1053 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 1054 if (phoneticNameSize != 0) { 1055 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 1056 } else { 1057 setPhoneticName(null, 0); 1058 } 1059 } 1060 1061 public void hidePhoneticName() { 1062 if (mPhoneticNameTextView != null) { 1063 removeView(mPhoneticNameTextView); 1064 mPhoneticNameTextView = null; 1065 } 1066 } 1067 1068 /** 1069 * Sets the proper icon (star or presence or nothing) and/or status message. 1070 */ 1071 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, 1072 int contactStatusColumnIndex) { 1073 Drawable icon = null; 1074 int presence = 0; 1075 if (!cursor.isNull(presenceColumnIndex)) { 1076 presence = cursor.getInt(presenceColumnIndex); 1077 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); 1078 } 1079 setPresence(icon); 1080 1081 String statusMessage = null; 1082 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { 1083 statusMessage = cursor.getString(contactStatusColumnIndex); 1084 } 1085 // If there is no status message from the contact, but there was a presence value, then use 1086 // the default status message string 1087 if (statusMessage == null && presence != 0) { 1088 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); 1089 } 1090 setStatus(statusMessage); 1091 } 1092 1093 /** 1094 * Shows search snippet. 1095 */ 1096 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 1097 if (cursor.getColumnCount() <= summarySnippetColumnIndex) { 1098 setSnippet(null); 1099 return; 1100 } 1101 1102 String snippet = cursor.getString(summarySnippetColumnIndex); 1103 if (snippet != null) { 1104 int from = 0; 1105 int to = snippet.length(); 1106 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 1107 if (start == -1) { 1108 snippet = null; 1109 } else { 1110 int firstNl = snippet.lastIndexOf('\n', start); 1111 if (firstNl != -1) { 1112 from = firstNl + 1; 1113 } 1114 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 1115 if (end != -1) { 1116 int lastNl = snippet.indexOf('\n', end); 1117 if (lastNl != -1) { 1118 to = lastNl; 1119 } 1120 } 1121 1122 StringBuilder sb = new StringBuilder(); 1123 for (int i = from; i < to; i++) { 1124 char c = snippet.charAt(i); 1125 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 1126 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 1127 sb.append(c); 1128 } 1129 } 1130 snippet = sb.toString(); 1131 } 1132 } 1133 setSnippet(snippet); 1134 } 1135 1136 /** 1137 * Shows data element (e.g. phone number). 1138 */ 1139 public void showData(Cursor cursor, int dataColumnIndex) { 1140 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 1141 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 1142 } 1143 1144 public void setActivatedStateSupported(boolean flag) { 1145 this.mActivatedStateSupported = flag; 1146 } 1147 1148 @Override 1149 public void requestLayout() { 1150 // We will assume that once measured this will not need to resize 1151 // itself, so there is no need to pass the layout request to the parent 1152 // view (ListView). 1153 forceLayout(); 1154 } 1155} 1156