1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.common.list;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.TypedArray;
22 import android.database.CharArrayBuffer;
23 import android.database.Cursor;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Rect;
27 import android.graphics.Typeface;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.provider.ContactsContract;
31 import android.provider.ContactsContract.Contacts;
32 import android.provider.ContactsContract.SearchSnippets;
33 import android.support.v4.content.ContextCompat;
34 import android.support.v4.graphics.drawable.DrawableCompat;
35 import android.text.Spannable;
36 import android.text.SpannableString;
37 import android.text.TextUtils;
38 import android.text.TextUtils.TruncateAt;
39 import android.util.AttributeSet;
40 import android.util.TypedValue;
41 import android.view.Gravity;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.AbsListView.SelectionBoundsAdjuster;
46 import android.widget.CheckBox;
47 import android.widget.ImageView;
48 import android.widget.ImageView.ScaleType;
49 import android.widget.QuickContactBadge;
50 import android.widget.TextView;
51 
52 import com.android.contacts.common.ContactPresenceIconUtil;
53 import com.android.contacts.common.ContactStatusUtil;
54 import com.android.contacts.common.R;
55 import com.android.contacts.common.compat.CompatUtils;
56 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
57 import com.android.contacts.common.format.TextHighlighter;
58 import com.android.contacts.common.util.ContactDisplayUtils;
59 import com.android.contacts.common.util.SearchUtil;
60 import com.android.contacts.common.util.ViewUtil;
61 
62 import com.google.common.collect.Lists;
63 
64 import java.util.ArrayList;
65 import java.util.List;
66 import java.util.Locale;
67 import java.util.regex.Matcher;
68 import java.util.regex.Pattern;
69 
70 /**
71  * A custom view for an item in the contact list.
72  * The view contains the contact's photo, a set of text views (for name, status, etc...) and
73  * icons for presence and call.
74  * The view uses no XML file for layout and all the measurements and layouts are done
75  * in the onMeasure and onLayout methods.
76  *
77  * The layout puts the contact's photo on the right side of the view, the call icon (if present)
78  * to the left of the photo, the text lines are aligned to the left and the presence icon (if
79  * present) is set to the left of the status line.
80  *
81  * The layout also supports a header (used as a header of a group of contacts) that is above the
82  * contact's data and a divider between contact view.
83  */
84 
85 public class ContactListItemView extends ViewGroup
86         implements SelectionBoundsAdjuster {
87 
88     private static final String TAG = "ContactListItemView";
89 
90     // Style values for layout and appearance
91     // The initialized values are defaults if none is provided through xml.
92     private int mPreferredHeight = 0;
93     private int mGapBetweenImageAndText = 0;
94     private int mGapBetweenLabelAndData = 0;
95     private int mPresenceIconMargin = 4;
96     private int mPresenceIconSize = 16;
97     private int mTextIndent = 0;
98     private int mTextOffsetTop;
99     private int mNameTextViewTextSize;
100     private int mHeaderWidth;
101     private Drawable mActivatedBackgroundDrawable;
102     private int mVideoCallIconSize = 32;
103     private int mVideoCallIconMargin = 16;
104 
105     // Set in onLayout. Represent left and right position of the View on the screen.
106     private int mLeftOffset;
107     private int mRightOffset;
108 
109     /**
110      * Used with {@link #mLabelView}, specifying the width ratio between label and data.
111      */
112     private int mLabelViewWidthWeight = 3;
113     /**
114      * Used with {@link #mDataView}, specifying the width ratio between label and data.
115      */
116     private int mDataViewWidthWeight = 5;
117 
118     protected static class HighlightSequence {
119         private final int start;
120         private final int end;
121 
HighlightSequence(int start, int end)122         HighlightSequence(int start, int end) {
123             this.start = start;
124             this.end = end;
125         }
126     }
127 
128     private ArrayList<HighlightSequence> mNameHighlightSequence;
129     private ArrayList<HighlightSequence> mNumberHighlightSequence;
130 
131     // Highlighting prefix for names.
132     private String mHighlightedPrefix;
133 
134     /**
135      * Used to notify listeners when a video call icon is clicked.
136      */
137     private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener;
138 
139     /**
140      * Indicates whether to show the "video call" icon, used to initiate a video call.
141      */
142     private boolean mShowVideoCallIcon = false;
143 
144     /**
145      * Indicates whether the view should leave room for the "video call" icon.
146      */
147     private boolean mSupportVideoCallIcon = false;
148 
149     /**
150      * Where to put contact photo. This affects the other Views' layout or look-and-feel.
151      *
152      * TODO: replace enum with int constants
153      */
154     public enum PhotoPosition {
155         LEFT,
156         RIGHT
157     }
158 
getDefaultPhotoPosition(boolean opposite)159     static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) {
160         final Locale locale = Locale.getDefault();
161         final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
162         switch (layoutDirection) {
163             case View.LAYOUT_DIRECTION_RTL:
164                 return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
165             case View.LAYOUT_DIRECTION_LTR:
166             default:
167                 return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
168         }
169     }
170 
171     private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
172 
173     // Header layout data
174     private TextView mHeaderTextView;
175     private boolean mIsSectionHeaderEnabled;
176 
177     // The views inside the contact view
178     private boolean mQuickContactEnabled = true;
179     private QuickContactBadge mQuickContact;
180     private ImageView mPhotoView;
181     private TextView mNameTextView;
182     private TextView mPhoneticNameTextView;
183     private TextView mLabelView;
184     private TextView mDataView;
185     private TextView mSnippetView;
186     private TextView mStatusView;
187     private ImageView mPresenceIcon;
188     private CheckBox mCheckBox;
189     private ImageView mVideoCallIcon;
190     private ImageView mWorkProfileIcon;
191 
192     private ColorStateList mSecondaryTextColor;
193 
194     private int mDefaultPhotoViewSize = 0;
195     /**
196      * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
197      * to align other data in this View.
198      */
199     private int mPhotoViewWidth;
200     /**
201      * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
202      */
203     private int mPhotoViewHeight;
204 
205     /**
206      * Only effective when {@link #mPhotoView} is null.
207      * When true all the Views on the right side of the photo should have horizontal padding on
208      * those left assuming there is a photo.
209      */
210     private boolean mKeepHorizontalPaddingForPhotoView;
211     /**
212      * Only effective when {@link #mPhotoView} is null.
213      */
214     private boolean mKeepVerticalPaddingForPhotoView;
215 
216     /**
217      * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
218      * False indicates those values should be updated before being used in position calculation.
219      */
220     private boolean mPhotoViewWidthAndHeightAreReady = false;
221 
222     private int mNameTextViewHeight;
223     private int mNameTextViewTextColor = Color.BLACK;
224     private int mPhoneticNameTextViewHeight;
225     private int mLabelViewHeight;
226     private int mDataViewHeight;
227     private int mSnippetTextViewHeight;
228     private int mStatusTextViewHeight;
229     private int mCheckBoxHeight;
230     private int mCheckBoxWidth;
231 
232     // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
233     // same row.
234     private int mLabelAndDataViewMaxHeight;
235 
236     // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
237     // more efficient for each case or in general, and simplify the whole implementation.
238     // Note: if we're sure MARQUEE will be used every time, there's no reason to use
239     // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
240     // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
241     // TextView without any modification.
242     private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
243     private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
244 
245     private boolean mActivatedStateSupported;
246     private boolean mAdjustSelectionBoundsEnabled = true;
247 
248     private Rect mBoundsWithoutHeader = new Rect();
249 
250     /** A helper used to highlight a prefix in a text field. */
251     private final TextHighlighter mTextHighlighter;
252     private CharSequence mUnknownNameText;
253     private int mPosition;
254 
ContactListItemView(Context context)255     public ContactListItemView(Context context) {
256         super(context);
257 
258         mTextHighlighter = new TextHighlighter(Typeface.BOLD);
259         mNameHighlightSequence = new ArrayList<HighlightSequence>();
260         mNumberHighlightSequence = new ArrayList<HighlightSequence>();
261     }
262 
ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon)263     public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
264         this(context, attrs);
265 
266         mSupportVideoCallIcon = supportVideoCallIcon;
267     }
268 
ContactListItemView(Context context, AttributeSet attrs)269     public ContactListItemView(Context context, AttributeSet attrs) {
270         super(context, attrs);
271 
272         TypedArray a;
273 
274         if (R.styleable.ContactListItemView != null) {
275             // Read all style values
276             a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
277             mPreferredHeight = a.getDimensionPixelSize(
278                     R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
279             mActivatedBackgroundDrawable = a.getDrawable(
280                     R.styleable.ContactListItemView_activated_background);
281 
282             mGapBetweenImageAndText = a.getDimensionPixelOffset(
283                     R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
284                     mGapBetweenImageAndText);
285             mGapBetweenLabelAndData = a.getDimensionPixelOffset(
286                     R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
287                     mGapBetweenLabelAndData);
288             mPresenceIconMargin = a.getDimensionPixelOffset(
289                     R.styleable.ContactListItemView_list_item_presence_icon_margin,
290                     mPresenceIconMargin);
291             mPresenceIconSize = a.getDimensionPixelOffset(
292                     R.styleable.ContactListItemView_list_item_presence_icon_size,
293                     mPresenceIconSize);
294             mDefaultPhotoViewSize = a.getDimensionPixelOffset(
295                     R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
296             mTextIndent = a.getDimensionPixelOffset(
297                     R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
298             mTextOffsetTop = a.getDimensionPixelOffset(
299                     R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
300             mDataViewWidthWeight = a.getInteger(
301                     R.styleable.ContactListItemView_list_item_data_width_weight,
302                     mDataViewWidthWeight);
303             mLabelViewWidthWeight = a.getInteger(
304                     R.styleable.ContactListItemView_list_item_label_width_weight,
305                     mLabelViewWidthWeight);
306             mNameTextViewTextColor = a.getColor(
307                     R.styleable.ContactListItemView_list_item_name_text_color,
308                     mNameTextViewTextColor);
309             mNameTextViewTextSize = (int) a.getDimension(
310                     R.styleable.ContactListItemView_list_item_name_text_size,
311                     (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
312             mVideoCallIconSize = a.getDimensionPixelOffset(
313                     R.styleable.ContactListItemView_list_item_video_call_icon_size,
314                     mVideoCallIconSize);
315             mVideoCallIconMargin = a.getDimensionPixelOffset(
316                     R.styleable.ContactListItemView_list_item_video_call_icon_margin,
317                     mVideoCallIconMargin);
318 
319 
320             setPaddingRelative(
321                     a.getDimensionPixelOffset(
322                             R.styleable.ContactListItemView_list_item_padding_left, 0),
323                     a.getDimensionPixelOffset(
324                             R.styleable.ContactListItemView_list_item_padding_top, 0),
325                     a.getDimensionPixelOffset(
326                             R.styleable.ContactListItemView_list_item_padding_right, 0),
327                     a.getDimensionPixelOffset(
328                             R.styleable.ContactListItemView_list_item_padding_bottom, 0));
329 
330             a.recycle();
331         }
332 
333         mTextHighlighter = new TextHighlighter(Typeface.BOLD);
334 
335         if (R.styleable.Theme != null) {
336             a = getContext().obtainStyledAttributes(R.styleable.Theme);
337             mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
338             a.recycle();
339         }
340 
341         mHeaderWidth =
342                 getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
343 
344         if (mActivatedBackgroundDrawable != null) {
345             mActivatedBackgroundDrawable.setCallback(this);
346         }
347 
348         mNameHighlightSequence = new ArrayList<HighlightSequence>();
349         mNumberHighlightSequence = new ArrayList<HighlightSequence>();
350 
351         setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
352     }
353 
setUnknownNameText(CharSequence unknownNameText)354     public void setUnknownNameText(CharSequence unknownNameText) {
355         mUnknownNameText = unknownNameText;
356     }
357 
setQuickContactEnabled(boolean flag)358     public void setQuickContactEnabled(boolean flag) {
359         mQuickContactEnabled = flag;
360     }
361 
362     /**
363      * Sets whether the video calling icon is shown.  For the video calling icon to be shown,
364      * {@link #mSupportVideoCallIcon} must be {@code true}.
365      *
366      * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false}
367      *      otherwise.
368      * @param listener Listener to notify when the video calling icon is clicked.
369      * @param position The position in the adapater of the video calling icon.
370      */
setShowVideoCallIcon(boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position)371     public void setShowVideoCallIcon(boolean showVideoCallIcon,
372             PhoneNumberListAdapter.Listener listener, int position) {
373         mShowVideoCallIcon = showVideoCallIcon;
374         mPhoneNumberListAdapterListener = listener;
375         mPosition = position;
376 
377         if (mShowVideoCallIcon) {
378             if (mVideoCallIcon == null) {
379                 mVideoCallIcon = new ImageView(getContext());
380                 addView(mVideoCallIcon);
381             }
382             mVideoCallIcon.setContentDescription(getContext().getString(
383                     R.string.description_search_video_call));
384             mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call);
385             mVideoCallIcon.setScaleType(ScaleType.CENTER);
386             mVideoCallIcon.setVisibility(View.VISIBLE);
387             mVideoCallIcon.setOnClickListener(new OnClickListener() {
388                 @Override
389                 public void onClick(View v) {
390                     // Inform the adapter that the video calling icon was clicked.
391                     if (mPhoneNumberListAdapterListener != null) {
392                         mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition);
393                     }
394                 }
395             });
396         } else {
397             if (mVideoCallIcon != null) {
398                 mVideoCallIcon.setVisibility(View.GONE);
399             }
400         }
401     }
402 
403     /**
404      * Sets whether the view supports a video calling icon.  This is independent of whether the view
405      * is actually showing an icon.  Support for the video calling icon ensures that the layout
406      * leaves space for the video icon, should it be shown.
407      *
408      * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false}
409      *      otherwise.
410      */
setSupportVideoCallIcon(boolean supportVideoCallIcon)411     public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
412         mSupportVideoCallIcon = supportVideoCallIcon;
413     }
414 
415     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)416     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
417         // We will match parent's width and wrap content vertically, but make sure
418         // height is no less than listPreferredItemHeight.
419         final int specWidth = resolveSize(0, widthMeasureSpec);
420         final int preferredHeight = mPreferredHeight;
421 
422         mNameTextViewHeight = 0;
423         mPhoneticNameTextViewHeight = 0;
424         mLabelViewHeight = 0;
425         mDataViewHeight = 0;
426         mLabelAndDataViewMaxHeight = 0;
427         mSnippetTextViewHeight = 0;
428         mStatusTextViewHeight = 0;
429         mCheckBoxWidth = 0;
430         mCheckBoxHeight = 0;
431 
432         ensurePhotoViewSize();
433 
434         // Width each TextView is able to use.
435         int effectiveWidth;
436         // All the other Views will honor the photo, so available width for them may be shrunk.
437         if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
438             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
439                     - (mPhotoViewWidth + mGapBetweenImageAndText);
440         } else {
441             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
442         }
443 
444         if (mIsSectionHeaderEnabled) {
445             effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText;
446         }
447 
448         if (mSupportVideoCallIcon) {
449             effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin);
450         }
451 
452         // Go over all visible text views and measure actual width of each of them.
453         // Also calculate their heights to get the total height for this entire view.
454 
455         if (isVisible(mCheckBox)) {
456             mCheckBox.measure(
457                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
458                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
459             mCheckBoxWidth = mCheckBox.getMeasuredWidth();
460             mCheckBoxHeight = mCheckBox.getMeasuredHeight();
461             effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText;
462         }
463 
464         if (isVisible(mNameTextView)) {
465             // Calculate width for name text - this parallels similar measurement in onLayout.
466             int nameTextWidth = effectiveWidth;
467             if (mPhotoPosition != PhotoPosition.LEFT) {
468                 nameTextWidth -= mTextIndent;
469             }
470             mNameTextView.measure(
471                     MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
472                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
473             mNameTextViewHeight = mNameTextView.getMeasuredHeight();
474         }
475 
476         if (isVisible(mPhoneticNameTextView)) {
477             mPhoneticNameTextView.measure(
478                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
479                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
480             mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
481         }
482 
483         // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
484         // we should ellipsize both using appropriate ratio.
485         final int dataWidth;
486         final int labelWidth;
487         if (isVisible(mDataView)) {
488             if (isVisible(mLabelView)) {
489                 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
490                 dataWidth = ((totalWidth * mDataViewWidthWeight)
491                         / (mDataViewWidthWeight + mLabelViewWidthWeight));
492                 labelWidth = ((totalWidth * mLabelViewWidthWeight) /
493                         (mDataViewWidthWeight + mLabelViewWidthWeight));
494             } else {
495                 dataWidth = effectiveWidth;
496                 labelWidth = 0;
497             }
498         } else {
499             dataWidth = 0;
500             if (isVisible(mLabelView)) {
501                 labelWidth = effectiveWidth;
502             } else {
503                 labelWidth = 0;
504             }
505         }
506 
507         if (isVisible(mDataView)) {
508             mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
509                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
510             mDataViewHeight = mDataView.getMeasuredHeight();
511         }
512 
513         if (isVisible(mLabelView)) {
514             mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
515                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
516             mLabelViewHeight = mLabelView.getMeasuredHeight();
517         }
518         mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
519 
520         if (isVisible(mSnippetView)) {
521             mSnippetView.measure(
522                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
523                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
524             mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
525         }
526 
527         // Status view height is the biggest of the text view and the presence icon
528         if (isVisible(mPresenceIcon)) {
529             mPresenceIcon.measure(
530                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
531                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
532             mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
533         }
534 
535         if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) {
536             mVideoCallIcon.measure(
537                     MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY),
538                     MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY));
539         }
540 
541         if (isVisible(mWorkProfileIcon)) {
542             mWorkProfileIcon.measure(
543                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
544                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
545             mNameTextViewHeight =
546                     Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
547         }
548 
549         if (isVisible(mStatusView)) {
550             // Presence and status are in a same row, so status will be affected by icon size.
551             final int statusWidth;
552             if (isVisible(mPresenceIcon)) {
553                 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
554                         - mPresenceIconMargin);
555             } else {
556                 statusWidth = effectiveWidth;
557             }
558             mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
559                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
560             mStatusTextViewHeight =
561                     Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
562         }
563 
564         // Calculate height including padding.
565         int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
566                 mLabelAndDataViewMaxHeight +
567                 mSnippetTextViewHeight + mStatusTextViewHeight);
568 
569         // Make sure the height is at least as high as the photo
570         height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
571 
572         // Make sure height is at least the preferred height
573         height = Math.max(height, preferredHeight);
574 
575         // Measure the header if it is visible.
576         if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
577             mHeaderTextView.measure(
578                     MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
579                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
580         }
581 
582         setMeasuredDimension(specWidth, height);
583     }
584 
585     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)586     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
587         final int height = bottom - top;
588         final int width = right - left;
589 
590         // Determine the vertical bounds by laying out the header first.
591         int topBound = 0;
592         int bottomBound = height;
593         int leftBound = getPaddingLeft();
594         int rightBound = width - getPaddingRight();
595 
596         final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
597 
598         // Put the section header on the left side of the contact view.
599         if (mIsSectionHeaderEnabled) {
600             if (mHeaderTextView != null) {
601                 int headerHeight = mHeaderTextView.getMeasuredHeight();
602                 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
603 
604                 mHeaderTextView.layout(
605                         isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
606                         headerTopBound,
607                         isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
608                         headerTopBound + headerHeight);
609             }
610             if (isLayoutRtl) {
611                 rightBound -= mHeaderWidth;
612             } else {
613                 leftBound += mHeaderWidth;
614             }
615         }
616 
617         mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
618         mLeftOffset = left + leftBound;
619         mRightOffset = left + rightBound;
620         if (mIsSectionHeaderEnabled) {
621             if (isLayoutRtl) {
622                 rightBound -= mGapBetweenImageAndText;
623             } else {
624                 leftBound += mGapBetweenImageAndText;
625             }
626         }
627 
628         if (mActivatedStateSupported && isActivated()) {
629             mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
630         }
631 
632         if (isVisible(mCheckBox)) {
633             final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2;
634             if (mPhotoPosition == PhotoPosition.LEFT) {
635                 mCheckBox.layout(rightBound - mCheckBoxWidth,
636                         photoTop,
637                         rightBound,
638                         photoTop + mCheckBoxHeight);
639             } else {
640                 mCheckBox.layout(leftBound,
641                         photoTop,
642                         leftBound + mCheckBoxWidth,
643                         photoTop + mCheckBoxHeight);
644             }
645         }
646 
647         final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
648         if (mPhotoPosition == PhotoPosition.LEFT) {
649             // Photo is the left most view. All the other Views should on the right of the photo.
650             if (photoView != null) {
651                 // Center the photo vertically
652                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
653                 photoView.layout(
654                         leftBound,
655                         photoTop,
656                         leftBound + mPhotoViewWidth,
657                         photoTop + mPhotoViewHeight);
658                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
659             } else if (mKeepHorizontalPaddingForPhotoView) {
660                 // Draw nothing but keep the padding.
661                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
662             }
663         } else {
664             // Photo is the right most view. Right bound should be adjusted that way.
665             if (photoView != null) {
666                 // Center the photo vertically
667                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
668                 photoView.layout(
669                         rightBound - mPhotoViewWidth,
670                         photoTop,
671                         rightBound,
672                         photoTop + mPhotoViewHeight);
673                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
674             } else if (mKeepHorizontalPaddingForPhotoView) {
675                 // Draw nothing but keep the padding.
676                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
677             }
678 
679             // Add indent between left-most padding and texts.
680             leftBound += mTextIndent;
681         }
682 
683         if (mSupportVideoCallIcon) {
684             // Place the video call button at the end of the list (e.g. take into account RTL mode).
685             if (isVisible(mVideoCallIcon)) {
686                 // Center the video icon vertically
687                 final int videoIconTop = topBound +
688                         (bottomBound - topBound - mVideoCallIconSize) / 2;
689 
690                 if (!isLayoutRtl) {
691                     // When photo is on left, video icon is placed on the right edge.
692                     mVideoCallIcon.layout(rightBound - mVideoCallIconSize,
693                             videoIconTop,
694                             rightBound,
695                             videoIconTop + mVideoCallIconSize);
696                 } else {
697                     // When photo is on right, video icon is placed on the left edge.
698                     mVideoCallIcon.layout(leftBound,
699                             videoIconTop,
700                             leftBound + mVideoCallIconSize,
701                             videoIconTop + mVideoCallIconSize);
702                 }
703             }
704 
705             if (mPhotoPosition == PhotoPosition.LEFT) {
706                 rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
707             } else {
708                 leftBound += mVideoCallIconSize + mVideoCallIconMargin;
709             }
710         }
711 
712 
713         // Center text vertically, then apply the top offset.
714         final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
715                 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
716         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
717 
718         // Work Profile icon align top
719         int workProfileIconWidth = 0;
720         if (isVisible(mWorkProfileIcon)) {
721             workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
722             final int distanceFromEnd = mCheckBoxWidth > 0
723                     ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
724             if (mPhotoPosition == PhotoPosition.LEFT) {
725                 // When photo is on left, label is placed on the right edge of the list item.
726                 mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd,
727                         textTopBound,
728                         rightBound - distanceFromEnd,
729                         textTopBound + mNameTextViewHeight);
730             } else {
731                 // When photo is on right, label is placed on the left of data view.
732                 mWorkProfileIcon.layout(leftBound + distanceFromEnd,
733                         textTopBound,
734                         leftBound + workProfileIconWidth + distanceFromEnd,
735                         textTopBound + mNameTextViewHeight);
736             }
737         }
738 
739         // Layout all text view and presence icon
740         // Put name TextView first
741         if (isVisible(mNameTextView)) {
742             final int distanceFromEnd = workProfileIconWidth
743                     + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
744             if (mPhotoPosition == PhotoPosition.LEFT) {
745                 mNameTextView.layout(leftBound,
746                         textTopBound,
747                         rightBound - distanceFromEnd,
748                         textTopBound + mNameTextViewHeight);
749             } else {
750                 mNameTextView.layout(leftBound + distanceFromEnd,
751                         textTopBound,
752                         rightBound,
753                         textTopBound + mNameTextViewHeight);
754             }
755         }
756 
757         if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
758             textTopBound += mNameTextViewHeight;
759         }
760 
761         // Presence and status
762         if (isLayoutRtl) {
763             int statusRightBound = rightBound;
764             if (isVisible(mPresenceIcon)) {
765                 int iconWidth = mPresenceIcon.getMeasuredWidth();
766                 mPresenceIcon.layout(
767                         rightBound - iconWidth,
768                         textTopBound,
769                         rightBound,
770                         textTopBound + mStatusTextViewHeight);
771                 statusRightBound -= (iconWidth + mPresenceIconMargin);
772             }
773 
774             if (isVisible(mStatusView)) {
775                 mStatusView.layout(leftBound,
776                         textTopBound,
777                         statusRightBound,
778                         textTopBound + mStatusTextViewHeight);
779             }
780         } else {
781             int statusLeftBound = leftBound;
782             if (isVisible(mPresenceIcon)) {
783                 int iconWidth = mPresenceIcon.getMeasuredWidth();
784                 mPresenceIcon.layout(
785                         leftBound,
786                         textTopBound,
787                         leftBound + iconWidth,
788                         textTopBound + mStatusTextViewHeight);
789                 statusLeftBound += (iconWidth + mPresenceIconMargin);
790             }
791 
792             if (isVisible(mStatusView)) {
793                 mStatusView.layout(statusLeftBound,
794                         textTopBound,
795                         rightBound,
796                         textTopBound + mStatusTextViewHeight);
797             }
798         }
799 
800         if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
801             textTopBound += mStatusTextViewHeight;
802         }
803 
804         // Rest of text views
805         int dataLeftBound = leftBound;
806         if (isVisible(mPhoneticNameTextView)) {
807             mPhoneticNameTextView.layout(leftBound,
808                     textTopBound,
809                     rightBound,
810                     textTopBound + mPhoneticNameTextViewHeight);
811             textTopBound += mPhoneticNameTextViewHeight;
812         }
813 
814         // Label and Data align bottom.
815         if (isVisible(mLabelView)) {
816             if (!isLayoutRtl) {
817                 mLabelView.layout(dataLeftBound,
818                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
819                         rightBound,
820                         textTopBound + mLabelAndDataViewMaxHeight);
821                 dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
822             } else {
823                 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
824                 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
825                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
826                         rightBound,
827                         textTopBound + mLabelAndDataViewMaxHeight);
828                 rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
829             }
830         }
831 
832         if (isVisible(mDataView)) {
833             if (!isLayoutRtl) {
834                 mDataView.layout(dataLeftBound,
835                         textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
836                         rightBound,
837                         textTopBound + mLabelAndDataViewMaxHeight);
838             } else {
839                 mDataView.layout(rightBound - mDataView.getMeasuredWidth(),
840                         textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
841                         rightBound,
842                         textTopBound + mLabelAndDataViewMaxHeight);
843             }
844         }
845         if (isVisible(mLabelView) || isVisible(mDataView)) {
846             textTopBound += mLabelAndDataViewMaxHeight;
847         }
848 
849         if (isVisible(mSnippetView)) {
850             mSnippetView.layout(leftBound,
851                     textTopBound,
852                     rightBound,
853                     textTopBound + mSnippetTextViewHeight);
854         }
855     }
856 
857     @Override
adjustListItemSelectionBounds(Rect bounds)858     public void adjustListItemSelectionBounds(Rect bounds) {
859         if (mAdjustSelectionBoundsEnabled) {
860             bounds.top += mBoundsWithoutHeader.top;
861             bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
862             bounds.left = mBoundsWithoutHeader.left;
863             bounds.right = mBoundsWithoutHeader.right;
864         }
865     }
866 
isVisible(View view)867     protected boolean isVisible(View view) {
868         return view != null && view.getVisibility() == View.VISIBLE;
869     }
870 
871     /**
872      * Extracts width and height from the style
873      */
ensurePhotoViewSize()874     private void ensurePhotoViewSize() {
875         if (!mPhotoViewWidthAndHeightAreReady) {
876             mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
877             if (!mQuickContactEnabled && mPhotoView == null) {
878                 if (!mKeepHorizontalPaddingForPhotoView) {
879                     mPhotoViewWidth = 0;
880                 }
881                 if (!mKeepVerticalPaddingForPhotoView) {
882                     mPhotoViewHeight = 0;
883                 }
884             }
885 
886             mPhotoViewWidthAndHeightAreReady = true;
887         }
888     }
889 
getDefaultPhotoViewSize()890     protected int getDefaultPhotoViewSize() {
891         return mDefaultPhotoViewSize;
892     }
893 
894     /**
895      * Gets a LayoutParam that corresponds to the default photo size.
896      *
897      * @return A new LayoutParam.
898      */
getDefaultPhotoLayoutParams()899     private LayoutParams getDefaultPhotoLayoutParams() {
900         LayoutParams params = generateDefaultLayoutParams();
901         params.width = getDefaultPhotoViewSize();
902         params.height = params.width;
903         return params;
904     }
905 
906     @Override
drawableStateChanged()907     protected void drawableStateChanged() {
908         super.drawableStateChanged();
909         if (mActivatedStateSupported) {
910             mActivatedBackgroundDrawable.setState(getDrawableState());
911         }
912     }
913 
914     @Override
verifyDrawable(Drawable who)915     protected boolean verifyDrawable(Drawable who) {
916         return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
917     }
918 
919     @Override
jumpDrawablesToCurrentState()920     public void jumpDrawablesToCurrentState() {
921         super.jumpDrawablesToCurrentState();
922         if (mActivatedStateSupported) {
923             mActivatedBackgroundDrawable.jumpToCurrentState();
924         }
925     }
926 
927     @Override
dispatchDraw(Canvas canvas)928     public void dispatchDraw(Canvas canvas) {
929         if (mActivatedStateSupported && isActivated()) {
930             mActivatedBackgroundDrawable.draw(canvas);
931         }
932 
933         super.dispatchDraw(canvas);
934     }
935 
936     /**
937      * Sets section header or makes it invisible if the title is null.
938      */
setSectionHeader(String title)939     public void setSectionHeader(String title) {
940         if (!TextUtils.isEmpty(title)) {
941             if (mHeaderTextView == null) {
942                 mHeaderTextView = new TextView(getContext());
943                 mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
944                 mHeaderTextView.setGravity(
945                         ViewUtil.isViewLayoutRtl(this) ? Gravity.RIGHT : Gravity.LEFT);
946                 addView(mHeaderTextView);
947             }
948             setMarqueeText(mHeaderTextView, title);
949             mHeaderTextView.setVisibility(View.VISIBLE);
950             mHeaderTextView.setAllCaps(true);
951         } else if (mHeaderTextView != null) {
952             mHeaderTextView.setVisibility(View.GONE);
953         }
954     }
955 
setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)956     public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
957         mIsSectionHeaderEnabled = isSectionHeaderEnabled;
958     }
959 
960     /**
961      * Returns the quick contact badge, creating it if necessary.
962      */
getQuickContact()963     public QuickContactBadge getQuickContact() {
964         if (!mQuickContactEnabled) {
965             throw new IllegalStateException("QuickContact is disabled for this view");
966         }
967         if (mQuickContact == null) {
968             mQuickContact = new QuickContactBadge(getContext());
969             if (CompatUtils.isLollipopCompatible()) {
970                 mQuickContact.setOverlay(null);
971             }
972             mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
973             if (mNameTextView != null) {
974                 mQuickContact.setContentDescription(getContext().getString(
975                         R.string.description_quick_contact_for, mNameTextView.getText()));
976             }
977 
978             addView(mQuickContact);
979             mPhotoViewWidthAndHeightAreReady = false;
980         }
981         return mQuickContact;
982     }
983 
984     /**
985      * Returns the photo view, creating it if necessary.
986      */
getPhotoView()987     public ImageView getPhotoView() {
988         if (mPhotoView == null) {
989             mPhotoView = new ImageView(getContext());
990             mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
991             // Quick contact style used above will set a background - remove it
992             mPhotoView.setBackground(null);
993             addView(mPhotoView);
994             mPhotoViewWidthAndHeightAreReady = false;
995         }
996         return mPhotoView;
997     }
998 
999     /**
1000      * Removes the photo view.
1001      */
removePhotoView()1002     public void removePhotoView() {
1003         removePhotoView(false, true);
1004     }
1005 
1006     /**
1007      * Removes the photo view.
1008      *
1009      * @param keepHorizontalPadding True means data on the right side will have
1010      *            padding on left, pretending there is still a photo view.
1011      * @param keepVerticalPadding True means the View will have some height
1012      *            enough for accommodating a photo view.
1013      */
removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)1014     public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
1015         mPhotoViewWidthAndHeightAreReady = false;
1016         mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
1017         mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
1018         if (mPhotoView != null) {
1019             removeView(mPhotoView);
1020             mPhotoView = null;
1021         }
1022         if (mQuickContact != null) {
1023             removeView(mQuickContact);
1024             mQuickContact = null;
1025         }
1026     }
1027 
1028     /**
1029      * Sets a word prefix that will be highlighted if encountered in fields like
1030      * name and search snippet. This will disable the mask highlighting for names.
1031      * <p>
1032      * NOTE: must be all upper-case
1033      */
setHighlightedPrefix(String upperCasePrefix)1034     public void setHighlightedPrefix(String upperCasePrefix) {
1035         mHighlightedPrefix = upperCasePrefix;
1036     }
1037 
1038     /**
1039      * Clears previously set highlight sequences for the view.
1040      */
clearHighlightSequences()1041     public void clearHighlightSequences() {
1042         mNameHighlightSequence.clear();
1043         mNumberHighlightSequence.clear();
1044         mHighlightedPrefix = null;
1045     }
1046 
1047     /**
1048      * Adds a highlight sequence to the name highlighter.
1049      * @param start The start position of the highlight sequence.
1050      * @param end The end position of the highlight sequence.
1051      */
addNameHighlightSequence(int start, int end)1052     public void addNameHighlightSequence(int start, int end) {
1053         mNameHighlightSequence.add(new HighlightSequence(start, end));
1054     }
1055 
1056     /**
1057      * Adds a highlight sequence to the number highlighter.
1058      * @param start The start position of the highlight sequence.
1059      * @param end The end position of the highlight sequence.
1060      */
addNumberHighlightSequence(int start, int end)1061     public void addNumberHighlightSequence(int start, int end) {
1062         mNumberHighlightSequence.add(new HighlightSequence(start, end));
1063     }
1064 
1065     /**
1066      * Returns the text view for the contact name, creating it if necessary.
1067      */
getNameTextView()1068     public TextView getNameTextView() {
1069         if (mNameTextView == null) {
1070             mNameTextView = new TextView(getContext());
1071             mNameTextView.setSingleLine(true);
1072             mNameTextView.setEllipsize(getTextEllipsis());
1073             mNameTextView.setTextColor(mNameTextViewTextColor);
1074             mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
1075                     mNameTextViewTextSize);
1076             // Manually call setActivated() since this view may be added after the first
1077             // setActivated() call toward this whole item view.
1078             mNameTextView.setActivated(isActivated());
1079             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
1080             mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1081             mNameTextView.setId(R.id.cliv_name_textview);
1082             if (CompatUtils.isLollipopCompatible()) {
1083                 mNameTextView.setElegantTextHeight(false);
1084             }
1085             addView(mNameTextView);
1086         }
1087         return mNameTextView;
1088     }
1089 
1090     /**
1091      * Adds or updates a text view for the phonetic name.
1092      */
setPhoneticName(char[] text, int size)1093     public void setPhoneticName(char[] text, int size) {
1094         if (text == null || size == 0) {
1095             if (mPhoneticNameTextView != null) {
1096                 mPhoneticNameTextView.setVisibility(View.GONE);
1097             }
1098         } else {
1099             getPhoneticNameTextView();
1100             setMarqueeText(mPhoneticNameTextView, text, size);
1101             mPhoneticNameTextView.setVisibility(VISIBLE);
1102         }
1103     }
1104 
1105     /**
1106      * Returns the text view for the phonetic name, creating it if necessary.
1107      */
getPhoneticNameTextView()1108     public TextView getPhoneticNameTextView() {
1109         if (mPhoneticNameTextView == null) {
1110             mPhoneticNameTextView = new TextView(getContext());
1111             mPhoneticNameTextView.setSingleLine(true);
1112             mPhoneticNameTextView.setEllipsize(getTextEllipsis());
1113             mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1114             mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1115             mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
1116             mPhoneticNameTextView.setActivated(isActivated());
1117             mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview);
1118             addView(mPhoneticNameTextView);
1119         }
1120         return mPhoneticNameTextView;
1121     }
1122 
1123     /**
1124      * Adds or updates a text view for the data label.
1125      */
setLabel(CharSequence text)1126     public void setLabel(CharSequence text) {
1127         if (TextUtils.isEmpty(text)) {
1128             if (mLabelView != null) {
1129                 mLabelView.setVisibility(View.GONE);
1130             }
1131         } else {
1132             getLabelView();
1133             setMarqueeText(mLabelView, text);
1134             mLabelView.setVisibility(VISIBLE);
1135         }
1136     }
1137 
1138     /**
1139      * Returns the text view for the data label, creating it if necessary.
1140      */
getLabelView()1141     public TextView getLabelView() {
1142         if (mLabelView == null) {
1143             mLabelView = new TextView(getContext());
1144             mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
1145                     LayoutParams.WRAP_CONTENT));
1146 
1147             mLabelView.setSingleLine(true);
1148             mLabelView.setEllipsize(getTextEllipsis());
1149             mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1150             if (mPhotoPosition == PhotoPosition.LEFT) {
1151                 mLabelView.setAllCaps(true);
1152             } else {
1153                 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
1154             }
1155             mLabelView.setActivated(isActivated());
1156             mLabelView.setId(R.id.cliv_label_textview);
1157             addView(mLabelView);
1158         }
1159         return mLabelView;
1160     }
1161 
1162     /**
1163      * Adds or updates a text view for the data element.
1164      */
setData(char[] text, int size)1165     public void setData(char[] text, int size) {
1166         if (text == null || size == 0) {
1167             if (mDataView != null) {
1168                 mDataView.setVisibility(View.GONE);
1169             }
1170         } else {
1171             getDataView();
1172             setMarqueeText(mDataView, text, size);
1173             mDataView.setVisibility(VISIBLE);
1174         }
1175     }
1176 
1177     /**
1178      * Sets phone number for a list item. This takes care of number highlighting if the highlight
1179      * mask exists.
1180      */
setPhoneNumber(String text, String countryIso)1181     public void setPhoneNumber(String text, String countryIso) {
1182         if (text == null) {
1183             if (mDataView != null) {
1184                 mDataView.setVisibility(View.GONE);
1185             }
1186         } else {
1187             getDataView();
1188 
1189             // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
1190             // mDataView. Make sure that determination of the highlight sequences are done only
1191             // after number formatting.
1192 
1193             // Sets phone number texts for display after highlighting it, if applicable.
1194             // CharSequence textToSet = text;
1195             final SpannableString textToSet = new SpannableString(text);
1196 
1197             if (mNumberHighlightSequence.size() != 0) {
1198                 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
1199                 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start,
1200                         highlightSequence.end);
1201             }
1202 
1203             setMarqueeText(mDataView, textToSet);
1204             mDataView.setVisibility(VISIBLE);
1205 
1206             // We have a phone number as "mDataView" so make it always LTR and VIEW_START
1207             mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
1208             mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1209         }
1210     }
1211 
setMarqueeText(TextView textView, char[] text, int size)1212     private void setMarqueeText(TextView textView, char[] text, int size) {
1213         if (getTextEllipsis() == TruncateAt.MARQUEE) {
1214             setMarqueeText(textView, new String(text, 0, size));
1215         } else {
1216             textView.setText(text, 0, size);
1217         }
1218     }
1219 
setMarqueeText(TextView textView, CharSequence text)1220     private void setMarqueeText(TextView textView, CharSequence text) {
1221         if (getTextEllipsis() == TruncateAt.MARQUEE) {
1222             // To show MARQUEE correctly (with END effect during non-active state), we need
1223             // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
1224             final SpannableString spannable = new SpannableString(text);
1225             spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
1226                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1227             textView.setText(spannable);
1228         } else {
1229             textView.setText(text);
1230         }
1231     }
1232 
1233     /**
1234      * Returns the {@link CheckBox} view, creating it if necessary.
1235      */
getCheckBox()1236     public CheckBox getCheckBox() {
1237         if (mCheckBox == null) {
1238             mCheckBox = new CheckBox(getContext());
1239             // Make non-focusable, so the rest of the ContactListItemView can be clicked.
1240             mCheckBox.setFocusable(false);
1241             addView(mCheckBox);
1242         }
1243         return mCheckBox;
1244     }
1245 
1246     /**
1247      * Returns the text view for the data text, creating it if necessary.
1248      */
getDataView()1249     public TextView getDataView() {
1250         if (mDataView == null) {
1251             mDataView = new TextView(getContext());
1252             mDataView.setSingleLine(true);
1253             mDataView.setEllipsize(getTextEllipsis());
1254             mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1255             mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1256             mDataView.setActivated(isActivated());
1257             mDataView.setId(R.id.cliv_data_view);
1258             if (CompatUtils.isLollipopCompatible()) {
1259                 mDataView.setElegantTextHeight(false);
1260             }
1261             addView(mDataView);
1262         }
1263         return mDataView;
1264     }
1265 
1266     /**
1267      * Adds or updates a text view for the search snippet.
1268      */
setSnippet(String text)1269     public void setSnippet(String text) {
1270         if (TextUtils.isEmpty(text)) {
1271             if (mSnippetView != null) {
1272                 mSnippetView.setVisibility(View.GONE);
1273             }
1274         } else {
1275             mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
1276             mSnippetView.setVisibility(VISIBLE);
1277             if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
1278                 // Give the text-to-speech engine a hint that it's a phone number
1279                 mSnippetView.setContentDescription(
1280                         PhoneNumberUtilsCompat.createTtsSpannable(text));
1281             } else {
1282                 mSnippetView.setContentDescription(null);
1283             }
1284         }
1285     }
1286 
1287     /**
1288      * Returns the text view for the search snippet, creating it if necessary.
1289      */
getSnippetView()1290     public TextView getSnippetView() {
1291         if (mSnippetView == null) {
1292             mSnippetView = new TextView(getContext());
1293             mSnippetView.setSingleLine(true);
1294             mSnippetView.setEllipsize(getTextEllipsis());
1295             mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1296             mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1297             mSnippetView.setActivated(isActivated());
1298             addView(mSnippetView);
1299         }
1300         return mSnippetView;
1301     }
1302 
1303     /**
1304      * Returns the text view for the status, creating it if necessary.
1305      */
getStatusView()1306     public TextView getStatusView() {
1307         if (mStatusView == null) {
1308             mStatusView = new TextView(getContext());
1309             mStatusView.setSingleLine(true);
1310             mStatusView.setEllipsize(getTextEllipsis());
1311             mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1312             mStatusView.setTextColor(mSecondaryTextColor);
1313             mStatusView.setActivated(isActivated());
1314             mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1315             addView(mStatusView);
1316         }
1317         return mStatusView;
1318     }
1319 
1320     /**
1321      * Adds or updates a text view for the status.
1322      */
setStatus(CharSequence text)1323     public void setStatus(CharSequence text) {
1324         if (TextUtils.isEmpty(text)) {
1325             if (mStatusView != null) {
1326                 mStatusView.setVisibility(View.GONE);
1327             }
1328         } else {
1329             getStatusView();
1330             setMarqueeText(mStatusView, text);
1331             mStatusView.setVisibility(VISIBLE);
1332         }
1333     }
1334 
1335     /**
1336      * Adds or updates the presence icon view.
1337      */
setPresence(Drawable icon)1338     public void setPresence(Drawable icon) {
1339         if (icon != null) {
1340             if (mPresenceIcon == null) {
1341                 mPresenceIcon = new ImageView(getContext());
1342                 addView(mPresenceIcon);
1343             }
1344             mPresenceIcon.setImageDrawable(icon);
1345             mPresenceIcon.setScaleType(ScaleType.CENTER);
1346             mPresenceIcon.setVisibility(View.VISIBLE);
1347         } else {
1348             if (mPresenceIcon != null) {
1349                 mPresenceIcon.setVisibility(View.GONE);
1350             }
1351         }
1352     }
1353 
1354     /**
1355      * Set to display work profile icon or not
1356      *
1357      * @param enabled set to display work profile icon or not
1358      */
setWorkProfileIconEnabled(boolean enabled)1359     public void setWorkProfileIconEnabled(boolean enabled) {
1360         if (mWorkProfileIcon != null) {
1361             mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
1362         } else if (enabled) {
1363             mWorkProfileIcon = new ImageView(getContext());
1364             addView(mWorkProfileIcon);
1365             mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
1366             mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
1367             mWorkProfileIcon.setVisibility(View.VISIBLE);
1368         }
1369     }
1370 
getTextEllipsis()1371     private TruncateAt getTextEllipsis() {
1372         return TruncateAt.MARQUEE;
1373     }
1374 
showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1375     public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1376         CharSequence name = cursor.getString(nameColumnIndex);
1377         setDisplayName(name);
1378 
1379         // Since the quick contact content description is derived from the display name and there is
1380         // no guarantee that when the quick contact is initialized the display name is already set,
1381         // do it here too.
1382         if (mQuickContact != null) {
1383             mQuickContact.setContentDescription(getContext().getString(
1384                     R.string.description_quick_contact_for, mNameTextView.getText()));
1385         }
1386     }
1387 
setDisplayName(CharSequence name, boolean highlight)1388     public void setDisplayName(CharSequence name, boolean highlight) {
1389         if (!TextUtils.isEmpty(name) && highlight) {
1390             clearHighlightSequences();
1391             addNameHighlightSequence(0, name.length());
1392         }
1393         setDisplayName(name);
1394     }
1395 
setDisplayName(CharSequence name)1396     public void setDisplayName(CharSequence name) {
1397         if (!TextUtils.isEmpty(name)) {
1398             // Chooses the available highlighting method for highlighting.
1399             if (mHighlightedPrefix != null) {
1400                 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
1401             } else if (mNameHighlightSequence.size() != 0) {
1402                 final SpannableString spannableName = new SpannableString(name);
1403                 for (HighlightSequence highlightSequence : mNameHighlightSequence) {
1404                     mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start,
1405                             highlightSequence.end);
1406                 }
1407                 name = spannableName;
1408             }
1409         } else {
1410             name = mUnknownNameText;
1411         }
1412         setMarqueeText(getNameTextView(), name);
1413 
1414         if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
1415             // Give the text-to-speech engine a hint that it's a phone number
1416             mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
1417             mNameTextView.setContentDescription(
1418                     PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
1419         } else {
1420             // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
1421             // of the name into two separate parts.
1422             mNameTextView.setContentDescription(name.toString());
1423         }
1424     }
1425 
hideCheckBox()1426     public void hideCheckBox() {
1427         if (mCheckBox != null) {
1428             removeView(mCheckBox);
1429             mCheckBox = null;
1430         }
1431     }
1432 
hideDisplayName()1433     public void hideDisplayName() {
1434         if (mNameTextView != null) {
1435             removeView(mNameTextView);
1436             mNameTextView = null;
1437         }
1438     }
1439 
showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1440     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1441         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1442         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1443         if (phoneticNameSize != 0) {
1444             setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1445         } else {
1446             setPhoneticName(null, 0);
1447         }
1448     }
1449 
hidePhoneticName()1450     public void hidePhoneticName() {
1451         if (mPhoneticNameTextView != null) {
1452             removeView(mPhoneticNameTextView);
1453             mPhoneticNameTextView = null;
1454         }
1455     }
1456 
1457     /**
1458      * Sets the proper icon (star or presence or nothing) and/or status message.
1459      */
showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1460     public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1461             int contactStatusColumnIndex) {
1462         Drawable icon = null;
1463         int presence = 0;
1464         if (!cursor.isNull(presenceColumnIndex)) {
1465             presence = cursor.getInt(presenceColumnIndex);
1466             icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1467         }
1468         setPresence(icon);
1469 
1470         String statusMessage = null;
1471         if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1472             statusMessage = cursor.getString(contactStatusColumnIndex);
1473         }
1474         // If there is no status message from the contact, but there was a presence value, then use
1475         // the default status message string
1476         if (statusMessage == null && presence != 0) {
1477             statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1478         }
1479         setStatus(statusMessage);
1480     }
1481 
1482     /**
1483      * Shows search snippet for email and phone number matches.
1484      */
showSnippet(Cursor cursor, String query, int snippetColumn)1485     public void showSnippet(Cursor cursor, String query, int snippetColumn) {
1486         // TODO: this does not properly handle phone numbers with control characters
1487         // For example if the phone number is 444-5555, the search query 4445 will match the
1488         // number since we normalize it before querying CP2 but the snippet will fail since
1489         // the portion to be highlighted is 444-5 not 4445.
1490         final String snippet = cursor.getString(snippetColumn);
1491         if (snippet == null) {
1492             setSnippet(null);
1493             return;
1494         }
1495         final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0
1496                 ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null;
1497         if (snippet.equals(displayName)) {
1498             // If the snippet exactly matches the display name (i.e. the phone number or email
1499             // address is being used as the display name) then no snippet is necessary
1500             setSnippet(null);
1501             return;
1502         }
1503         // Show the snippet with the part of the query that matched it
1504         setSnippet(updateSnippet(snippet, query, displayName));
1505     }
1506 
1507     /**
1508      * Shows search snippet.
1509      */
showSnippet(Cursor cursor, int summarySnippetColumnIndex)1510     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
1511         if (cursor.getColumnCount() <= summarySnippetColumnIndex
1512             || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
1513             setSnippet(null);
1514             return;
1515         }
1516 
1517         String snippet = cursor.getString(summarySnippetColumnIndex);
1518 
1519         // Do client side snippeting if provider didn't do it
1520         final Bundle extras = cursor.getExtras();
1521         if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
1522 
1523             final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
1524 
1525             String displayName = null;
1526             int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1527             if (displayNameIndex >= 0) {
1528                 displayName = cursor.getString(displayNameIndex);
1529             }
1530 
1531             snippet = updateSnippet(snippet, query, displayName);
1532 
1533         } else {
1534             if (snippet != null) {
1535                 int from = 0;
1536                 int to = snippet.length();
1537                 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1538                 if (start == -1) {
1539                     snippet = null;
1540                 } else {
1541                     int firstNl = snippet.lastIndexOf('\n', start);
1542                     if (firstNl != -1) {
1543                         from = firstNl + 1;
1544                     }
1545                     int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1546                     if (end != -1) {
1547                         int lastNl = snippet.indexOf('\n', end);
1548                         if (lastNl != -1) {
1549                             to = lastNl;
1550                         }
1551                     }
1552 
1553                     StringBuilder sb = new StringBuilder();
1554                     for (int i = from; i < to; i++) {
1555                         char c = snippet.charAt(i);
1556                         if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1557                                 c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1558                             sb.append(c);
1559                         }
1560                     }
1561                     snippet = sb.toString();
1562                 }
1563             }
1564         }
1565 
1566         setSnippet(snippet);
1567     }
1568 
1569     /**
1570      * Used for deferred snippets from the database. The contents come back as large strings which
1571      * need to be extracted for display.
1572      *
1573      * @param snippet The snippet from the database.
1574      * @param query The search query substring.
1575      * @param displayName The contact display name.
1576      * @return The proper snippet to display.
1577      */
updateSnippet(String snippet, String query, String displayName)1578     private String updateSnippet(String snippet, String query, String displayName) {
1579 
1580         if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
1581             return null;
1582         }
1583         query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
1584 
1585         // If the display name already contains the query term, return empty - snippets should
1586         // not be needed in that case.
1587         if (!TextUtils.isEmpty(displayName)) {
1588             final String lowerDisplayName = displayName.toLowerCase();
1589             final List<String> nameTokens = split(lowerDisplayName);
1590             for (String nameToken : nameTokens) {
1591                 if (nameToken.startsWith(query)) {
1592                     return null;
1593                 }
1594             }
1595         }
1596 
1597         // The snippet may contain multiple data lines.
1598         // Show the first line that matches the query.
1599         final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
1600 
1601         if (matched != null && matched.line != null) {
1602             // Tokenize for long strings since the match may be at the end of it.
1603             // Skip this part for short strings since the whole string will be displayed.
1604             // Most contact strings are short so the snippetize method will be called infrequently.
1605             final int lengthThreshold = getResources().getInteger(
1606                     R.integer.snippet_length_before_tokenize);
1607             if (matched.line.length() > lengthThreshold) {
1608                 return snippetize(matched.line, matched.startIndex, lengthThreshold);
1609             } else {
1610                 return matched.line;
1611             }
1612         }
1613 
1614         // No match found.
1615         return null;
1616     }
1617 
snippetize(String line, int matchIndex, int maxLength)1618     private String snippetize(String line, int matchIndex, int maxLength) {
1619         // Show up to maxLength characters. But we only show full tokens so show the last full token
1620         // up to maxLength characters. So as many starting tokens as possible before trying ending
1621         // tokens.
1622         int remainingLength = maxLength;
1623         int tempRemainingLength = remainingLength;
1624 
1625         // Start the end token after the matched query.
1626         int index = matchIndex;
1627         int endTokenIndex = index;
1628 
1629         // Find the match token first.
1630         while (index < line.length()) {
1631             if (!Character.isLetterOrDigit(line.charAt(index))) {
1632                 endTokenIndex = index;
1633                 remainingLength = tempRemainingLength;
1634                 break;
1635             }
1636             tempRemainingLength--;
1637             index++;
1638         }
1639 
1640         // Find as much content before the match.
1641         index = matchIndex - 1;
1642         tempRemainingLength = remainingLength;
1643         int startTokenIndex = matchIndex;
1644         while (index > -1 && tempRemainingLength > 0) {
1645             if (!Character.isLetterOrDigit(line.charAt(index))) {
1646                 startTokenIndex = index;
1647                 remainingLength = tempRemainingLength;
1648             }
1649             tempRemainingLength--;
1650             index--;
1651         }
1652 
1653         index = endTokenIndex;
1654         tempRemainingLength = remainingLength;
1655         // Find remaining content at after match.
1656         while (index < line.length() && tempRemainingLength > 0) {
1657             if (!Character.isLetterOrDigit(line.charAt(index))) {
1658                 endTokenIndex = index;
1659             }
1660             tempRemainingLength--;
1661             index++;
1662         }
1663         // Append ellipse if there is content before or after.
1664         final StringBuilder sb = new StringBuilder();
1665         if (startTokenIndex > 0) {
1666             sb.append("...");
1667         }
1668         sb.append(line.substring(startTokenIndex, endTokenIndex));
1669         if (endTokenIndex < line.length()) {
1670             sb.append("...");
1671         }
1672         return sb.toString();
1673     }
1674 
1675     private static final Pattern SPLIT_PATTERN = Pattern.compile(
1676             "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
1677 
1678     /**
1679      * Helper method for splitting a string into tokens.  The lists passed in are populated with
1680      * the
1681      * tokens and offsets into the content of each token.  The tokenization function parses e-mail
1682      * addresses as a single token; otherwise it splits on any non-alphanumeric character.
1683      *
1684      * @param content Content to split.
1685      * @return List of token strings.
1686      */
split(String content)1687     private static List<String> split(String content) {
1688         final Matcher matcher = SPLIT_PATTERN.matcher(content);
1689         final ArrayList<String> tokens = Lists.newArrayList();
1690         while (matcher.find()) {
1691             tokens.add(matcher.group());
1692         }
1693         return tokens;
1694     }
1695 
1696     /**
1697      * Shows data element.
1698      */
showData(Cursor cursor, int dataColumnIndex)1699     public void showData(Cursor cursor, int dataColumnIndex) {
1700         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1701         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1702     }
1703 
setActivatedStateSupported(boolean flag)1704     public void setActivatedStateSupported(boolean flag) {
1705         this.mActivatedStateSupported = flag;
1706     }
1707 
setAdjustSelectionBoundsEnabled(boolean enabled)1708     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
1709         mAdjustSelectionBoundsEnabled = enabled;
1710     }
1711 
1712     @Override
requestLayout()1713     public void requestLayout() {
1714         // We will assume that once measured this will not need to resize
1715         // itself, so there is no need to pass the layout request to the parent
1716         // view (ListView).
1717         forceLayout();
1718     }
1719 
setPhotoPosition(PhotoPosition photoPosition)1720     public void setPhotoPosition(PhotoPosition photoPosition) {
1721         mPhotoPosition = photoPosition;
1722     }
1723 
getPhotoPosition()1724     public PhotoPosition getPhotoPosition() {
1725         return mPhotoPosition;
1726     }
1727 
1728     /**
1729      * Set drawable resources directly for the drawable resource of the photo view.
1730      *
1731      * @param drawableId Id of drawable resource.
1732      */
setDrawableResource(int drawableId)1733     public void setDrawableResource(int drawableId) {
1734         ImageView photo = getPhotoView();
1735         photo.setScaleType(ImageView.ScaleType.CENTER);
1736         final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
1737         final int iconColor =
1738                 ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
1739         if (CompatUtils.isLollipopCompatible()) {
1740             photo.setImageDrawable(drawable);
1741             photo.setImageTintList(ColorStateList.valueOf(iconColor));
1742         } else {
1743             final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
1744             DrawableCompat.setTint(drawableWrapper, iconColor);
1745             photo.setImageDrawable(drawableWrapper);
1746         }
1747     }
1748 
1749     @Override
onTouchEvent(MotionEvent event)1750     public boolean onTouchEvent(MotionEvent event) {
1751         final float x = event.getX();
1752         final float y = event.getY();
1753         // If the touch event's coordinates are not within the view's header, then delegate
1754         // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
1755         // and ignore the touch event.
1756         if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
1757             return super.onTouchEvent(event);
1758         } else {
1759             return true;
1760         }
1761     }
1762 
pointIsInView(float localX, float localY)1763     private final boolean pointIsInView(float localX, float localY) {
1764         return localX >= mLeftOffset && localX < mRightOffset
1765                 && localY >= 0 && localY < (getBottom() - getTop());
1766     }
1767 }
1768