| /* |
| * 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.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.provider.ContactsContract.SearchSnippets; |
| 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.MotionEvent; |
| 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 androidx.appcompat.widget.AppCompatCheckBox; |
| import androidx.appcompat.widget.AppCompatImageButton; |
| import androidx.core.content.ContextCompat; |
| import androidx.core.content.res.ResourcesCompat; |
| import androidx.core.graphics.drawable.DrawableCompat; |
| import com.android.contacts.ContactPresenceIconUtil; |
| import com.android.contacts.ContactStatusUtil; |
| import com.android.contacts.R; |
| import com.android.contacts.compat.CompatUtils; |
| import com.android.contacts.compat.PhoneNumberUtilsCompat; |
| import com.android.contacts.format.TextHighlighter; |
| import com.android.contacts.util.ContactDisplayUtils; |
| import com.android.contacts.util.SearchUtil; |
| import com.android.contacts.util.ViewUtil; |
| import com.google.common.collect.Lists; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * 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 { |
| |
| private static final String TAG = "ContactListItemView"; |
| |
| // 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 mGapBetweenIndexerAndImage = 0; |
| private int mGapBetweenLabelAndData = 0; |
| private int mPresenceIconMargin = 4; |
| private int mPresenceIconSize = 16; |
| private int mTextIndent = 0; |
| private int mTextOffsetTop; |
| private int mAvatarOffsetTop; |
| private int mNameTextViewTextSize; |
| private int mHeaderWidth; |
| private Drawable mActivatedBackgroundDrawable; |
| private int mVideoCallIconSize = 32; |
| private int mVideoCallIconMargin = 16; |
| private int mGapFromScrollBar = 20; |
| |
| // Set in onLayout. Represent left and right position of the View on the screen. |
| private int mLeftOffset; |
| private int mRightOffset; |
| |
| /** |
| * 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; |
| |
| protected static class HighlightSequence { |
| private final int start; |
| private final int end; |
| |
| HighlightSequence(int start, int end) { |
| this.start = start; |
| this.end = end; |
| } |
| } |
| |
| private ArrayList<HighlightSequence> mNameHighlightSequence; |
| private ArrayList<HighlightSequence> mNumberHighlightSequence; |
| |
| // Highlighting prefix for names. |
| private String mHighlightedPrefix; |
| |
| /** |
| * Used to notify listeners when a video call icon is clicked. |
| */ |
| private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener; |
| |
| /** |
| * Indicates whether to show the "video call" icon, used to initiate a video call. |
| */ |
| private boolean mShowVideoCallIcon = false; |
| |
| /** |
| * Indicates whether the view should leave room for the "video call" icon. |
| */ |
| private boolean mSupportVideoCallIcon = false; |
| |
| /** |
| * Where to put contact photo. This affects the other Views' layout or look-and-feel. |
| * |
| * TODO: replace enum with int constants |
| */ |
| public enum PhotoPosition { |
| LEFT, |
| RIGHT |
| } |
| |
| static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) { |
| final Locale locale = Locale.getDefault(); |
| final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); |
| switch (layoutDirection) { |
| case View.LAYOUT_DIRECTION_RTL: |
| return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT); |
| case View.LAYOUT_DIRECTION_LTR: |
| default: |
| return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT); |
| } |
| } |
| |
| private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */); |
| |
| // Header layout data |
| private View mHeaderView; |
| private boolean mIsSectionHeaderEnabled; |
| |
| // 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 ImageView mPresenceIcon; |
| private AppCompatCheckBox mCheckBox; |
| private AppCompatImageButton mDeleteImageButton; |
| private ImageView mVideoCallIcon; |
| private ImageView mWorkProfileIcon; |
| |
| private ColorStateList mSecondaryTextColor; |
| |
| 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 mNameTextViewTextColor = Color.BLACK; |
| private int mPhoneticNameTextViewHeight; |
| private int mLabelViewHeight; |
| private int mDataViewHeight; |
| private int mSnippetTextViewHeight; |
| private int mStatusTextViewHeight; |
| private int mCheckBoxHeight; |
| private int mCheckBoxWidth; |
| private int mDeleteImageButtonHeight; |
| private int mDeleteImageButtonWidth; |
| |
| // 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 boolean mAdjustSelectionBoundsEnabled = true; |
| |
| private Rect mBoundsWithoutHeader = new Rect(); |
| |
| /** A helper used to highlight a prefix in a text field. */ |
| private final TextHighlighter mTextHighlighter; |
| private CharSequence mUnknownNameText; |
| private int mPosition; |
| |
| public ContactListItemView(Context context) { |
| super(context); |
| |
| mTextHighlighter = new TextHighlighter(Typeface.BOLD); |
| mNameHighlightSequence = new ArrayList<HighlightSequence>(); |
| mNumberHighlightSequence = new ArrayList<HighlightSequence>(); |
| } |
| |
| public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) { |
| this(context, attrs); |
| |
| mSupportVideoCallIcon = supportVideoCallIcon; |
| } |
| |
| public ContactListItemView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| |
| TypedArray a; |
| |
| if (R.styleable.ContactListItemView != null) { |
| // Read all style values |
| 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); |
| |
| mGapBetweenImageAndText = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_gap_between_image_and_text, |
| mGapBetweenImageAndText); |
| mGapBetweenIndexerAndImage = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_gap_between_indexer_and_image, |
| mGapBetweenIndexerAndImage); |
| 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); |
| mTextIndent = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); |
| mTextOffsetTop = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); |
| mAvatarOffsetTop = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_avatar_offset_top, mAvatarOffsetTop); |
| mDataViewWidthWeight = a.getInteger( |
| R.styleable.ContactListItemView_list_item_data_width_weight, |
| mDataViewWidthWeight); |
| mLabelViewWidthWeight = a.getInteger( |
| R.styleable.ContactListItemView_list_item_label_width_weight, |
| mLabelViewWidthWeight); |
| mNameTextViewTextColor = a.getColor( |
| R.styleable.ContactListItemView_list_item_name_text_color, |
| mNameTextViewTextColor); |
| mNameTextViewTextSize = (int) a.getDimension( |
| R.styleable.ContactListItemView_list_item_name_text_size, |
| (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); |
| mVideoCallIconSize = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_video_call_icon_size, |
| mVideoCallIconSize); |
| mVideoCallIconMargin = a.getDimensionPixelOffset( |
| R.styleable.ContactListItemView_list_item_video_call_icon_margin, |
| mVideoCallIconMargin); |
| |
| |
| setPaddingRelative( |
| 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)); |
| |
| a.recycle(); |
| } |
| |
| mTextHighlighter = new TextHighlighter(Typeface.BOLD); |
| |
| if (R.styleable.Theme != null) { |
| a = getContext().obtainStyledAttributes(R.styleable.Theme); |
| mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); |
| a.recycle(); |
| } |
| |
| mHeaderWidth = |
| getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); |
| |
| if (mActivatedBackgroundDrawable != null) { |
| mActivatedBackgroundDrawable.setCallback(this); |
| } |
| |
| mNameHighlightSequence = new ArrayList<HighlightSequence>(); |
| mNumberHighlightSequence = new ArrayList<HighlightSequence>(); |
| |
| setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); |
| } |
| |
| public void setUnknownNameText(CharSequence unknownNameText) { |
| mUnknownNameText = unknownNameText; |
| } |
| |
| public void setQuickContactEnabled(boolean flag) { |
| mQuickContactEnabled = flag; |
| } |
| |
| /** |
| * Sets whether the video calling icon is shown. For the video calling icon to be shown, |
| * {@link #mSupportVideoCallIcon} must be {@code true}. |
| * |
| * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false} |
| * otherwise. |
| * @param listener Listener to notify when the video calling icon is clicked. |
| * @param position The position in the adapater of the video calling icon. |
| */ |
| public void setShowVideoCallIcon(boolean showVideoCallIcon, |
| PhoneNumberListAdapter.Listener listener, int position) { |
| mShowVideoCallIcon = showVideoCallIcon; |
| mPhoneNumberListAdapterListener = listener; |
| mPosition = position; |
| |
| if (mShowVideoCallIcon) { |
| if (mVideoCallIcon == null) { |
| mVideoCallIcon = new ImageView(getContext()); |
| addView(mVideoCallIcon); |
| } |
| mVideoCallIcon.setContentDescription(getContext().getString( |
| R.string.description_search_video_call)); |
| mVideoCallIcon.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); |
| mVideoCallIcon.setScaleType(ScaleType.CENTER); |
| mVideoCallIcon.setVisibility(View.VISIBLE); |
| mVideoCallIcon.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| // Inform the adapter that the video calling icon was clicked. |
| if (mPhoneNumberListAdapterListener != null) { |
| mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition); |
| } |
| } |
| }); |
| } else { |
| if (mVideoCallIcon != null) { |
| mVideoCallIcon.setVisibility(View.GONE); |
| } |
| } |
| } |
| |
| /** |
| * Sets whether the view supports a video calling icon. This is independent of whether the view |
| * is actually showing an icon. Support for the video calling icon ensures that the layout |
| * leaves space for the video icon, should it be shown. |
| * |
| * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false} |
| * otherwise. |
| */ |
| public void setSupportVideoCallIcon(boolean supportVideoCallIcon) { |
| mSupportVideoCallIcon = supportVideoCallIcon; |
| } |
| |
| @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 = mPreferredHeight; |
| |
| mNameTextViewHeight = 0; |
| mPhoneticNameTextViewHeight = 0; |
| mLabelViewHeight = 0; |
| mDataViewHeight = 0; |
| mLabelAndDataViewMaxHeight = 0; |
| mSnippetTextViewHeight = 0; |
| mStatusTextViewHeight = 0; |
| mCheckBoxWidth = 0; |
| mCheckBoxHeight = 0; |
| mDeleteImageButtonWidth = 0; |
| mDeleteImageButtonHeight = 0; |
| |
| ensurePhotoViewSize(); |
| |
| // Width each TextView is able to use. |
| 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 + mGapBetweenIndexerAndImage); |
| } else { |
| effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); |
| } |
| |
| if (mIsSectionHeaderEnabled) { |
| effectiveWidth -= mHeaderWidth; |
| } |
| |
| if (mSupportVideoCallIcon) { |
| effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin); |
| } |
| |
| // 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(mCheckBox)) { |
| mCheckBox.measure( |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
| mCheckBoxWidth = mCheckBox.getMeasuredWidth(); |
| mCheckBoxHeight = mCheckBox.getMeasuredHeight(); |
| effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText; |
| } |
| |
| if (isVisible(mDeleteImageButton)) { |
| mDeleteImageButton.measure( |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
| mDeleteImageButtonWidth = mDeleteImageButton.getMeasuredWidth(); |
| mDeleteImageButtonHeight = mDeleteImageButton.getMeasuredHeight(); |
| effectiveWidth -= mDeleteImageButtonWidth + mGapBetweenImageAndText; |
| } |
| |
| if (isVisible(mNameTextView)) { |
| // Calculate 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)) { |
| mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST), |
| 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 (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) { |
| mVideoCallIcon.measure( |
| MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY)); |
| } |
| |
| if (isVisible(mWorkProfileIcon)) { |
| mWorkProfileIcon.measure( |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
| mNameTextViewHeight = |
| Math.max(mNameTextViewHeight, mWorkProfileIcon.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 |
| + getPaddingBottom() + getPaddingTop()); |
| |
| // Make sure the height is at least as high as the photo |
| height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); |
| |
| // Make sure height is at least the preferred height |
| height = Math.max(height, preferredHeight); |
| |
| // Measure the header if it is visible. |
| if (mHeaderView != null && mHeaderView.getVisibility() == VISIBLE) { |
| mHeaderView.measure( |
| MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); |
| } |
| |
| 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(); |
| |
| final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); |
| |
| // Put the section header on the left side of the contact view. |
| if (mIsSectionHeaderEnabled) { |
| if (mHeaderView != null) { |
| int headerHeight = mHeaderView.getMeasuredHeight(); |
| int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; |
| |
| mHeaderView.layout( |
| isLayoutRtl ? rightBound - mHeaderWidth : leftBound, |
| headerTopBound, |
| isLayoutRtl ? rightBound : leftBound + mHeaderWidth, |
| headerTopBound + headerHeight); |
| } |
| if (isLayoutRtl) { |
| rightBound -= mHeaderWidth; |
| } else { |
| leftBound += mHeaderWidth; |
| } |
| } |
| |
| mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); |
| mLeftOffset = left + leftBound; |
| mRightOffset = left + rightBound; |
| if (isLayoutRtl) { |
| rightBound -= mGapBetweenIndexerAndImage; |
| } else { |
| leftBound += mGapBetweenIndexerAndImage; |
| } |
| |
| if (mActivatedStateSupported && isActivated()) { |
| mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); |
| } |
| |
| if (isVisible(mCheckBox)) { |
| final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2; |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| mCheckBox.layout(rightBound - mGapFromScrollBar - mCheckBoxWidth, |
| photoTop, |
| rightBound - mGapFromScrollBar, |
| photoTop + mCheckBoxHeight); |
| } else { |
| mCheckBox.layout(leftBound + mGapFromScrollBar, |
| photoTop, |
| leftBound + mGapFromScrollBar + mCheckBoxWidth, |
| photoTop + mCheckBoxHeight); |
| } |
| } |
| |
| if (isVisible(mDeleteImageButton)) { |
| final int photoTop = topBound + (bottomBound - topBound - mDeleteImageButtonHeight) / 2; |
| final int mDeleteImageButtonSize = mDeleteImageButtonHeight > mDeleteImageButtonWidth |
| ? mDeleteImageButtonHeight : mDeleteImageButtonWidth; |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| mDeleteImageButton.layout(rightBound - mDeleteImageButtonSize, |
| photoTop, |
| rightBound, |
| photoTop + mDeleteImageButtonSize); |
| rightBound -= mDeleteImageButtonSize; |
| } else { |
| mDeleteImageButton.layout(leftBound, |
| photoTop, |
| leftBound + mDeleteImageButtonSize, |
| photoTop + mDeleteImageButtonSize); |
| leftBound += mDeleteImageButtonSize; |
| } |
| } |
| |
| 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 |
| + mAvatarOffsetTop; |
| 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 |
| + mAvatarOffsetTop; |
| photoView.layout( |
| rightBound - mPhotoViewWidth, |
| photoTop, |
| rightBound, |
| photoTop + mPhotoViewHeight); |
| rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); |
| } else if (mKeepHorizontalPaddingForPhotoView) { |
| // Draw nothing but keep the padding. |
| rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); |
| } |
| |
| // Add indent between left-most padding and texts. |
| leftBound += mTextIndent; |
| } |
| |
| if (mSupportVideoCallIcon) { |
| // Place the video call button at the end of the list (e.g. take into account RTL mode). |
| if (isVisible(mVideoCallIcon)) { |
| // Center the video icon vertically |
| final int videoIconTop = topBound + |
| (bottomBound - topBound - mVideoCallIconSize) / 2; |
| |
| if (!isLayoutRtl) { |
| // When photo is on left, video icon is placed on the right edge. |
| mVideoCallIcon.layout(rightBound - mVideoCallIconSize, |
| videoIconTop, |
| rightBound, |
| videoIconTop + mVideoCallIconSize); |
| } else { |
| // When photo is on right, video icon is placed on the left edge. |
| mVideoCallIcon.layout(leftBound, |
| videoIconTop, |
| leftBound + mVideoCallIconSize, |
| videoIconTop + mVideoCallIconSize); |
| } |
| } |
| |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| rightBound -= (mVideoCallIconSize + mVideoCallIconMargin); |
| } else { |
| leftBound += mVideoCallIconSize + mVideoCallIconMargin; |
| } |
| } |
| |
| |
| // Center text vertically, then apply the top offset. |
| final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + |
| mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight; |
| int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; |
| |
| // Work Profile icon align top |
| int workProfileIconWidth = 0; |
| if (isVisible(mWorkProfileIcon)) { |
| workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth(); |
| final int distanceFromEnd = mCheckBoxWidth > 0 |
| ? mCheckBoxWidth + mGapBetweenImageAndText : 0; |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| // When photo is on left, label is placed on the right edge of the list item. |
| mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd, |
| textTopBound, |
| rightBound - distanceFromEnd, |
| textTopBound + mNameTextViewHeight); |
| } else { |
| // When photo is on right, label is placed on the left of data view. |
| mWorkProfileIcon.layout(leftBound + distanceFromEnd, |
| textTopBound, |
| leftBound + workProfileIconWidth + distanceFromEnd, |
| textTopBound + mNameTextViewHeight); |
| } |
| } |
| |
| // Layout all text view and presence icon |
| // Put name TextView first |
| if (isVisible(mNameTextView)) { |
| final int distanceFromEnd = workProfileIconWidth |
| + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0); |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| mNameTextView.layout(leftBound, |
| textTopBound, |
| rightBound - distanceFromEnd, |
| textTopBound + mNameTextViewHeight); |
| } else { |
| mNameTextView.layout(leftBound + distanceFromEnd, |
| textTopBound, |
| rightBound, |
| textTopBound + mNameTextViewHeight); |
| } |
| } |
| |
| if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) { |
| textTopBound += mNameTextViewHeight; |
| } |
| |
| // Presence and status |
| if (isLayoutRtl) { |
| int statusRightBound = rightBound; |
| if (isVisible(mPresenceIcon)) { |
| int iconWidth = mPresenceIcon.getMeasuredWidth(); |
| mPresenceIcon.layout( |
| rightBound - iconWidth, |
| textTopBound, |
| rightBound, |
| textTopBound + mStatusTextViewHeight); |
| statusRightBound -= (iconWidth + mPresenceIconMargin); |
| } |
| |
| if (isVisible(mStatusView)) { |
| mStatusView.layout(leftBound, |
| textTopBound, |
| statusRightBound, |
| textTopBound + mStatusTextViewHeight); |
| } |
| } else { |
| 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 (!isLayoutRtl) { |
| mLabelView.layout(dataLeftBound, |
| textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, |
| rightBound, |
| textTopBound + mLabelAndDataViewMaxHeight); |
| dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData; |
| } else { |
| dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); |
| mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(), |
| textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, |
| rightBound, |
| textTopBound + mLabelAndDataViewMaxHeight); |
| rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData); |
| } |
| } |
| |
| if (isVisible(mDataView)) { |
| if (!isLayoutRtl) { |
| mDataView.layout(dataLeftBound, |
| textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, |
| rightBound, |
| textTopBound + mLabelAndDataViewMaxHeight); |
| } else { |
| mDataView.layout(rightBound - mDataView.getMeasuredWidth(), |
| 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) { |
| if (mAdjustSelectionBoundsEnabled) { |
| bounds.top += mBoundsWithoutHeader.top; |
| bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); |
| bounds.left = mBoundsWithoutHeader.left; |
| bounds.right = mBoundsWithoutHeader.right; |
| } |
| } |
| |
| 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 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); |
| } |
| |
| super.dispatchDraw(canvas); |
| } |
| |
| /** |
| * Sets section header or makes it invisible if the title is null. |
| */ |
| public void setSectionHeader(String title) { |
| if (title != null) { |
| // Empty section title is the favorites so show the star here. |
| if (title.isEmpty()) { |
| if (mHeaderView == null) { |
| addStarImageHeader(); |
| } else if (mHeaderView instanceof TextView) { |
| removeView(mHeaderView); |
| addStarImageHeader(); |
| } else { |
| mHeaderView.setVisibility(View.VISIBLE); |
| } |
| } else { |
| if (mHeaderView == null) { |
| addTextHeader(title); |
| } else if (mHeaderView instanceof ImageView) { |
| removeView(mHeaderView); |
| addTextHeader(title); |
| } else { |
| updateHeaderText((TextView) mHeaderView, title); |
| } |
| } |
| } else if (mHeaderView != null) { |
| mHeaderView.setVisibility(View.GONE); |
| } |
| } |
| |
| private void addTextHeader(String title) { |
| mHeaderView = new TextView(getContext()); |
| final TextView headerTextView = (TextView) mHeaderView; |
| headerTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); |
| headerTextView.setGravity(Gravity.CENTER_HORIZONTAL); |
| updateHeaderText(headerTextView, title); |
| addView(headerTextView); |
| } |
| |
| private void updateHeaderText(TextView headerTextView, String title) { |
| setMarqueeText(headerTextView, title); |
| headerTextView.setAllCaps(true); |
| if (ContactsSectionIndexer.BLANK_HEADER_STRING.equals(title)) { |
| headerTextView.setContentDescription( |
| getContext().getString(R.string.description_no_name_header)); |
| } else { |
| headerTextView.setContentDescription(title); |
| } |
| headerTextView.setVisibility(View.VISIBLE); |
| } |
| |
| private void addStarImageHeader() { |
| mHeaderView = new ImageView(getContext()); |
| final ImageView headerImageView = (ImageView) mHeaderView; |
| headerImageView.setImageDrawable( |
| getResources().getDrawable(R.drawable.quantum_ic_star_vd_theme_24, |
| getContext().getTheme())); |
| headerImageView.setImageTintList(ColorStateList.valueOf(getResources() |
| .getColor(R.color.material_star_pink))); |
| headerImageView.setContentDescription( |
| getContext().getString(R.string.contactsFavoritesLabel)); |
| headerImageView.setVisibility(View.VISIBLE); |
| addView(headerImageView); |
| } |
| |
| public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { |
| mIsSectionHeaderEnabled = isSectionHeaderEnabled; |
| } |
| |
| /** |
| * 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(getContext()); |
| if (CompatUtils.isLollipopCompatible()) { |
| mQuickContact.setOverlay(null); |
| } |
| mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); |
| if (mNameTextView != null) { |
| mQuickContact.setContentDescription(getContext().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(getContext()); |
| 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. This will disable the mask highlighting for names. |
| * <p> |
| * NOTE: must be all upper-case |
| */ |
| public void setHighlightedPrefix(String upperCasePrefix) { |
| mHighlightedPrefix = upperCasePrefix; |
| } |
| |
| /** |
| * Clears previously set highlight sequences for the view. |
| */ |
| public void clearHighlightSequences() { |
| mNameHighlightSequence.clear(); |
| mNumberHighlightSequence.clear(); |
| mHighlightedPrefix = null; |
| } |
| |
| /** |
| * Adds a highlight sequence to the name highlighter. |
| * @param start The start position of the highlight sequence. |
| * @param end The end position of the highlight sequence. |
| */ |
| public void addNameHighlightSequence(int start, int end) { |
| mNameHighlightSequence.add(new HighlightSequence(start, end)); |
| } |
| |
| /** |
| * Adds a highlight sequence to the number highlighter. |
| * @param start The start position of the highlight sequence. |
| * @param end The end position of the highlight sequence. |
| */ |
| public void addNumberHighlightSequence(int start, int end) { |
| mNumberHighlightSequence.add(new HighlightSequence(start, end)); |
| } |
| |
| /** |
| * Returns the text view for the contact name, creating it if necessary. |
| */ |
| public TextView getNameTextView() { |
| if (mNameTextView == null) { |
| mNameTextView = new TextView(getContext()); |
| mNameTextView.setSingleLine(true); |
| mNameTextView.setEllipsize(getTextEllipsis()); |
| mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(), |
| R.color.contact_list_name_text_color, getContext().getTheme())); |
| mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize); |
| // 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); |
| mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| mNameTextView.setId(R.id.cliv_name_textview); |
| if (CompatUtils.isLollipopCompatible()) { |
| mNameTextView.setElegantTextHeight(false); |
| } |
| 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(getContext()); |
| mPhoneticNameTextView.setSingleLine(true); |
| mPhoneticNameTextView.setEllipsize(getTextEllipsis()); |
| mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); |
| mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); |
| mPhoneticNameTextView.setActivated(isActivated()); |
| mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); |
| 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(getContext()); |
| mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, |
| LayoutParams.WRAP_CONTENT)); |
| |
| mLabelView.setSingleLine(true); |
| mLabelView.setEllipsize(getTextEllipsis()); |
| mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); |
| if (mPhotoPosition == PhotoPosition.LEFT) { |
| mLabelView.setAllCaps(true); |
| } else { |
| mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); |
| } |
| mLabelView.setActivated(isActivated()); |
| mLabelView.setId(R.id.cliv_label_textview); |
| 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); |
| } |
| } |
| |
| /** |
| * Sets phone number for a list item. This takes care of number highlighting if the highlight |
| * mask exists. |
| */ |
| public void setPhoneNumber(String text, String countryIso) { |
| if (text == null) { |
| if (mDataView != null) { |
| mDataView.setVisibility(View.GONE); |
| } |
| } else { |
| getDataView(); |
| |
| // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to |
| // mDataView. Make sure that determination of the highlight sequences are done only |
| // after number formatting. |
| |
| // Sets phone number texts for display after highlighting it, if applicable. |
| // CharSequence textToSet = text; |
| final SpannableString textToSet = new SpannableString(text); |
| |
| if (mNumberHighlightSequence.size() != 0) { |
| final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); |
| mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start, |
| highlightSequence.end); |
| } |
| |
| setMarqueeText(mDataView, textToSet); |
| mDataView.setVisibility(VISIBLE); |
| |
| // We have a phone number as "mDataView" so make it always LTR and VIEW_START |
| mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); |
| mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| } |
| } |
| |
| 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 {@link AppCompatCheckBox} view, creating it if necessary. |
| */ |
| public AppCompatCheckBox getCheckBox() { |
| if (mCheckBox == null) { |
| mCheckBox = new AppCompatCheckBox(getContext()); |
| // Make non-focusable, so the rest of the ContactListItemView can be clicked. |
| mCheckBox.setFocusable(false); |
| addView(mCheckBox); |
| } |
| return mCheckBox; |
| } |
| |
| /** |
| * Returns the {@link AppCompatImageButton} delete button, creating it if necessary. |
| */ |
| public AppCompatImageButton getDeleteImageButton( |
| final MultiSelectEntryContactListAdapter.DeleteContactListener listener, |
| final int position) { |
| if (mDeleteImageButton == null) { |
| mDeleteImageButton = new AppCompatImageButton(getContext()); |
| mDeleteImageButton.setImageResource(R.drawable.quantum_ic_cancel_vd_theme_24); |
| mDeleteImageButton.setScaleType(ScaleType.CENTER); |
| mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT); |
| mDeleteImageButton.setContentDescription( |
| getResources().getString(R.string.description_delete_contact)); |
| if (CompatUtils. isLollipopCompatible()) { |
| final TypedValue typedValue = new TypedValue(); |
| getContext().getTheme().resolveAttribute( |
| android.R.attr.selectableItemBackgroundBorderless, typedValue, true); |
| mDeleteImageButton.setBackgroundResource(typedValue.resourceId); |
| } |
| addView(mDeleteImageButton); |
| } |
| // Reset onClickListener because after reloading the view, position might be changed. |
| mDeleteImageButton.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| // Inform the adapter that delete icon was clicked. |
| if (listener != null) { |
| listener.onContactDeleteClicked(position); |
| } |
| } |
| }); |
| return mDeleteImageButton; |
| } |
| |
| /** |
| * Returns the text view for the data text, creating it if necessary. |
| */ |
| public TextView getDataView() { |
| if (mDataView == null) { |
| mDataView = new TextView(getContext()); |
| mDataView.setSingleLine(true); |
| mDataView.setEllipsize(getTextEllipsis()); |
| mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); |
| mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| mDataView.setActivated(isActivated()); |
| mDataView.setId(R.id.cliv_data_view); |
| if (CompatUtils.isLollipopCompatible()) { |
| mDataView.setElegantTextHeight(false); |
| } |
| 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 { |
| mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); |
| mSnippetView.setVisibility(VISIBLE); |
| if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { |
| // Give the text-to-speech engine a hint that it's a phone number |
| mSnippetView.setContentDescription( |
| PhoneNumberUtilsCompat.createTtsSpannable(text)); |
| } else { |
| mSnippetView.setContentDescription(null); |
| } |
| } |
| } |
| |
| /** |
| * Returns the text view for the search snippet, creating it if necessary. |
| */ |
| public TextView getSnippetView() { |
| if (mSnippetView == null) { |
| mSnippetView = new TextView(getContext()); |
| mSnippetView.setSingleLine(true); |
| mSnippetView.setEllipsize(getTextEllipsis()); |
| mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); |
| mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| 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(getContext()); |
| mStatusView.setSingleLine(true); |
| mStatusView.setEllipsize(getTextEllipsis()); |
| mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); |
| mStatusView.setTextColor(mSecondaryTextColor); |
| mStatusView.setActivated(isActivated()); |
| mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); |
| addView(mStatusView); |
| } |
| return mStatusView; |
| } |
| |
| /** |
| * 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(getContext()); |
| addView(mPresenceIcon); |
| } |
| mPresenceIcon.setImageDrawable(icon); |
| mPresenceIcon.setScaleType(ScaleType.CENTER); |
| mPresenceIcon.setVisibility(View.VISIBLE); |
| } else { |
| if (mPresenceIcon != null) { |
| mPresenceIcon.setVisibility(View.GONE); |
| } |
| } |
| } |
| |
| /** |
| * Set to display work profile icon or not |
| * |
| * @param enabled set to display work profile icon or not |
| */ |
| public void setWorkProfileIconEnabled(boolean enabled) { |
| if (mWorkProfileIcon != null) { |
| mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); |
| } else if (enabled) { |
| mWorkProfileIcon = new ImageView(getContext()); |
| addView(mWorkProfileIcon); |
| mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile); |
| mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE); |
| mWorkProfileIcon.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| private TruncateAt getTextEllipsis() { |
| return TruncateAt.MARQUEE; |
| } |
| |
| public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) { |
| CharSequence name = cursor.getString(nameColumnIndex); |
| setDisplayName(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(getContext().getString( |
| R.string.description_quick_contact_for, mNameTextView.getText())); |
| } |
| } |
| |
| public void setDisplayName(CharSequence name, boolean highlight) { |
| if (!TextUtils.isEmpty(name) && highlight) { |
| clearHighlightSequences(); |
| addNameHighlightSequence(0, name.length()); |
| } |
| setDisplayName(name); |
| } |
| |
| public void setDisplayName(CharSequence name) { |
| if (!TextUtils.isEmpty(name)) { |
| // Chooses the available highlighting method for highlighting. |
| if (mHighlightedPrefix != null) { |
| name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); |
| } else if (mNameHighlightSequence.size() != 0) { |
| final SpannableString spannableName = new SpannableString(name); |
| for (HighlightSequence highlightSequence : mNameHighlightSequence) { |
| mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start, |
| highlightSequence.end); |
| } |
| name = spannableName; |
| } |
| } else { |
| name = mUnknownNameText; |
| } |
| setMarqueeText(getNameTextView(), name); |
| |
| if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { |
| // Give the text-to-speech engine a hint that it's a phone number |
| mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR); |
| mNameTextView.setContentDescription( |
| PhoneNumberUtilsCompat.createTtsSpannable(name.toString())); |
| } else { |
| // Remove span tags of highlighting for talkback to avoid reading highlighting and rest |
| // of the name into two separate parts. |
| mNameTextView.setContentDescription(name.toString()); |
| } |
| } |
| |
| public void hideCheckBox() { |
| if (mCheckBox != null) { |
| removeView(mCheckBox); |
| mCheckBox = null; |
| } |
| } |
| |
| public void hideDeleteImageButton() { |
| if (mDeleteImageButton != null) { |
| removeView(mDeleteImageButton); |
| mDeleteImageButton = null; |
| } |
| } |
| |
| 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 for email and phone number matches. |
| */ |
| public void showSnippet(Cursor cursor, String query, int snippetColumn) { |
| // TODO: this does not properly handle phone numbers with control characters |
| // For example if the phone number is 444-5555, the search query 4445 will match the |
| // number since we normalize it before querying CP2 but the snippet will fail since |
| // the portion to be highlighted is 444-5 not 4445. |
| final String snippet = cursor.getString(snippetColumn); |
| if (snippet == null) { |
| setSnippet(null); |
| return; |
| } |
| final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0 |
| ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null; |
| if (snippet.equals(displayName)) { |
| // If the snippet exactly matches the display name (i.e. the phone number or email |
| // address is being used as the display name) then no snippet is necessary |
| setSnippet(null); |
| return; |
| } |
| // Show the snippet with the part of the query that matched it |
| setSnippet(updateSnippet(snippet, query, displayName)); |
| } |
| |
| /** |
| * Shows search snippet. |
| */ |
| public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { |
| if (cursor.getColumnCount() <= summarySnippetColumnIndex |
| || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) { |
| setSnippet(null); |
| return; |
| } |
| |
| String snippet = cursor.getString(summarySnippetColumnIndex); |
| |
| // Do client side snippeting if provider didn't do it |
| final Bundle extras = cursor.getExtras(); |
| if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { |
| |
| final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); |
| |
| String displayName = null; |
| int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); |
| if (displayNameIndex >= 0) { |
| displayName = cursor.getString(displayNameIndex); |
| } |
| |
| snippet = updateSnippet(snippet, query, displayName); |
| |
| } else { |
| 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); |
| } |
| |
| /** |
| * Used for deferred snippets from the database. The contents come back as large strings which |
| * need to be extracted for display. |
| * |
| * @param snippet The snippet from the database. |
| * @param query The search query substring. |
| * @param displayName The contact display name. |
| * @return The proper snippet to display. |
| */ |
| private String updateSnippet(String snippet, String query, String displayName) { |
| |
| if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { |
| return null; |
| } |
| query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); |
| |
| // If the display name already contains the query term, return empty - snippets should |
| // not be needed in that case. |
| if (!TextUtils.isEmpty(displayName)) { |
| final String lowerDisplayName = displayName.toLowerCase(); |
| final List<String> nameTokens = split(lowerDisplayName); |
| for (String nameToken : nameTokens) { |
| if (nameToken.startsWith(query)) { |
| return null; |
| } |
| } |
| } |
| |
| // The snippet may contain multiple data lines. |
| // Show the first line that matches the query. |
| final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); |
| |
| if (matched != null && matched.line != null) { |
| // Tokenize for long strings since the match may be at the end of it. |
| // Skip this part for short strings since the whole string will be displayed. |
| // Most contact strings are short so the snippetize method will be called infrequently. |
| final int lengthThreshold = getResources().getInteger( |
| R.integer.snippet_length_before_tokenize); |
| if (matched.line.length() > lengthThreshold) { |
| return snippetize(matched.line, matched.startIndex, lengthThreshold); |
| } else { |
| return matched.line; |
| } |
| } |
| |
| // No match found. |
| return null; |
| } |
| |
| private String snippetize(String line, int matchIndex, int maxLength) { |
| // Show up to maxLength characters. But we only show full tokens so show the last full token |
| // up to maxLength characters. So as many starting tokens as possible before trying ending |
| // tokens. |
| int remainingLength = maxLength; |
| int tempRemainingLength = remainingLength; |
| |
| // Start the end token after the matched query. |
| int index = matchIndex; |
| int endTokenIndex = index; |
| |
| // Find the match token first. |
| while (index < line.length()) { |
| if (!Character.isLetterOrDigit(line.charAt(index))) { |
| endTokenIndex = index; |
| remainingLength = tempRemainingLength; |
| break; |
| } |
| tempRemainingLength--; |
| index++; |
| } |
| |
| // Find as much content before the match. |
| index = matchIndex - 1; |
| tempRemainingLength = remainingLength; |
| int startTokenIndex = matchIndex; |
| while (index > -1 && tempRemainingLength > 0) { |
| if (!Character.isLetterOrDigit(line.charAt(index))) { |
| startTokenIndex = index; |
| remainingLength = tempRemainingLength; |
| } |
| tempRemainingLength--; |
| index--; |
| } |
| |
| index = endTokenIndex; |
| tempRemainingLength = remainingLength; |
| // Find remaining content at after match. |
| while (index < line.length() && tempRemainingLength > 0) { |
| if (!Character.isLetterOrDigit(line.charAt(index))) { |
| endTokenIndex = index; |
| } |
| tempRemainingLength--; |
| index++; |
| } |
| // Append ellipse if there is content before or after. |
| final StringBuilder sb = new StringBuilder(); |
| if (startTokenIndex > 0) { |
| sb.append("..."); |
| } |
| sb.append(line.substring(startTokenIndex, endTokenIndex)); |
| if (endTokenIndex < line.length()) { |
| sb.append("..."); |
| } |
| return sb.toString(); |
| } |
| |
| private static final Pattern SPLIT_PATTERN = Pattern.compile( |
| "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); |
| |
| /** |
| * Helper method for splitting a string into tokens. The lists passed in are populated with |
| * the |
| * tokens and offsets into the content of each token. The tokenization function parses e-mail |
| * addresses as a single token; otherwise it splits on any non-alphanumeric character. |
| * |
| * @param content Content to split. |
| * @return List of token strings. |
| */ |
| private static List<String> split(String content) { |
| final Matcher matcher = SPLIT_PATTERN.matcher(content); |
| final ArrayList<String> tokens = Lists.newArrayList(); |
| while (matcher.find()) { |
| tokens.add(matcher.group()); |
| } |
| return tokens; |
| } |
| |
| /** |
| * Shows data element. |
| */ |
| public void showData(Cursor cursor, int dataColumnIndex) { |
| cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); |
| setData(mDataBuffer.data, mDataBuffer.sizeCopied); |
| } |
| |
| public void setActivatedStateSupported(boolean flag) { |
| this.mActivatedStateSupported = flag; |
| } |
| |
| public void setAdjustSelectionBoundsEnabled(boolean enabled) { |
| mAdjustSelectionBoundsEnabled = enabled; |
| } |
| |
| @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; |
| } |
| |
| /** |
| * Set drawable resources directly for the drawable resource of the photo view. |
| * |
| * @param drawableId Id of drawable resource. |
| */ |
| public void setDrawableResource(int drawableId) { |
| ImageView photo = getPhotoView(); |
| photo.setScaleType(ImageView.ScaleType.CENTER); |
| final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId); |
| final int iconColor = |
| ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color); |
| if (CompatUtils.isLollipopCompatible()) { |
| photo.setImageDrawable(drawable); |
| photo.setImageTintList(ColorStateList.valueOf(iconColor)); |
| } else { |
| final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate(); |
| DrawableCompat.setTint(drawableWrapper, iconColor); |
| photo.setImageDrawable(drawableWrapper); |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| final float x = event.getX(); |
| final float y = event.getY(); |
| // If the touch event's coordinates are not within the view's header, then delegate |
| // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume |
| // and ignore the touch event. |
| if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { |
| return super.onTouchEvent(event); |
| } else { |
| return true; |
| } |
| } |
| |
| private final boolean pointIsInView(float localX, float localY) { |
| return localX >= mLeftOffset && localX < mRightOffset |
| && localY >= 0 && localY < (getBottom() - getTop()); |
| } |
| } |