/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * 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.silentcircle.contacts.list; import android.annotation.TargetApi; import android.content.Context; 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.Build; 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.TextView; //import com.android.contacts.ContactPresenceIconUtil; //import com.android.contacts.ContactStatusUtil; import com.silentcircle.contacts.utils.PrefixHighlighter; import com.silentcircle.contacts.widget.ScQuickContactBadge; import com.silentcircle.contacts.R; /** * 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 private final int mPreferredHeight; private final int mGapBetweenImageAndText; private final int mGapBetweenLabelAndData; private final int mPresenceIconMargin; private final int mPresenceIconSize; private final int mHeaderTextColor; private final int mHeaderTextIndent; private final int mHeaderTextSize; private final int mHeaderUnderlineHeight; private final int mHeaderUnderlineColor; private final int mCountViewTextSize; private final int mContactsCountTextColor; private final int mTextIndent; private Drawable mActivatedBackgroundDrawable; /** * Used with {@link #mLabelView}, specifying the width ratio between label and data. */ private final int mLabelViewWidthWeight; /** * Used with {@link #mDataView}, specifying the width ratio between label and data. */ private final int mDataViewWidthWeight; // 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; private TextView mHeaderTextView; // The views inside the contact view private boolean mQuickContactEnabled = true; private ScQuickContactBadge 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 final int mSecondaryTextColor; // private ColorStateList mSecondaryTextColor; private char[] mHighlightedPrefix; private int mDefaultPhotoViewSize; /** * 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; private Context mContext; // 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, 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, 0); 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, 0); mGapBetweenLabelAndData = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0); mPresenceIconMargin = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_presence_icon_margin, 4); mPresenceIconSize = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_presence_icon_size, 16); mDefaultPhotoViewSize = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_photo_size, 0); mHeaderTextIndent = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_header_text_indent, 0); mHeaderTextColor = a.getColor( R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK); mHeaderTextSize = a.getDimensionPixelSize( R.styleable.ContactListItemView_list_item_header_text_size, 12); mHeaderBackgroundHeight = a.getDimensionPixelSize( R.styleable.ContactListItemView_list_item_header_height, 30); mHeaderUnderlineHeight = a.getDimensionPixelSize( R.styleable.ContactListItemView_list_item_header_underline_height, 1); mHeaderUnderlineColor = a.getColor( R.styleable.ContactListItemView_list_item_header_underline_color, 0); mTextIndent = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_text_indent, 0); mCountViewTextSize = a.getDimensionPixelSize( R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12); mContactsCountTextColor = a.getColor( R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK); mDataViewWidthWeight = a.getInteger( R.styleable.ContactListItemView_list_item_data_width_weight, 5); mLabelViewWidthWeight = a.getInteger( R.styleable.ContactListItemView_list_item_label_width_weight, 3); 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)); mSecondaryTextColor = a.getColor(R.styleable.ContactListItemView_list_item_textColorSecondary, 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); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @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 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && 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); } } // @TargetApi(Build.VERSION_CODES.HONEYCOMB) // @Override // public void adjustListItemSelectionBounds(Rect bounds) { // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) // return; // 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); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void jumpDrawablesToCurrentState() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { super.jumpDrawablesToCurrentState(); if (mActivatedStateSupported) { mActivatedBackgroundDrawable.jumpToCurrentState(); } } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void dispatchDraw(Canvas canvas) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 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. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) 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 ScQuickContactBadge getQuickContact() { if (!mQuickContactEnabled) { throw new IllegalStateException("QuickContact is disabled for this view"); } if (mQuickContact == null) { mQuickContact = new ScQuickContactBadge(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 // TODO 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. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) 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. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 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. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 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. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) mLabelView.setAllCaps(true); mLabelView.setGravity(Gravity.RIGHT); } else { mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 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. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public TextView getDataView() { if (mDataView == null) { mDataView = new TextView(mContext); mDataView.setSingleLine(true); mDataView.setEllipsize(getTextEllipsis()); mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 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. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) mSnippetView.setActivated(isActivated()); addView(mSnippetView); } return mSnippetView; } /** * Returns the text view for the status, creating it if necessary. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) 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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) 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(mContactsCountTextColor); 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; if (!flag) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) setBackground(mActivatedBackgroundDrawable); else setBackgroundDrawable(mActivatedBackgroundDrawable); } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) setBackground(null); else setBackgroundDrawable(null); } } @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; } }