1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.contacts; 18 19 import com.android.contacts.ui.widget.DontPressWithParentImageView; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Typeface; 26 import android.graphics.drawable.Drawable; 27 import android.provider.ContactsContract.Contacts; 28 import android.text.TextUtils; 29 import android.text.TextUtils.TruncateAt; 30 import android.util.AttributeSet; 31 import android.view.Gravity; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.ImageView; 35 import android.widget.QuickContactBadge; 36 import android.widget.TextView; 37 import android.widget.ImageView.ScaleType; 38 39 /** 40 * A custom view for an item in the contact list. 41 */ 42 public class ContactListItemView extends ViewGroup { 43 44 private static final int QUICK_CONTACT_BADGE_STYLE = 45 com.android.internal.R.attr.quickContactBadgeStyleWindowMedium; 46 47 private final Context mContext; 48 49 private final int mPreferredHeight; 50 private final int mVerticalDividerMargin; 51 private final int mPaddingTop; 52 private final int mPaddingRight; 53 private final int mPaddingBottom; 54 private final int mPaddingLeft; 55 private final int mGapBetweenImageAndText; 56 private final int mGapBetweenLabelAndData; 57 private final int mCallButtonPadding; 58 private final int mPresenceIconMargin; 59 private final int mHeaderTextWidth; 60 61 private boolean mHorizontalDividerVisible; 62 private Drawable mHorizontalDividerDrawable; 63 private int mHorizontalDividerHeight; 64 65 private boolean mVerticalDividerVisible; 66 private Drawable mVerticalDividerDrawable; 67 private int mVerticalDividerWidth; 68 69 private boolean mHeaderVisible; 70 private Drawable mHeaderBackgroundDrawable; 71 private int mHeaderBackgroundHeight; 72 private TextView mHeaderTextView; 73 74 private QuickContactBadge mQuickContact; 75 private ImageView mPhotoView; 76 private TextView mNameTextView; 77 private DontPressWithParentImageView mCallButton; 78 private TextView mLabelView; 79 private TextView mDataView; 80 private TextView mSnippetView; 81 private ImageView mPresenceIcon; 82 83 private int mPhotoViewWidth; 84 private int mPhotoViewHeight; 85 private int mLine1Height; 86 private int mLine2Height; 87 private int mLine3Height; 88 89 private OnClickListener mCallButtonClickListener; 90 ContactListItemView(Context context, AttributeSet attrs)91 public ContactListItemView(Context context, AttributeSet attrs) { 92 super(context, attrs); 93 mContext = context; 94 95 // Obtain preferred item height from the current theme 96 TypedArray a = context.obtainStyledAttributes(null, com.android.internal.R.styleable.Theme); 97 mPreferredHeight = 98 a.getDimensionPixelSize(android.R.styleable.Theme_listPreferredItemHeight, 0); 99 a.recycle(); 100 101 Resources resources = context.getResources(); 102 mVerticalDividerMargin = 103 resources.getDimensionPixelOffset(R.dimen.list_item_vertical_divider_margin); 104 mPaddingTop = 105 resources.getDimensionPixelOffset(R.dimen.list_item_padding_top); 106 mPaddingBottom = 107 resources.getDimensionPixelOffset(R.dimen.list_item_padding_bottom); 108 mPaddingLeft = 109 resources.getDimensionPixelOffset(R.dimen.list_item_padding_left); 110 mPaddingRight = 111 resources.getDimensionPixelOffset(R.dimen.list_item_padding_right); 112 mGapBetweenImageAndText = 113 resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_image_and_text); 114 mGapBetweenLabelAndData = 115 resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_label_and_data); 116 mCallButtonPadding = 117 resources.getDimensionPixelOffset(R.dimen.list_item_call_button_padding); 118 mPresenceIconMargin = 119 resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin); 120 mHeaderTextWidth = 121 resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width); 122 } 123 124 /** 125 * Installs a call button listener. 126 */ setOnCallButtonClickListener(OnClickListener callButtonClickListener)127 public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) { 128 mCallButtonClickListener = callButtonClickListener; 129 } 130 131 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)132 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 133 // We will match parent's width and wrap content vertically, but make sure 134 // height is no less than listPreferredItemHeight. 135 int width = resolveSize(0, widthMeasureSpec); 136 int height = 0; 137 138 mLine1Height = 0; 139 mLine2Height = 0; 140 mLine3Height = 0; 141 142 // Obtain the natural dimensions of the name text (we only care about height) 143 mNameTextView.measure(0, 0); 144 145 mLine1Height = mNameTextView.getMeasuredHeight(); 146 147 if (isVisible(mLabelView)) { 148 mLabelView.measure(0, 0); 149 mLine2Height = mLabelView.getMeasuredHeight(); 150 } 151 152 if (isVisible(mDataView)) { 153 mDataView.measure(0, 0); 154 mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight()); 155 } 156 157 if (isVisible(mSnippetView)) { 158 mSnippetView.measure(0, 0); 159 mLine3Height = mSnippetView.getMeasuredHeight(); 160 } 161 162 height += mLine1Height + mLine2Height + mLine3Height; 163 164 if (isVisible(mCallButton)) { 165 mCallButton.measure(0, 0); 166 } 167 if (isVisible(mPresenceIcon)) { 168 mPresenceIcon.measure(0, 0); 169 } 170 171 ensurePhotoViewSize(); 172 173 height = Math.max(height, mPhotoViewHeight); 174 height = Math.max(height, mPreferredHeight); 175 176 if (mHeaderVisible) { 177 ensureHeaderBackground(); 178 mHeaderTextView.measure( 179 MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY), 180 MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY)); 181 height += mHeaderBackgroundDrawable.getIntrinsicHeight(); 182 } 183 184 setMeasuredDimension(width, height); 185 } 186 187 @Override onLayout(boolean changed, int left, int top, int right, int bottom)188 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 189 int height = bottom - top; 190 int width = right - left; 191 192 // Determine the vertical bounds by laying out the header first. 193 int topBound = 0; 194 195 if (mHeaderVisible) { 196 mHeaderBackgroundDrawable.setBounds( 197 0, 198 0, 199 width, 200 mHeaderBackgroundHeight); 201 mHeaderTextView.layout(0, 0, width, mHeaderBackgroundHeight); 202 topBound += mHeaderBackgroundHeight; 203 } 204 205 // Positions of views on the left are fixed and so are those on the right side. 206 // The stretchable part of the layout is in the middle. So, we will start off 207 // by laying out the left and right sides. Then we will allocate the remainder 208 // to the text fields in the middle. 209 210 // Left side 211 int leftBound = mPaddingLeft; 212 View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 213 if (photoView != null) { 214 // Center the photo vertically 215 int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2; 216 photoView.layout( 217 leftBound, 218 photoTop, 219 leftBound + mPhotoViewWidth, 220 photoTop + mPhotoViewHeight); 221 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 222 } 223 224 // Right side 225 int rightBound = right; 226 if (isVisible(mCallButton)) { 227 int buttonWidth = mCallButton.getMeasuredWidth(); 228 rightBound -= buttonWidth; 229 mCallButton.layout( 230 rightBound, 231 topBound, 232 rightBound + buttonWidth, 233 height); 234 mVerticalDividerVisible = true; 235 ensureVerticalDivider(); 236 rightBound -= mVerticalDividerWidth; 237 mVerticalDividerDrawable.setBounds( 238 rightBound, 239 topBound + mVerticalDividerMargin, 240 rightBound + mVerticalDividerWidth, 241 height - mVerticalDividerMargin); 242 } else { 243 mVerticalDividerVisible = false; 244 } 245 246 if (isVisible(mPresenceIcon)) { 247 int iconWidth = mPresenceIcon.getMeasuredWidth(); 248 rightBound -= mPresenceIconMargin + iconWidth; 249 mPresenceIcon.layout( 250 rightBound, 251 topBound, 252 rightBound + iconWidth, 253 height); 254 } 255 256 if (mHorizontalDividerVisible) { 257 ensureHorizontalDivider(); 258 mHorizontalDividerDrawable.setBounds( 259 0, 260 height - mHorizontalDividerHeight, 261 width, 262 height); 263 } 264 265 topBound += mPaddingTop; 266 int bottomBound = height - mPaddingBottom; 267 268 // Text lines, centered vertically 269 rightBound -= mPaddingRight; 270 271 // Center text vertically 272 int totalTextHeight = mLine1Height + mLine2Height + mLine3Height; 273 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2; 274 275 mNameTextView.layout(leftBound, 276 textTopBound, 277 rightBound, 278 textTopBound + mLine1Height); 279 280 int dataLeftBound = leftBound; 281 if (isVisible(mLabelView)) { 282 dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); 283 mLabelView.layout(leftBound, 284 textTopBound + mLine1Height, 285 dataLeftBound, 286 textTopBound + mLine1Height + mLine2Height); 287 dataLeftBound += mGapBetweenLabelAndData; 288 } 289 290 if (isVisible(mDataView)) { 291 mDataView.layout(dataLeftBound, 292 textTopBound + mLine1Height, 293 rightBound, 294 textTopBound + mLine1Height + mLine2Height); 295 } 296 297 if (isVisible(mSnippetView)) { 298 mSnippetView.layout(leftBound, 299 textTopBound + mLine1Height + mLine2Height, 300 rightBound, 301 textTopBound + mLine1Height + mLine2Height + mLine3Height); 302 } 303 } 304 isVisible(View view)305 private boolean isVisible(View view) { 306 return view != null && view.getVisibility() == View.VISIBLE; 307 } 308 309 /** 310 * Loads the drawable for the vertical divider if it has not yet been loaded. 311 */ ensureVerticalDivider()312 private void ensureVerticalDivider() { 313 if (mVerticalDividerDrawable == null) { 314 mVerticalDividerDrawable = mContext.getResources().getDrawable( 315 R.drawable.divider_vertical_dark); 316 mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth(); 317 } 318 } 319 320 /** 321 * Loads the drawable for the horizontal divider if it has not yet been loaded. 322 */ ensureHorizontalDivider()323 private void ensureHorizontalDivider() { 324 if (mHorizontalDividerDrawable == null) { 325 mHorizontalDividerDrawable = mContext.getResources().getDrawable( 326 com.android.internal.R.drawable.divider_horizontal_dark_opaque); 327 mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight(); 328 } 329 } 330 331 /** 332 * Loads the drawable for the header background if it has not yet been loaded. 333 */ ensureHeaderBackground()334 private void ensureHeaderBackground() { 335 if (mHeaderBackgroundDrawable == null) { 336 mHeaderBackgroundDrawable = mContext.getResources().getDrawable( 337 android.R.drawable.dark_header); 338 mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight(); 339 } 340 } 341 342 /** 343 * Extracts width and height from the style 344 */ ensurePhotoViewSize()345 private void ensurePhotoViewSize() { 346 if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) { 347 TypedArray a = mContext.obtainStyledAttributes(null, 348 com.android.internal.R.styleable.ViewGroup_Layout, 349 QUICK_CONTACT_BADGE_STYLE, 0); 350 mPhotoViewWidth = a.getLayoutDimension( 351 android.R.styleable.ViewGroup_Layout_layout_width, 352 ViewGroup.LayoutParams.WRAP_CONTENT); 353 mPhotoViewHeight = a.getLayoutDimension( 354 android.R.styleable.ViewGroup_Layout_layout_height, 355 ViewGroup.LayoutParams.WRAP_CONTENT); 356 a.recycle(); 357 } 358 } 359 360 @Override dispatchDraw(Canvas canvas)361 public void dispatchDraw(Canvas canvas) { 362 if (mHeaderVisible) { 363 mHeaderBackgroundDrawable.draw(canvas); 364 } 365 if (mHorizontalDividerVisible) { 366 mHorizontalDividerDrawable.draw(canvas); 367 } 368 if (mVerticalDividerVisible) { 369 mVerticalDividerDrawable.draw(canvas); 370 } 371 super.dispatchDraw(canvas); 372 } 373 374 /** 375 * Sets the flag that determines whether a divider should drawn at the bottom 376 * of the view. 377 */ setDividerVisible(boolean visible)378 public void setDividerVisible(boolean visible) { 379 mHorizontalDividerVisible = visible; 380 } 381 382 /** 383 * Sets section header or makes it invisible if the title is null. 384 */ setSectionHeader(String title)385 public void setSectionHeader(String title) { 386 if (!TextUtils.isEmpty(title)) { 387 if (mHeaderTextView == null) { 388 mHeaderTextView = new TextView(mContext); 389 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD); 390 mHeaderTextView.setTextColor(mContext.getResources() 391 .getColor(com.android.internal.R.color.dim_foreground_dark)); 392 mHeaderTextView.setTextSize(14); 393 mHeaderTextView.setGravity(Gravity.CENTER); 394 addView(mHeaderTextView); 395 } 396 mHeaderTextView.setText(title); 397 mHeaderTextView.setVisibility(View.VISIBLE); 398 mHeaderVisible = true; 399 } else { 400 if (mHeaderTextView != null) { 401 mHeaderTextView.setVisibility(View.GONE); 402 } 403 mHeaderVisible = false; 404 } 405 } 406 407 /** 408 * Returns the quick contact badge, creating it if necessary. 409 */ getQuickContact()410 public QuickContactBadge getQuickContact() { 411 if (mQuickContact == null) { 412 mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE); 413 mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE }); 414 addView(mQuickContact); 415 } 416 return mQuickContact; 417 } 418 419 /** 420 * Returns the photo view, creating it if necessary. 421 */ getPhotoView()422 public ImageView getPhotoView() { 423 if (mPhotoView == null) { 424 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE); 425 // Quick contact style used above will set a background - remove it 426 mPhotoView.setBackgroundDrawable(null); 427 addView(mPhotoView); 428 } 429 return mPhotoView; 430 } 431 432 /** 433 * Returns the text view for the contact name, creating it if necessary. 434 */ getNameTextView()435 public TextView getNameTextView() { 436 if (mNameTextView == null) { 437 mNameTextView = new TextView(mContext); 438 mNameTextView.setSingleLine(true); 439 mNameTextView.setEllipsize(TruncateAt.MARQUEE); 440 mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Large); 441 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 442 addView(mNameTextView); 443 } 444 return mNameTextView; 445 } 446 447 /** 448 * Adds a call button using the supplied arguments as an id and tag. 449 */ showCallButton(int id, int tag)450 public void showCallButton(int id, int tag) { 451 if (mCallButton == null) { 452 mCallButton = new DontPressWithParentImageView(mContext, null); 453 mCallButton.setId(id); 454 mCallButton.setOnClickListener(mCallButtonClickListener); 455 mCallButton.setBackgroundResource(R.drawable.call_background); 456 mCallButton.setImageResource(android.R.drawable.sym_action_call); 457 mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0); 458 mCallButton.setScaleType(ScaleType.CENTER); 459 addView(mCallButton); 460 } 461 462 mCallButton.setTag(tag); 463 mCallButton.setVisibility(View.VISIBLE); 464 } 465 hideCallButton()466 public void hideCallButton() { 467 if (mCallButton != null) { 468 mCallButton.setVisibility(View.GONE); 469 } 470 } 471 472 /** 473 * Adds or updates a text view for the data label. 474 */ setLabel(CharSequence text)475 public void setLabel(CharSequence text) { 476 if (TextUtils.isEmpty(text)) { 477 if (mLabelView != null) { 478 mLabelView.setVisibility(View.GONE); 479 } 480 } else { 481 getLabelView(); 482 mLabelView.setText(text); 483 mLabelView.setVisibility(VISIBLE); 484 } 485 } 486 487 /** 488 * Adds or updates a text view for the data label. 489 */ setLabel(char[] text, int size)490 public void setLabel(char[] text, int size) { 491 if (text == null || size == 0) { 492 if (mLabelView != null) { 493 mLabelView.setVisibility(View.GONE); 494 } 495 } else { 496 getLabelView(); 497 mLabelView.setText(text, 0, size); 498 mLabelView.setVisibility(VISIBLE); 499 } 500 } 501 502 /** 503 * Returns the text view for the data label, creating it if necessary. 504 */ getLabelView()505 public TextView getLabelView() { 506 if (mLabelView == null) { 507 mLabelView = new TextView(mContext); 508 mLabelView.setSingleLine(true); 509 mLabelView.setEllipsize(TruncateAt.MARQUEE); 510 mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 511 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 512 addView(mLabelView); 513 } 514 return mLabelView; 515 } 516 517 /** 518 * Adds or updates a text view for the data element. 519 */ setData(char[] text, int size)520 public void setData(char[] text, int size) { 521 if (text == null || size == 0) { 522 if (mDataView != null) { 523 mDataView.setVisibility(View.GONE); 524 } 525 return; 526 } else { 527 getDataView(); 528 mDataView.setText(text, 0, size); 529 mDataView.setVisibility(VISIBLE); 530 } 531 } 532 533 /** 534 * Returns the text view for the data text, creating it if necessary. 535 */ getDataView()536 public TextView getDataView() { 537 if (mDataView == null) { 538 mDataView = new TextView(mContext); 539 mDataView.setSingleLine(true); 540 mDataView.setEllipsize(TruncateAt.MARQUEE); 541 mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 542 addView(mDataView); 543 } 544 return mDataView; 545 } 546 547 /** 548 * Adds or updates a text view for the search snippet. 549 */ setSnippet(CharSequence text)550 public void setSnippet(CharSequence text) { 551 if (TextUtils.isEmpty(text)) { 552 if (mSnippetView != null) { 553 mSnippetView.setVisibility(View.GONE); 554 } 555 } else { 556 getSnippetView(); 557 mSnippetView.setText(text); 558 mSnippetView.setVisibility(VISIBLE); 559 } 560 } 561 562 /** 563 * Returns the text view for the search snippet, creating it if necessary. 564 */ getSnippetView()565 public TextView getSnippetView() { 566 if (mSnippetView == null) { 567 mSnippetView = new TextView(mContext); 568 mSnippetView.setSingleLine(true); 569 mSnippetView.setEllipsize(TruncateAt.MARQUEE); 570 mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); 571 mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD); 572 addView(mSnippetView); 573 } 574 return mSnippetView; 575 } 576 577 /** 578 * Adds or updates the presence icon view. 579 */ setPresence(Drawable icon)580 public void setPresence(Drawable icon) { 581 if (icon != null) { 582 if (mPresenceIcon == null) { 583 mPresenceIcon = new ImageView(mContext); 584 addView(mPresenceIcon); 585 } 586 mPresenceIcon.setImageDrawable(icon); 587 mPresenceIcon.setScaleType(ScaleType.CENTER); 588 mPresenceIcon.setVisibility(View.VISIBLE); 589 } else { 590 if (mPresenceIcon != null) { 591 mPresenceIcon.setVisibility(View.GONE); 592 } 593 } 594 } 595 } 596