summaryrefslogtreecommitdiffstats
path: root/src/com/android/contacts/common/list/ContactListItemView.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/contacts/common/list/ContactListItemView.java')
-rw-r--r--src/com/android/contacts/common/list/ContactListItemView.java1231
1 files changed, 1231 insertions, 0 deletions
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
new file mode 100644
index 00000000..d89aea9a
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -0,0 +1,1231 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.SelectionBoundsAdjuster;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.format.PrefixHighlighter;
+
+/**
+ * A custom view for an item in the contact list.
+ * The view contains the contact's photo, a set of text views (for name, status, etc...) and
+ * icons for presence and call.
+ * The view uses no XML file for layout and all the measurements and layouts are done
+ * in the onMeasure and onLayout methods.
+ *
+ * The layout puts the contact's photo on the right side of the view, the call icon (if present)
+ * to the left of the photo, the text lines are aligned to the left and the presence icon (if
+ * present) is set to the left of the status line.
+ *
+ * The layout also supports a header (used as a header of a group of contacts) that is above the
+ * contact's data and a divider between contact view.
+ */
+
+public class ContactListItemView extends ViewGroup
+ implements SelectionBoundsAdjuster {
+
+ // Style values for layout and appearance
+ // The initialized values are defaults if none is provided through xml.
+ private int mPreferredHeight = 0;
+ private int mGapBetweenImageAndText = 0;
+ private int mGapBetweenLabelAndData = 0;
+ private int mPresenceIconMargin = 4;
+ private int mPresenceIconSize = 16;
+ private int mHeaderTextColor = Color.BLACK;
+ private int mHeaderTextIndent = 0;
+ private int mHeaderTextSize = 12;
+ private int mHeaderUnderlineHeight = 1;
+ private int mHeaderUnderlineColor = 0;
+ private int mCountViewTextSize = 12;
+ private int mContactsCountTextColor = Color.BLACK;
+ private int mTextIndent = 0;
+ private Drawable mActivatedBackgroundDrawable;
+
+ /**
+ * Used with {@link #mLabelView}, specifying the width ratio between label and data.
+ */
+ private int mLabelViewWidthWeight = 3;
+ /**
+ * Used with {@link #mDataView}, specifying the width ratio between label and data.
+ */
+ private int mDataViewWidthWeight = 5;
+
+ // Will be used with adjustListItemSelectionBounds().
+ private int mSelectionBoundsMarginLeft;
+ private int mSelectionBoundsMarginRight;
+
+ // Horizontal divider between contact views.
+ private boolean mHorizontalDividerVisible = true;
+ private Drawable mHorizontalDividerDrawable;
+ private int mHorizontalDividerHeight;
+
+ /**
+ * Where to put contact photo. This affects the other Views' layout or look-and-feel.
+ */
+ public enum PhotoPosition {
+ LEFT,
+ RIGHT
+ }
+ public static final PhotoPosition DEFAULT_PHOTO_POSITION = PhotoPosition.RIGHT;
+ private PhotoPosition mPhotoPosition = DEFAULT_PHOTO_POSITION;
+
+ // Header layout data
+ private boolean mHeaderVisible;
+ private View mHeaderDivider;
+ private int mHeaderBackgroundHeight = 30;
+ private TextView mHeaderTextView;
+
+ // The views inside the contact view
+ private boolean mQuickContactEnabled = true;
+ private QuickContactBadge mQuickContact;
+ private ImageView mPhotoView;
+ private TextView mNameTextView;
+ private TextView mPhoneticNameTextView;
+ private TextView mLabelView;
+ private TextView mDataView;
+ private TextView mSnippetView;
+ private TextView mStatusView;
+ private TextView mCountView;
+ private ImageView mPresenceIcon;
+
+ private ColorStateList mSecondaryTextColor;
+
+ private char[] mHighlightedPrefix;
+
+ private int mDefaultPhotoViewSize = 0;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+ * to align other data in this View.
+ */
+ private int mPhotoViewWidth;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+ */
+ private int mPhotoViewHeight;
+
+ /**
+ * Only effective when {@link #mPhotoView} is null.
+ * When true all the Views on the right side of the photo should have horizontal padding on
+ * those left assuming there is a photo.
+ */
+ private boolean mKeepHorizontalPaddingForPhotoView;
+ /**
+ * Only effective when {@link #mPhotoView} is null.
+ */
+ private boolean mKeepVerticalPaddingForPhotoView;
+
+ /**
+ * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+ * False indicates those values should be updated before being used in position calculation.
+ */
+ private boolean mPhotoViewWidthAndHeightAreReady = false;
+
+ private int mNameTextViewHeight;
+ private int mPhoneticNameTextViewHeight;
+ private int mLabelViewHeight;
+ private int mDataViewHeight;
+ private int mSnippetTextViewHeight;
+ private int mStatusTextViewHeight;
+
+ // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
+ // same row.
+ private int mLabelAndDataViewMaxHeight;
+
+ // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
+ // more efficient for each case or in general, and simplify the whole implementation.
+ // Note: if we're sure MARQUEE will be used every time, there's no reason to use
+ // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
+ // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
+ // TextView without any modification.
+ private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
+ private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
+
+ private boolean mActivatedStateSupported;
+
+ private Rect mBoundsWithoutHeader = new Rect();
+
+ /** A helper used to highlight a prefix in a text field. */
+ private PrefixHighlighter mPrefixHighlighter;
+ private CharSequence mUnknownNameText;
+
+ public ContactListItemView(Context context) {
+ super(context);
+ mContext = context;
+
+ mPrefixHighlighter = new PrefixHighlighter(Color.GREEN);
+ }
+
+ public ContactListItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+
+ // Read all style values
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+ mPreferredHeight = a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
+ mActivatedBackgroundDrawable = a.getDrawable(
+ R.styleable.ContactListItemView_activated_background);
+ mHorizontalDividerDrawable = a.getDrawable(
+ R.styleable.ContactListItemView_list_item_divider);
+
+ mGapBetweenImageAndText = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
+ mGapBetweenImageAndText);
+ mGapBetweenLabelAndData = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
+ mGapBetweenLabelAndData);
+ mPresenceIconMargin = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_margin,
+ mPresenceIconMargin);
+ mPresenceIconSize = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
+ mDefaultPhotoViewSize = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+ mHeaderTextIndent = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_header_text_indent, mHeaderTextIndent);
+ mHeaderTextColor = a.getColor(
+ R.styleable.ContactListItemView_list_item_header_text_color, mHeaderTextColor);
+ mHeaderTextSize = a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_header_text_size, mHeaderTextSize);
+ mHeaderBackgroundHeight = a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_header_height, mHeaderBackgroundHeight);
+ mHeaderUnderlineHeight = a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_header_underline_height,
+ mHeaderUnderlineHeight);
+ mHeaderUnderlineColor = a.getColor(
+ R.styleable.ContactListItemView_list_item_header_underline_color,
+ mHeaderUnderlineColor);
+ mTextIndent = a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
+ mCountViewTextSize = a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_contacts_count_text_size,
+ mCountViewTextSize);
+ mContactsCountTextColor = a.getColor(
+ R.styleable.ContactListItemView_list_item_contacts_count_text_color,
+ mContactsCountTextColor);
+ mDataViewWidthWeight = a.getInteger(
+ R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
+ mLabelViewWidthWeight = a.getInteger(
+ R.styleable.ContactListItemView_list_item_label_width_weight,
+ mLabelViewWidthWeight);
+
+ setPadding(
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_padding_left, 0),
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_padding_top, 0),
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_padding_right, 0),
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_padding_bottom, 0));
+
+ final int prefixHighlightColor = a.getColor(
+ R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
+ mPrefixHighlighter = new PrefixHighlighter(prefixHighlightColor);
+ a.recycle();
+
+ a = getContext().obtainStyledAttributes(android.R.styleable.Theme);
+ mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary);
+ a.recycle();
+
+ mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
+
+ if (mActivatedBackgroundDrawable != null) {
+ mActivatedBackgroundDrawable.setCallback(this);
+ }
+ }
+
+ public void setUnknownNameText(CharSequence unknownNameText) {
+ mUnknownNameText = unknownNameText;
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ mQuickContactEnabled = flag;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // We will match parent's width and wrap content vertically, but make sure
+ // height is no less than listPreferredItemHeight.
+ final int specWidth = resolveSize(0, widthMeasureSpec);
+ final int preferredHeight;
+ if (mHorizontalDividerVisible) {
+ preferredHeight = mPreferredHeight + mHorizontalDividerHeight;
+ } else {
+ preferredHeight = mPreferredHeight;
+ }
+
+ mNameTextViewHeight = 0;
+ mPhoneticNameTextViewHeight = 0;
+ mLabelViewHeight = 0;
+ mDataViewHeight = 0;
+ mLabelAndDataViewMaxHeight = 0;
+ mSnippetTextViewHeight = 0;
+ mStatusTextViewHeight = 0;
+
+ ensurePhotoViewSize();
+
+ // Width each TextView is able to use.
+ final int effectiveWidth;
+ // All the other Views will honor the photo, so available width for them may be shrunk.
+ if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
+ effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
+ - (mPhotoViewWidth + mGapBetweenImageAndText);
+ } else {
+ effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+ }
+
+ // Go over all visible text views and measure actual width of each of them.
+ // Also calculate their heights to get the total height for this entire view.
+
+ if (isVisible(mNameTextView)) {
+ // Caculate width for name text - this parallels similar measurement in onLayout.
+ int nameTextWidth = effectiveWidth;
+ if (mPhotoPosition != PhotoPosition.LEFT) {
+ nameTextWidth -= mTextIndent;
+ }
+ mNameTextView.measure(
+ MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mNameTextViewHeight = mNameTextView.getMeasuredHeight();
+ }
+
+ if (isVisible(mPhoneticNameTextView)) {
+ mPhoneticNameTextView.measure(
+ MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
+ }
+
+ // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
+ // we should ellipsize both using appropriate ratio.
+ final int dataWidth;
+ final int labelWidth;
+ if (isVisible(mDataView)) {
+ if (isVisible(mLabelView)) {
+ final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
+ dataWidth = ((totalWidth * mDataViewWidthWeight)
+ / (mDataViewWidthWeight + mLabelViewWidthWeight));
+ labelWidth = ((totalWidth * mLabelViewWidthWeight) /
+ (mDataViewWidthWeight + mLabelViewWidthWeight));
+ } else {
+ dataWidth = effectiveWidth;
+ labelWidth = 0;
+ }
+ } else {
+ dataWidth = 0;
+ if (isVisible(mLabelView)) {
+ labelWidth = effectiveWidth;
+ } else {
+ labelWidth = 0;
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mDataViewHeight = mDataView.getMeasuredHeight();
+ }
+
+ if (isVisible(mLabelView)) {
+ // For performance reason we don't want AT_MOST usually, but when the picture is
+ // on right, we need to use it anyway because mDataView is next to mLabelView.
+ final int mode = (mPhotoPosition == PhotoPosition.LEFT
+ ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST);
+ mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mLabelViewHeight = mLabelView.getMeasuredHeight();
+ }
+ mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.measure(
+ MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
+ }
+
+ // Status view height is the biggest of the text view and the presence icon
+ if (isVisible(mPresenceIcon)) {
+ mPresenceIcon.measure(
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
+ mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
+ }
+
+ if (isVisible(mStatusView)) {
+ // Presence and status are in a same row, so status will be affected by icon size.
+ final int statusWidth;
+ if (isVisible(mPresenceIcon)) {
+ statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
+ - mPresenceIconMargin);
+ } else {
+ statusWidth = effectiveWidth;
+ }
+ mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mStatusTextViewHeight =
+ Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
+ }
+
+ // Calculate height including padding.
+ int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
+ mLabelAndDataViewMaxHeight +
+ mSnippetTextViewHeight + mStatusTextViewHeight);
+
+ // Make sure the height is at least as high as the photo
+ height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
+
+ // Add horizontal divider height
+ if (mHorizontalDividerVisible) {
+ height += mHorizontalDividerHeight;
+ }
+
+ // Make sure height is at least the preferred height
+ height = Math.max(height, preferredHeight);
+
+ // Add the height of the header if visible
+ if (mHeaderVisible) {
+ mHeaderTextView.measure(
+ MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+ if (mCountView != null) {
+ mCountView.measure(
+ MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+ }
+ mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight,
+ mHeaderTextView.getMeasuredHeight());
+ height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+ }
+
+ setMeasuredDimension(specWidth, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int height = bottom - top;
+ final int width = right - left;
+
+ // Determine the vertical bounds by laying out the header first.
+ int topBound = 0;
+ int bottomBound = height;
+ int leftBound = getPaddingLeft();
+ int rightBound = width - getPaddingRight();
+
+ // Put the header in the top of the contact view (Text + underline view)
+ if (mHeaderVisible) {
+ mHeaderTextView.layout(leftBound + mHeaderTextIndent,
+ 0,
+ rightBound,
+ mHeaderBackgroundHeight);
+ if (mCountView != null) {
+ mCountView.layout(rightBound - mCountView.getMeasuredWidth(),
+ 0,
+ rightBound,
+ mHeaderBackgroundHeight);
+ }
+ mHeaderDivider.layout(leftBound,
+ mHeaderBackgroundHeight,
+ rightBound,
+ mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+ topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+ }
+
+ // Put horizontal divider at the bottom
+ if (mHorizontalDividerVisible) {
+ mHorizontalDividerDrawable.setBounds(
+ leftBound,
+ height - mHorizontalDividerHeight,
+ rightBound,
+ height);
+ bottomBound -= mHorizontalDividerHeight;
+ }
+
+ mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
+
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
+ }
+
+ final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // Photo is the left most view. All the other Views should on the right of the photo.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ leftBound,
+ photoTop,
+ leftBound + mPhotoViewWidth,
+ photoTop + mPhotoViewHeight);
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ } else if (mKeepHorizontalPaddingForPhotoView) {
+ // Draw nothing but keep the padding.
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ }
+ } else {
+ // Photo is the right most view. Right bound should be adjusted that way.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ rightBound - mPhotoViewWidth,
+ photoTop,
+ rightBound,
+ photoTop + mPhotoViewHeight);
+ rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+ }
+
+ // Add indent between left-most padding and texts.
+ leftBound += mTextIndent;
+ }
+
+ // Center text vertically
+ final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
+ mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
+ int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+ // Layout all text view and presence icon
+ // Put name TextView first
+ if (isVisible(mNameTextView)) {
+ mNameTextView.layout(leftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mNameTextViewHeight);
+ textTopBound += mNameTextViewHeight;
+ }
+
+ // Presence and status
+ int statusLeftBound = leftBound;
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ mPresenceIcon.layout(
+ leftBound,
+ textTopBound,
+ leftBound + iconWidth,
+ textTopBound + mStatusTextViewHeight);
+ statusLeftBound += (iconWidth + mPresenceIconMargin);
+ }
+
+ if (isVisible(mStatusView)) {
+ mStatusView.layout(statusLeftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mStatusTextViewHeight);
+ }
+
+ if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
+ textTopBound += mStatusTextViewHeight;
+ }
+
+ // Rest of text views
+ int dataLeftBound = leftBound;
+ if (isVisible(mPhoneticNameTextView)) {
+ mPhoneticNameTextView.layout(leftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mPhoneticNameTextViewHeight);
+ textTopBound += mPhoneticNameTextViewHeight;
+ }
+
+ // Label and Data align bottom.
+ if (isVisible(mLabelView)) {
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // When photo is on left, label is placed on the right edge of the list item.
+ mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ rightBound -= mLabelView.getMeasuredWidth();
+ } else {
+ // When photo is on right, label is placed on the left of data view.
+ dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+ mLabelView.layout(leftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ dataLeftBound += mGapBetweenLabelAndData;
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.layout(dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ }
+ if (isVisible(mLabelView) || isVisible(mDataView)) {
+ textTopBound += mLabelAndDataViewMaxHeight;
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.layout(leftBound,
+ textTopBound,
+ rightBound,
+ textTopBound + mSnippetTextViewHeight);
+ }
+ }
+
+ @Override
+ public void adjustListItemSelectionBounds(Rect bounds) {
+ bounds.top += mBoundsWithoutHeader.top;
+ bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
+ bounds.left += mSelectionBoundsMarginLeft;
+ bounds.right -= mSelectionBoundsMarginRight;
+ }
+
+ protected boolean isVisible(View view) {
+ return view != null && view.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Extracts width and height from the style
+ */
+ private void ensurePhotoViewSize() {
+ if (!mPhotoViewWidthAndHeightAreReady) {
+ mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+ if (!mQuickContactEnabled && mPhotoView == null) {
+ if (!mKeepHorizontalPaddingForPhotoView) {
+ mPhotoViewWidth = 0;
+ }
+ if (!mKeepVerticalPaddingForPhotoView) {
+ mPhotoViewHeight = 0;
+ }
+ }
+
+ mPhotoViewWidthAndHeightAreReady = true;
+ }
+ }
+
+ protected void setDefaultPhotoViewSize(int pixels) {
+ mDefaultPhotoViewSize = pixels;
+ }
+
+ protected int getDefaultPhotoViewSize() {
+ return mDefaultPhotoViewSize;
+ }
+
+ /**
+ * Gets a LayoutParam that corresponds to the default photo size.
+ *
+ * @return A new LayoutParam.
+ */
+ private LayoutParams getDefaultPhotoLayoutParams() {
+ LayoutParams params = generateDefaultLayoutParams();
+ params.width = getDefaultPhotoViewSize();
+ params.height = params.width;
+ return params;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.draw(canvas);
+ }
+ if (mHorizontalDividerVisible) {
+ mHorizontalDividerDrawable.draw(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+ }
+
+ /**
+ * Sets the flag that determines whether a divider should drawn at the bottom
+ * of the view.
+ */
+ public void setDividerVisible(boolean visible) {
+ mHorizontalDividerVisible = visible;
+ }
+
+ /**
+ * Sets section header or makes it invisible if the title is null.
+ */
+ public void setSectionHeader(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mHeaderTextView == null) {
+ mHeaderTextView = new TextView(mContext);
+ mHeaderTextView.setTextColor(mHeaderTextColor);
+ mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
+ mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
+ mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
+ addView(mHeaderTextView);
+ }
+ if (mHeaderDivider == null) {
+ mHeaderDivider = new View(mContext);
+ mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
+ addView(mHeaderDivider);
+ }
+ setMarqueeText(mHeaderTextView, title);
+ mHeaderTextView.setVisibility(View.VISIBLE);
+ mHeaderDivider.setVisibility(View.VISIBLE);
+ mHeaderTextView.setAllCaps(true);
+ mHeaderVisible = true;
+ } else {
+ if (mHeaderTextView != null) {
+ mHeaderTextView.setVisibility(View.GONE);
+ }
+ if (mHeaderDivider != null) {
+ mHeaderDivider.setVisibility(View.GONE);
+ }
+ mHeaderVisible = false;
+ }
+ }
+
+ /**
+ * Returns the quick contact badge, creating it if necessary.
+ */
+ public QuickContactBadge getQuickContact() {
+ if (!mQuickContactEnabled) {
+ throw new IllegalStateException("QuickContact is disabled for this view");
+ }
+ if (mQuickContact == null) {
+ mQuickContact = new QuickContactBadge(mContext);
+ mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
+ if (mNameTextView != null) {
+ mQuickContact.setContentDescription(mContext.getString(
+ R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+
+ addView(mQuickContact);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mQuickContact;
+ }
+
+ /**
+ * Returns the photo view, creating it if necessary.
+ */
+ public ImageView getPhotoView() {
+ if (mPhotoView == null) {
+ mPhotoView = new ImageView(mContext);
+ mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
+ // Quick contact style used above will set a background - remove it
+ mPhotoView.setBackground(null);
+ addView(mPhotoView);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mPhotoView;
+ }
+
+ /**
+ * Removes the photo view.
+ */
+ public void removePhotoView() {
+ removePhotoView(false, true);
+ }
+
+ /**
+ * Removes the photo view.
+ *
+ * @param keepHorizontalPadding True means data on the right side will have
+ * padding on left, pretending there is still a photo view.
+ * @param keepVerticalPadding True means the View will have some height
+ * enough for accommodating a photo view.
+ */
+ public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+ mPhotoViewWidthAndHeightAreReady = false;
+ mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+ mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
+ if (mPhotoView != null) {
+ removeView(mPhotoView);
+ mPhotoView = null;
+ }
+ if (mQuickContact != null) {
+ removeView(mQuickContact);
+ mQuickContact = null;
+ }
+ }
+
+ /**
+ * Sets a word prefix that will be highlighted if encountered in fields like
+ * name and search snippet.
+ * <p>
+ * NOTE: must be all upper-case
+ */
+ public void setHighlightedPrefix(char[] upperCasePrefix) {
+ mHighlightedPrefix = upperCasePrefix;
+ }
+
+ /**
+ * Returns the text view for the contact name, creating it if necessary.
+ */
+ public TextView getNameTextView() {
+ if (mNameTextView == null) {
+ mNameTextView = new TextView(mContext);
+ mNameTextView.setSingleLine(true);
+ mNameTextView.setEllipsize(getTextEllipsis());
+ mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+ // Manually call setActivated() since this view may be added after the first
+ // setActivated() call toward this whole item view.
+ mNameTextView.setActivated(isActivated());
+ mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+ addView(mNameTextView);
+ }
+ return mNameTextView;
+ }
+
+ /**
+ * Adds or updates a text view for the phonetic name.
+ */
+ public void setPhoneticName(char[] text, int size) {
+ if (text == null || size == 0) {
+ if (mPhoneticNameTextView != null) {
+ mPhoneticNameTextView.setVisibility(View.GONE);
+ }
+ } else {
+ getPhoneticNameTextView();
+ setMarqueeText(mPhoneticNameTextView, text, size);
+ mPhoneticNameTextView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the text view for the phonetic name, creating it if necessary.
+ */
+ public TextView getPhoneticNameTextView() {
+ if (mPhoneticNameTextView == null) {
+ mPhoneticNameTextView = new TextView(mContext);
+ mPhoneticNameTextView.setSingleLine(true);
+ mPhoneticNameTextView.setEllipsize(getTextEllipsis());
+ mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
+ mPhoneticNameTextView.setActivated(isActivated());
+ addView(mPhoneticNameTextView);
+ }
+ return mPhoneticNameTextView;
+ }
+
+ /**
+ * Adds or updates a text view for the data label.
+ */
+ public void setLabel(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mLabelView != null) {
+ mLabelView.setVisibility(View.GONE);
+ }
+ } else {
+ getLabelView();
+ setMarqueeText(mLabelView, text);
+ mLabelView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the text view for the data label, creating it if necessary.
+ */
+ public TextView getLabelView() {
+ if (mLabelView == null) {
+ mLabelView = new TextView(mContext);
+ mLabelView.setSingleLine(true);
+ mLabelView.setEllipsize(getTextEllipsis());
+ mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
+ mLabelView.setAllCaps(true);
+ mLabelView.setGravity(Gravity.RIGHT);
+ } else {
+ mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+ }
+ mLabelView.setActivated(isActivated());
+ addView(mLabelView);
+ }
+ return mLabelView;
+ }
+
+ /**
+ * Adds or updates a text view for the data element.
+ */
+ public void setData(char[] text, int size) {
+ if (text == null || size == 0) {
+ if (mDataView != null) {
+ mDataView.setVisibility(View.GONE);
+ }
+ } else {
+ getDataView();
+ setMarqueeText(mDataView, text, size);
+ mDataView.setVisibility(VISIBLE);
+ }
+ }
+
+ private void setMarqueeText(TextView textView, char[] text, int size) {
+ if (getTextEllipsis() == TruncateAt.MARQUEE) {
+ setMarqueeText(textView, new String(text, 0, size));
+ } else {
+ textView.setText(text, 0, size);
+ }
+ }
+
+ private void setMarqueeText(TextView textView, CharSequence text) {
+ if (getTextEllipsis() == TruncateAt.MARQUEE) {
+ // To show MARQUEE correctly (with END effect during non-active state), we need
+ // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
+ final SpannableString spannable = new SpannableString(text);
+ spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(spannable);
+ } else {
+ textView.setText(text);
+ }
+ }
+
+ /**
+ * Returns the text view for the data text, creating it if necessary.
+ */
+ public TextView getDataView() {
+ if (mDataView == null) {
+ mDataView = new TextView(mContext);
+ mDataView.setSingleLine(true);
+ mDataView.setEllipsize(getTextEllipsis());
+ mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mDataView.setActivated(isActivated());
+ addView(mDataView);
+ }
+ return mDataView;
+ }
+
+ /**
+ * Adds or updates a text view for the search snippet.
+ */
+ public void setSnippet(String text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mSnippetView != null) {
+ mSnippetView.setVisibility(View.GONE);
+ }
+ } else {
+ mPrefixHighlighter.setText(getSnippetView(), text, mHighlightedPrefix);
+ mSnippetView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the text view for the search snippet, creating it if necessary.
+ */
+ public TextView getSnippetView() {
+ if (mSnippetView == null) {
+ mSnippetView = new TextView(mContext);
+ mSnippetView.setSingleLine(true);
+ mSnippetView.setEllipsize(getTextEllipsis());
+ mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
+ mSnippetView.setActivated(isActivated());
+ addView(mSnippetView);
+ }
+ return mSnippetView;
+ }
+
+ /**
+ * Returns the text view for the status, creating it if necessary.
+ */
+ public TextView getStatusView() {
+ if (mStatusView == null) {
+ mStatusView = new TextView(mContext);
+ mStatusView.setSingleLine(true);
+ mStatusView.setEllipsize(getTextEllipsis());
+ mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+ mStatusView.setTextColor(mSecondaryTextColor);
+ mStatusView.setActivated(isActivated());
+ addView(mStatusView);
+ }
+ return mStatusView;
+ }
+
+ /**
+ * Returns the text view for the contacts count, creating it if necessary.
+ */
+ public TextView getCountView() {
+ if (mCountView == null) {
+ mCountView = new TextView(mContext);
+ mCountView.setSingleLine(true);
+ mCountView.setEllipsize(getTextEllipsis());
+ mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+ mCountView.setTextColor(R.color.contact_count_text_color);
+ addView(mCountView);
+ }
+ return mCountView;
+ }
+
+ /**
+ * Adds or updates a text view for the contacts count.
+ */
+ public void setCountView(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mCountView != null) {
+ mCountView.setVisibility(View.GONE);
+ }
+ } else {
+ getCountView();
+ setMarqueeText(mCountView, text);
+ mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
+ mCountView.setGravity(Gravity.CENTER_VERTICAL);
+ mCountView.setTextColor(mContactsCountTextColor);
+ mCountView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Adds or updates a text view for the status.
+ */
+ public void setStatus(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mStatusView != null) {
+ mStatusView.setVisibility(View.GONE);
+ }
+ } else {
+ getStatusView();
+ setMarqueeText(mStatusView, text);
+ mStatusView.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * Adds or updates the presence icon view.
+ */
+ public void setPresence(Drawable icon) {
+ if (icon != null) {
+ if (mPresenceIcon == null) {
+ mPresenceIcon = new ImageView(mContext);
+ addView(mPresenceIcon);
+ }
+ mPresenceIcon.setImageDrawable(icon);
+ mPresenceIcon.setScaleType(ScaleType.CENTER);
+ mPresenceIcon.setVisibility(View.VISIBLE);
+ } else {
+ if (mPresenceIcon != null) {
+ mPresenceIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private TruncateAt getTextEllipsis() {
+ return TruncateAt.MARQUEE;
+ }
+
+ public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
+ CharSequence name = cursor.getString(nameColumnIndex);
+ if (!TextUtils.isEmpty(name)) {
+ name = mPrefixHighlighter.apply(name, mHighlightedPrefix);
+ } else {
+ name = mUnknownNameText;
+ }
+ setMarqueeText(getNameTextView(), name);
+
+ // Since the quick contact content description is derived from the display name and there is
+ // no guarantee that when the quick contact is initialized the display name is already set,
+ // do it here too.
+ if (mQuickContact != null) {
+ mQuickContact.setContentDescription(mContext.getString(
+ R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+ }
+
+ public void hideDisplayName() {
+ if (mNameTextView != null) {
+ removeView(mNameTextView);
+ mNameTextView = null;
+ }
+ }
+
+ public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
+ cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
+ int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
+ if (phoneticNameSize != 0) {
+ setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
+ } else {
+ setPhoneticName(null, 0);
+ }
+ }
+
+ public void hidePhoneticName() {
+ if (mPhoneticNameTextView != null) {
+ removeView(mPhoneticNameTextView);
+ mPhoneticNameTextView = null;
+ }
+ }
+
+ /**
+ * Sets the proper icon (star or presence or nothing) and/or status message.
+ */
+ public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
+ int contactStatusColumnIndex) {
+ Drawable icon = null;
+ int presence = 0;
+ if (!cursor.isNull(presenceColumnIndex)) {
+ presence = cursor.getInt(presenceColumnIndex);
+ icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
+ }
+ setPresence(icon);
+
+ String statusMessage = null;
+ if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
+ statusMessage = cursor.getString(contactStatusColumnIndex);
+ }
+ // If there is no status message from the contact, but there was a presence value, then use
+ // the default status message string
+ if (statusMessage == null && presence != 0) {
+ statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
+ }
+ setStatus(statusMessage);
+ }
+
+ /**
+ * Shows search snippet.
+ */
+ public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
+ if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
+ setSnippet(null);
+ return;
+ }
+ String snippet;
+ String columnContent = cursor.getString(summarySnippetColumnIndex);
+
+ // Do client side snippeting if provider didn't do it
+ Bundle extras = cursor.getExtras();
+ if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
+ int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
+
+ snippet = ContactsContract.snippetize(columnContent,
+ displayNameIndex < 0 ? null : cursor.getString(displayNameIndex),
+ extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY),
+ DefaultContactListAdapter.SNIPPET_START_MATCH,
+ DefaultContactListAdapter.SNIPPET_END_MATCH,
+ DefaultContactListAdapter.SNIPPET_ELLIPSIS,
+ DefaultContactListAdapter.SNIPPET_MAX_TOKENS);
+ } else {
+ snippet = columnContent;
+ }
+
+ if (snippet != null) {
+ int from = 0;
+ int to = snippet.length();
+ int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
+ if (start == -1) {
+ snippet = null;
+ } else {
+ int firstNl = snippet.lastIndexOf('\n', start);
+ if (firstNl != -1) {
+ from = firstNl + 1;
+ }
+ int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
+ if (end != -1) {
+ int lastNl = snippet.indexOf('\n', end);
+ if (lastNl != -1) {
+ to = lastNl;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = from; i < to; i++) {
+ char c = snippet.charAt(i);
+ if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
+ c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
+ sb.append(c);
+ }
+ }
+ snippet = sb.toString();
+ }
+ }
+ setSnippet(snippet);
+ }
+
+ /**
+ * Shows data element (e.g. phone number).
+ */
+ public void showData(Cursor cursor, int dataColumnIndex) {
+ cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
+ setData(mDataBuffer.data, mDataBuffer.sizeCopied);
+ }
+
+ public void setActivatedStateSupported(boolean flag) {
+ this.mActivatedStateSupported = flag;
+ }
+
+ @Override
+ public void requestLayout() {
+ // We will assume that once measured this will not need to resize
+ // itself, so there is no need to pass the layout request to the parent
+ // view (ListView).
+ forceLayout();
+ }
+
+ public void setPhotoPosition(PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+ }
+
+ public PhotoPosition getPhotoPosition() {
+ return mPhotoPosition;
+ }
+
+ /**
+ * Specifies left and right margin for selection bounds. See also
+ * {@link #adjustListItemSelectionBounds(Rect)}.
+ */
+ public void setSelectionBoundsHorizontalMargin(int left, int right) {
+ mSelectionBoundsMarginLeft = left;
+ mSelectionBoundsMarginRight = right;
+ }
+}