• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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.text.Spannable;
33 import android.text.SpannableString;
34 import android.text.TextUtils;
35 import android.text.TextUtils.TruncateAt;
36 import android.util.AttributeSet;
37 import android.util.TypedValue;
38 import android.view.Gravity;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.widget.AbsListView.SelectionBoundsAdjuster;
42 import android.widget.ImageView;
43 import android.widget.ImageView.ScaleType;
44 import android.widget.QuickContactBadge;
45 import android.widget.TextView;
46 
47 import com.android.contacts.ContactPresenceIconUtil;
48 import com.android.contacts.ContactStatusUtil;
49 import com.android.contacts.R;
50 import com.android.contacts.format.PrefixHighlighter;
51 
52 /**
53  * A custom view for an item in the contact list.
54  * The view contains the contact's photo, a set of text views (for name, status, etc...) and
55  * icons for presence and call.
56  * The view uses no XML file for layout and all the measurements and layouts are done
57  * in the onMeasure and onLayout methods.
58  *
59  * The layout puts the contact's photo on the right side of the view, the call icon (if present)
60  * to the left of the photo, the text lines are aligned to the left and the presence icon (if
61  * present) is set to the left of the status line.
62  *
63  * The layout also supports a header (used as a header of a group of contacts) that is above the
64  * contact's data and a divider between contact view.
65  */
66 
67 public class ContactListItemView extends ViewGroup
68         implements SelectionBoundsAdjuster {
69 
70     // Style values for layout and appearance
71     private final int mPreferredHeight;
72     private final int mGapBetweenImageAndText;
73     private final int mGapBetweenLabelAndData;
74     private final int mPresenceIconMargin;
75     private final int mPresenceIconSize;
76     private final int mHeaderTextColor;
77     private final int mHeaderTextIndent;
78     private final int mHeaderTextSize;
79     private final int mHeaderUnderlineHeight;
80     private final int mHeaderUnderlineColor;
81     private final int mCountViewTextSize;
82     private final int mContactsCountTextColor;
83     private final int mTextIndent;
84     private Drawable mActivatedBackgroundDrawable;
85 
86     /**
87      * Used with {@link #mLabelView}, specifying the width ratio between label and data.
88      */
89     private final int mLabelViewWidthWeight;
90     /**
91      * Used with {@link #mDataView}, specifying the width ratio between label and data.
92      */
93     private final int mDataViewWidthWeight;
94 
95     // Will be used with adjustListItemSelectionBounds().
96     private int mSelectionBoundsMarginLeft;
97     private int mSelectionBoundsMarginRight;
98 
99     // Horizontal divider between contact views.
100     private boolean mHorizontalDividerVisible = true;
101     private Drawable mHorizontalDividerDrawable;
102     private int mHorizontalDividerHeight;
103 
104     /**
105      * Where to put contact photo. This affects the other Views' layout or look-and-feel.
106      */
107     public enum PhotoPosition {
108         LEFT,
109         RIGHT
110     }
111     public static final PhotoPosition DEFAULT_PHOTO_POSITION = PhotoPosition.RIGHT;
112     private PhotoPosition mPhotoPosition = DEFAULT_PHOTO_POSITION;
113 
114     // Header layout data
115     private boolean mHeaderVisible;
116     private View mHeaderDivider;
117     private int mHeaderBackgroundHeight;
118     private TextView mHeaderTextView;
119 
120     // The views inside the contact view
121     private boolean mQuickContactEnabled = true;
122     private QuickContactBadge mQuickContact;
123     private ImageView mPhotoView;
124     private TextView mNameTextView;
125     private TextView mPhoneticNameTextView;
126     private TextView mLabelView;
127     private TextView mDataView;
128     private TextView mSnippetView;
129     private TextView mStatusView;
130     private TextView mCountView;
131     private ImageView mPresenceIcon;
132 
133     private ColorStateList mSecondaryTextColor;
134 
135     private char[] mHighlightedPrefix;
136 
137     private int mDefaultPhotoViewSize;
138     /**
139      * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
140      * to align other data in this View.
141      */
142     private int mPhotoViewWidth;
143     /**
144      * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
145      */
146     private int mPhotoViewHeight;
147 
148     /**
149      * Only effective when {@link #mPhotoView} is null.
150      * When true all the Views on the right side of the photo should have horizontal padding on
151      * those left assuming there is a photo.
152      */
153     private boolean mKeepHorizontalPaddingForPhotoView;
154     /**
155      * Only effective when {@link #mPhotoView} is null.
156      */
157     private boolean mKeepVerticalPaddingForPhotoView;
158 
159     /**
160      * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
161      * False indicates those values should be updated before being used in position calculation.
162      */
163     private boolean mPhotoViewWidthAndHeightAreReady = false;
164 
165     private int mNameTextViewHeight;
166     private int mPhoneticNameTextViewHeight;
167     private int mLabelViewHeight;
168     private int mDataViewHeight;
169     private int mSnippetTextViewHeight;
170     private int mStatusTextViewHeight;
171 
172     // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
173     // same row.
174     private int mLabelAndDataViewMaxHeight;
175 
176     // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
177     // more efficient for each case or in general, and simplify the whole implementation.
178     // Note: if we're sure MARQUEE will be used every time, there's no reason to use
179     // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
180     // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
181     // TextView without any modification.
182     private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
183     private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
184 
185     private boolean mActivatedStateSupported;
186 
187     private Rect mBoundsWithoutHeader = new Rect();
188 
189     /** A helper used to highlight a prefix in a text field. */
190     private PrefixHighlighter mPrefixHighlighter;
191     private CharSequence mUnknownNameText;
192 
ContactListItemView(Context context, AttributeSet attrs)193     public ContactListItemView(Context context, AttributeSet attrs) {
194         super(context, attrs);
195         mContext = context;
196 
197         // Read all style values
198         TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
199         mPreferredHeight = a.getDimensionPixelSize(
200                 R.styleable.ContactListItemView_list_item_height, 0);
201         mActivatedBackgroundDrawable = a.getDrawable(
202                 R.styleable.ContactListItemView_activated_background);
203         mHorizontalDividerDrawable = a.getDrawable(
204                 R.styleable.ContactListItemView_list_item_divider);
205 
206         mGapBetweenImageAndText = a.getDimensionPixelOffset(
207                 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0);
208         mGapBetweenLabelAndData = a.getDimensionPixelOffset(
209                 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0);
210         mPresenceIconMargin = a.getDimensionPixelOffset(
211                 R.styleable.ContactListItemView_list_item_presence_icon_margin, 4);
212         mPresenceIconSize = a.getDimensionPixelOffset(
213                 R.styleable.ContactListItemView_list_item_presence_icon_size, 16);
214         mDefaultPhotoViewSize = a.getDimensionPixelOffset(
215                 R.styleable.ContactListItemView_list_item_photo_size, 0);
216         mHeaderTextIndent = a.getDimensionPixelOffset(
217                 R.styleable.ContactListItemView_list_item_header_text_indent, 0);
218         mHeaderTextColor = a.getColor(
219                 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK);
220         mHeaderTextSize = a.getDimensionPixelSize(
221                 R.styleable.ContactListItemView_list_item_header_text_size, 12);
222         mHeaderBackgroundHeight = a.getDimensionPixelSize(
223                 R.styleable.ContactListItemView_list_item_header_height, 30);
224         mHeaderUnderlineHeight = a.getDimensionPixelSize(
225                 R.styleable.ContactListItemView_list_item_header_underline_height, 1);
226         mHeaderUnderlineColor = a.getColor(
227                 R.styleable.ContactListItemView_list_item_header_underline_color, 0);
228         mTextIndent = a.getDimensionPixelOffset(
229                 R.styleable.ContactListItemView_list_item_text_indent, 0);
230         mCountViewTextSize = a.getDimensionPixelSize(
231                 R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12);
232         mContactsCountTextColor = a.getColor(
233                 R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK);
234         mDataViewWidthWeight = a.getInteger(
235                 R.styleable.ContactListItemView_list_item_data_width_weight, 5);
236         mLabelViewWidthWeight = a.getInteger(
237                 R.styleable.ContactListItemView_list_item_label_width_weight, 3);
238 
239         setPadding(
240                 a.getDimensionPixelOffset(
241                         R.styleable.ContactListItemView_list_item_padding_left, 0),
242                 a.getDimensionPixelOffset(
243                         R.styleable.ContactListItemView_list_item_padding_top, 0),
244                 a.getDimensionPixelOffset(
245                         R.styleable.ContactListItemView_list_item_padding_right, 0),
246                 a.getDimensionPixelOffset(
247                         R.styleable.ContactListItemView_list_item_padding_bottom, 0));
248 
249         final int prefixHighlightColor = a.getColor(
250                 R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
251         mPrefixHighlighter = new PrefixHighlighter(prefixHighlightColor);
252         a.recycle();
253 
254         a = getContext().obtainStyledAttributes(android.R.styleable.Theme);
255         mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary);
256         a.recycle();
257 
258         mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
259 
260         if (mActivatedBackgroundDrawable != null) {
261             mActivatedBackgroundDrawable.setCallback(this);
262         }
263     }
264 
setUnknownNameText(CharSequence unknownNameText)265     public void setUnknownNameText(CharSequence unknownNameText) {
266         mUnknownNameText = unknownNameText;
267     }
268 
setQuickContactEnabled(boolean flag)269     public void setQuickContactEnabled(boolean flag) {
270         mQuickContactEnabled = flag;
271     }
272 
273     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)274     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
275         // We will match parent's width and wrap content vertically, but make sure
276         // height is no less than listPreferredItemHeight.
277         final int specWidth = resolveSize(0, widthMeasureSpec);
278         final int preferredHeight;
279         if (mHorizontalDividerVisible) {
280             preferredHeight = mPreferredHeight + mHorizontalDividerHeight;
281         } else {
282             preferredHeight = mPreferredHeight;
283         }
284 
285         mNameTextViewHeight = 0;
286         mPhoneticNameTextViewHeight = 0;
287         mLabelViewHeight = 0;
288         mDataViewHeight = 0;
289         mLabelAndDataViewMaxHeight = 0;
290         mSnippetTextViewHeight = 0;
291         mStatusTextViewHeight = 0;
292 
293         ensurePhotoViewSize();
294 
295         // Width each TextView is able to use.
296         final int effectiveWidth;
297         // All the other Views will honor the photo, so available width for them may be shrunk.
298         if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
299             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
300                     - (mPhotoViewWidth + mGapBetweenImageAndText);
301         } else {
302             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
303         }
304 
305         // Go over all visible text views and measure actual width of each of them.
306         // Also calculate their heights to get the total height for this entire view.
307 
308         if (isVisible(mNameTextView)) {
309             // Caculate width for name text - this parallels similar measurement in onLayout.
310             int nameTextWidth = effectiveWidth;
311             if (mPhotoPosition != PhotoPosition.LEFT) {
312                 nameTextWidth -= mTextIndent;
313             }
314             mNameTextView.measure(
315                     MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
316                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
317             mNameTextViewHeight = mNameTextView.getMeasuredHeight();
318         }
319 
320         if (isVisible(mPhoneticNameTextView)) {
321             mPhoneticNameTextView.measure(
322                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
323                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
324             mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
325         }
326 
327         // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
328         // we should ellipsize both using appropriate ratio.
329         final int dataWidth;
330         final int labelWidth;
331         if (isVisible(mDataView)) {
332             if (isVisible(mLabelView)) {
333                 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
334                 dataWidth = ((totalWidth * mDataViewWidthWeight)
335                         / (mDataViewWidthWeight + mLabelViewWidthWeight));
336                 labelWidth = ((totalWidth * mLabelViewWidthWeight) /
337                         (mDataViewWidthWeight + mLabelViewWidthWeight));
338             } else {
339                 dataWidth = effectiveWidth;
340                 labelWidth = 0;
341             }
342         } else {
343             dataWidth = 0;
344             if (isVisible(mLabelView)) {
345                 labelWidth = effectiveWidth;
346             } else {
347                 labelWidth = 0;
348             }
349         }
350 
351         if (isVisible(mDataView)) {
352             mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
353                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
354             mDataViewHeight = mDataView.getMeasuredHeight();
355         }
356 
357         if (isVisible(mLabelView)) {
358             // For performance reason we don't want AT_MOST usually, but when the picture is
359             // on right, we need to use it anyway because mDataView is next to mLabelView.
360             final int mode = (mPhotoPosition == PhotoPosition.LEFT
361                     ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST);
362             mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode),
363                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
364             mLabelViewHeight = mLabelView.getMeasuredHeight();
365         }
366         mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
367 
368         if (isVisible(mSnippetView)) {
369             mSnippetView.measure(
370                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
371                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
372             mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
373         }
374 
375         // Status view height is the biggest of the text view and the presence icon
376         if (isVisible(mPresenceIcon)) {
377             mPresenceIcon.measure(
378                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
379                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
380             mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
381         }
382 
383         if (isVisible(mStatusView)) {
384             // Presence and status are in a same row, so status will be affected by icon size.
385             final int statusWidth;
386             if (isVisible(mPresenceIcon)) {
387                 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
388                         - mPresenceIconMargin);
389             } else {
390                 statusWidth = effectiveWidth;
391             }
392             mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
393                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
394             mStatusTextViewHeight =
395                     Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
396         }
397 
398         // Calculate height including padding.
399         int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
400                 mLabelAndDataViewMaxHeight +
401                 mSnippetTextViewHeight + mStatusTextViewHeight);
402 
403         // Make sure the height is at least as high as the photo
404         height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
405 
406         // Add horizontal divider height
407         if (mHorizontalDividerVisible) {
408             height += mHorizontalDividerHeight;
409         }
410 
411         // Make sure height is at least the preferred height
412         height = Math.max(height, preferredHeight);
413 
414         // Add the height of the header if visible
415         if (mHeaderVisible) {
416             mHeaderTextView.measure(
417                     MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.EXACTLY),
418                     MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
419             if (mCountView != null) {
420                 mCountView.measure(
421                         MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.AT_MOST),
422                         MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
423             }
424             mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight,
425                     mHeaderTextView.getMeasuredHeight());
426             height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
427         }
428 
429         setMeasuredDimension(specWidth, height);
430     }
431 
432     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)433     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
434         final int height = bottom - top;
435         final int width = right - left;
436 
437         // Determine the vertical bounds by laying out the header first.
438         int topBound = 0;
439         int bottomBound = height;
440         int leftBound = getPaddingLeft();
441         int rightBound = width - getPaddingRight();
442 
443         // Put the header in the top of the contact view (Text + underline view)
444         if (mHeaderVisible) {
445             mHeaderTextView.layout(leftBound + mHeaderTextIndent,
446                     0,
447                     rightBound,
448                     mHeaderBackgroundHeight);
449             if (mCountView != null) {
450                 mCountView.layout(rightBound - mCountView.getMeasuredWidth(),
451                         0,
452                         rightBound,
453                         mHeaderBackgroundHeight);
454             }
455             mHeaderDivider.layout(leftBound,
456                     mHeaderBackgroundHeight,
457                     rightBound,
458                     mHeaderBackgroundHeight + mHeaderUnderlineHeight);
459             topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
460         }
461 
462         // Put horizontal divider at the bottom
463         if (mHorizontalDividerVisible) {
464             mHorizontalDividerDrawable.setBounds(
465                     leftBound,
466                     height - mHorizontalDividerHeight,
467                     rightBound,
468                     height);
469             bottomBound -= mHorizontalDividerHeight;
470         }
471 
472         mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
473 
474         if (mActivatedStateSupported && isActivated()) {
475             mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
476         }
477 
478         final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
479         if (mPhotoPosition == PhotoPosition.LEFT) {
480             // Photo is the left most view. All the other Views should on the right of the photo.
481             if (photoView != null) {
482                 // Center the photo vertically
483                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
484                 photoView.layout(
485                         leftBound,
486                         photoTop,
487                         leftBound + mPhotoViewWidth,
488                         photoTop + mPhotoViewHeight);
489                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
490             } else if (mKeepHorizontalPaddingForPhotoView) {
491                 // Draw nothing but keep the padding.
492                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
493             }
494         } else {
495             // Photo is the right most view. Right bound should be adjusted that way.
496             if (photoView != null) {
497                 // Center the photo vertically
498                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
499                 photoView.layout(
500                         rightBound - mPhotoViewWidth,
501                         photoTop,
502                         rightBound,
503                         photoTop + mPhotoViewHeight);
504                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
505             }
506 
507             // Add indent between left-most padding and texts.
508             leftBound += mTextIndent;
509         }
510 
511         // Center text vertically
512         final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
513                 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
514         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
515 
516         // Layout all text view and presence icon
517         // Put name TextView first
518         if (isVisible(mNameTextView)) {
519             mNameTextView.layout(leftBound,
520                     textTopBound,
521                     rightBound,
522                     textTopBound + mNameTextViewHeight);
523             textTopBound += mNameTextViewHeight;
524         }
525 
526         // Presence and status
527         int statusLeftBound = leftBound;
528         if (isVisible(mPresenceIcon)) {
529             int iconWidth = mPresenceIcon.getMeasuredWidth();
530             mPresenceIcon.layout(
531                     leftBound,
532                     textTopBound,
533                     leftBound + iconWidth,
534                     textTopBound + mStatusTextViewHeight);
535             statusLeftBound += (iconWidth + mPresenceIconMargin);
536         }
537 
538         if (isVisible(mStatusView)) {
539             mStatusView.layout(statusLeftBound,
540                     textTopBound,
541                     rightBound,
542                     textTopBound + mStatusTextViewHeight);
543         }
544 
545         if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
546             textTopBound += mStatusTextViewHeight;
547         }
548 
549         // Rest of text views
550         int dataLeftBound = leftBound;
551         if (isVisible(mPhoneticNameTextView)) {
552             mPhoneticNameTextView.layout(leftBound,
553                     textTopBound,
554                     rightBound,
555                     textTopBound + mPhoneticNameTextViewHeight);
556             textTopBound += mPhoneticNameTextViewHeight;
557         }
558 
559         // Label and Data align bottom.
560         if (isVisible(mLabelView)) {
561             if (mPhotoPosition == PhotoPosition.LEFT) {
562                 // When photo is on left, label is placed on the right edge of the list item.
563                 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
564                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
565                         rightBound,
566                         textTopBound + mLabelAndDataViewMaxHeight);
567                 rightBound -= mLabelView.getMeasuredWidth();
568             } else {
569                 // When photo is on right, label is placed on the left of data view.
570                 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
571                 mLabelView.layout(leftBound,
572                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
573                         dataLeftBound,
574                         textTopBound + mLabelAndDataViewMaxHeight);
575                 dataLeftBound += mGapBetweenLabelAndData;
576             }
577         }
578 
579         if (isVisible(mDataView)) {
580             mDataView.layout(dataLeftBound,
581                     textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
582                     rightBound,
583                     textTopBound + mLabelAndDataViewMaxHeight);
584         }
585         if (isVisible(mLabelView) || isVisible(mDataView)) {
586             textTopBound += mLabelAndDataViewMaxHeight;
587         }
588 
589         if (isVisible(mSnippetView)) {
590             mSnippetView.layout(leftBound,
591                     textTopBound,
592                     rightBound,
593                     textTopBound + mSnippetTextViewHeight);
594         }
595     }
596 
597     @Override
adjustListItemSelectionBounds(Rect bounds)598     public void adjustListItemSelectionBounds(Rect bounds) {
599         bounds.top += mBoundsWithoutHeader.top;
600         bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
601         bounds.left += mSelectionBoundsMarginLeft;
602         bounds.right -= mSelectionBoundsMarginRight;
603     }
604 
isVisible(View view)605     protected boolean isVisible(View view) {
606         return view != null && view.getVisibility() == View.VISIBLE;
607     }
608 
609     /**
610      * Extracts width and height from the style
611      */
ensurePhotoViewSize()612     private void ensurePhotoViewSize() {
613         if (!mPhotoViewWidthAndHeightAreReady) {
614             mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
615             if (!mQuickContactEnabled && mPhotoView == null) {
616                 if (!mKeepHorizontalPaddingForPhotoView) {
617                     mPhotoViewWidth = 0;
618                 }
619                 if (!mKeepVerticalPaddingForPhotoView) {
620                     mPhotoViewHeight = 0;
621                 }
622             }
623 
624             mPhotoViewWidthAndHeightAreReady = true;
625         }
626     }
627 
setDefaultPhotoViewSize(int pixels)628     protected void setDefaultPhotoViewSize(int pixels) {
629         mDefaultPhotoViewSize = pixels;
630     }
631 
getDefaultPhotoViewSize()632     protected int getDefaultPhotoViewSize() {
633         return mDefaultPhotoViewSize;
634     }
635 
636     /**
637      * Gets a LayoutParam that corresponds to the default photo size.
638      *
639      * @return A new LayoutParam.
640      */
getDefaultPhotoLayoutParams()641     private LayoutParams getDefaultPhotoLayoutParams() {
642         LayoutParams params = generateDefaultLayoutParams();
643         params.width = getDefaultPhotoViewSize();
644         params.height = params.width;
645         return params;
646     }
647 
648     @Override
drawableStateChanged()649     protected void drawableStateChanged() {
650         super.drawableStateChanged();
651         if (mActivatedStateSupported) {
652             mActivatedBackgroundDrawable.setState(getDrawableState());
653         }
654     }
655 
656     @Override
verifyDrawable(Drawable who)657     protected boolean verifyDrawable(Drawable who) {
658         return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
659     }
660 
661     @Override
jumpDrawablesToCurrentState()662     public void jumpDrawablesToCurrentState() {
663         super.jumpDrawablesToCurrentState();
664         if (mActivatedStateSupported) {
665             mActivatedBackgroundDrawable.jumpToCurrentState();
666         }
667     }
668 
669     @Override
dispatchDraw(Canvas canvas)670     public void dispatchDraw(Canvas canvas) {
671         if (mActivatedStateSupported && isActivated()) {
672             mActivatedBackgroundDrawable.draw(canvas);
673         }
674         if (mHorizontalDividerVisible) {
675             mHorizontalDividerDrawable.draw(canvas);
676         }
677 
678         super.dispatchDraw(canvas);
679     }
680 
681     /**
682      * Sets the flag that determines whether a divider should drawn at the bottom
683      * of the view.
684      */
setDividerVisible(boolean visible)685     public void setDividerVisible(boolean visible) {
686         mHorizontalDividerVisible = visible;
687     }
688 
689     /**
690      * Sets section header or makes it invisible if the title is null.
691      */
setSectionHeader(String title)692     public void setSectionHeader(String title) {
693         if (!TextUtils.isEmpty(title)) {
694             if (mHeaderTextView == null) {
695                 mHeaderTextView = new TextView(mContext);
696                 mHeaderTextView.setTextColor(mHeaderTextColor);
697                 mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
698                 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
699                 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
700                 addView(mHeaderTextView);
701             }
702             if (mHeaderDivider == null) {
703                 mHeaderDivider = new View(mContext);
704                 mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
705                 addView(mHeaderDivider);
706             }
707             setMarqueeText(mHeaderTextView, title);
708             mHeaderTextView.setVisibility(View.VISIBLE);
709             mHeaderDivider.setVisibility(View.VISIBLE);
710             mHeaderTextView.setAllCaps(true);
711             mHeaderVisible = true;
712         } else {
713             if (mHeaderTextView != null) {
714                 mHeaderTextView.setVisibility(View.GONE);
715             }
716             if (mHeaderDivider != null) {
717                 mHeaderDivider.setVisibility(View.GONE);
718             }
719             mHeaderVisible = false;
720         }
721     }
722 
723     /**
724      * Returns the quick contact badge, creating it if necessary.
725      */
getQuickContact()726     public QuickContactBadge getQuickContact() {
727         if (!mQuickContactEnabled) {
728             throw new IllegalStateException("QuickContact is disabled for this view");
729         }
730         if (mQuickContact == null) {
731             mQuickContact = new QuickContactBadge(mContext);
732             mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
733             if (mNameTextView != null) {
734                 mQuickContact.setContentDescription(mContext.getString(
735                         R.string.description_quick_contact_for, mNameTextView.getText()));
736             }
737 
738             addView(mQuickContact);
739             mPhotoViewWidthAndHeightAreReady = false;
740         }
741         return mQuickContact;
742     }
743 
744     /**
745      * Returns the photo view, creating it if necessary.
746      */
getPhotoView()747     public ImageView getPhotoView() {
748         if (mPhotoView == null) {
749             mPhotoView = new ImageView(mContext);
750             mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
751             // Quick contact style used above will set a background - remove it
752             mPhotoView.setBackground(null);
753             addView(mPhotoView);
754             mPhotoViewWidthAndHeightAreReady = false;
755         }
756         return mPhotoView;
757     }
758 
759     /**
760      * Removes the photo view.
761      */
removePhotoView()762     public void removePhotoView() {
763         removePhotoView(false, true);
764     }
765 
766     /**
767      * Removes the photo view.
768      *
769      * @param keepHorizontalPadding True means data on the right side will have
770      *            padding on left, pretending there is still a photo view.
771      * @param keepVerticalPadding True means the View will have some height
772      *            enough for accommodating a photo view.
773      */
removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)774     public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
775         mPhotoViewWidthAndHeightAreReady = false;
776         mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
777         mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
778         if (mPhotoView != null) {
779             removeView(mPhotoView);
780             mPhotoView = null;
781         }
782         if (mQuickContact != null) {
783             removeView(mQuickContact);
784             mQuickContact = null;
785         }
786     }
787 
788     /**
789      * Sets a word prefix that will be highlighted if encountered in fields like
790      * name and search snippet.
791      * <p>
792      * NOTE: must be all upper-case
793      */
setHighlightedPrefix(char[] upperCasePrefix)794     public void setHighlightedPrefix(char[] upperCasePrefix) {
795         mHighlightedPrefix = upperCasePrefix;
796     }
797 
798     /**
799      * Returns the text view for the contact name, creating it if necessary.
800      */
getNameTextView()801     public TextView getNameTextView() {
802         if (mNameTextView == null) {
803             mNameTextView = new TextView(mContext);
804             mNameTextView.setSingleLine(true);
805             mNameTextView.setEllipsize(getTextEllipsis());
806             mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
807             // Manually call setActivated() since this view may be added after the first
808             // setActivated() call toward this whole item view.
809             mNameTextView.setActivated(isActivated());
810             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
811             addView(mNameTextView);
812         }
813         return mNameTextView;
814     }
815 
816     /**
817      * Adds or updates a text view for the phonetic name.
818      */
setPhoneticName(char[] text, int size)819     public void setPhoneticName(char[] text, int size) {
820         if (text == null || size == 0) {
821             if (mPhoneticNameTextView != null) {
822                 mPhoneticNameTextView.setVisibility(View.GONE);
823             }
824         } else {
825             getPhoneticNameTextView();
826             setMarqueeText(mPhoneticNameTextView, text, size);
827             mPhoneticNameTextView.setVisibility(VISIBLE);
828         }
829     }
830 
831     /**
832      * Returns the text view for the phonetic name, creating it if necessary.
833      */
getPhoneticNameTextView()834     public TextView getPhoneticNameTextView() {
835         if (mPhoneticNameTextView == null) {
836             mPhoneticNameTextView = new TextView(mContext);
837             mPhoneticNameTextView.setSingleLine(true);
838             mPhoneticNameTextView.setEllipsize(getTextEllipsis());
839             mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
840             mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
841             mPhoneticNameTextView.setActivated(isActivated());
842             addView(mPhoneticNameTextView);
843         }
844         return mPhoneticNameTextView;
845     }
846 
847     /**
848      * Adds or updates a text view for the data label.
849      */
setLabel(CharSequence text)850     public void setLabel(CharSequence text) {
851         if (TextUtils.isEmpty(text)) {
852             if (mLabelView != null) {
853                 mLabelView.setVisibility(View.GONE);
854             }
855         } else {
856             getLabelView();
857             setMarqueeText(mLabelView, text);
858             mLabelView.setVisibility(VISIBLE);
859         }
860     }
861 
862     /**
863      * Returns the text view for the data label, creating it if necessary.
864      */
getLabelView()865     public TextView getLabelView() {
866         if (mLabelView == null) {
867             mLabelView = new TextView(mContext);
868             mLabelView.setSingleLine(true);
869             mLabelView.setEllipsize(getTextEllipsis());
870             mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
871             if (mPhotoPosition == PhotoPosition.LEFT) {
872                 mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
873                 mLabelView.setAllCaps(true);
874                 mLabelView.setGravity(Gravity.RIGHT);
875             } else {
876                 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
877             }
878             mLabelView.setActivated(isActivated());
879             addView(mLabelView);
880         }
881         return mLabelView;
882     }
883 
884     /**
885      * Adds or updates a text view for the data element.
886      */
setData(char[] text, int size)887     public void setData(char[] text, int size) {
888         if (text == null || size == 0) {
889             if (mDataView != null) {
890                 mDataView.setVisibility(View.GONE);
891             }
892         } else {
893             getDataView();
894             setMarqueeText(mDataView, text, size);
895             mDataView.setVisibility(VISIBLE);
896         }
897     }
898 
setMarqueeText(TextView textView, char[] text, int size)899     private void setMarqueeText(TextView textView, char[] text, int size) {
900         if (getTextEllipsis() == TruncateAt.MARQUEE) {
901             setMarqueeText(textView, new String(text, 0, size));
902         } else {
903             textView.setText(text, 0, size);
904         }
905     }
906 
setMarqueeText(TextView textView, CharSequence text)907     private void setMarqueeText(TextView textView, CharSequence text) {
908         if (getTextEllipsis() == TruncateAt.MARQUEE) {
909             // To show MARQUEE correctly (with END effect during non-active state), we need
910             // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
911             final SpannableString spannable = new SpannableString(text);
912             spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
913                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
914             textView.setText(spannable);
915         } else {
916             textView.setText(text);
917         }
918     }
919 
920     /**
921      * Returns the text view for the data text, creating it if necessary.
922      */
getDataView()923     public TextView getDataView() {
924         if (mDataView == null) {
925             mDataView = new TextView(mContext);
926             mDataView.setSingleLine(true);
927             mDataView.setEllipsize(getTextEllipsis());
928             mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
929             mDataView.setActivated(isActivated());
930             addView(mDataView);
931         }
932         return mDataView;
933     }
934 
935     /**
936      * Adds or updates a text view for the search snippet.
937      */
setSnippet(String text)938     public void setSnippet(String text) {
939         if (TextUtils.isEmpty(text)) {
940             if (mSnippetView != null) {
941                 mSnippetView.setVisibility(View.GONE);
942             }
943         } else {
944             mPrefixHighlighter.setText(getSnippetView(), text, mHighlightedPrefix);
945             mSnippetView.setVisibility(VISIBLE);
946         }
947     }
948 
949     /**
950      * Returns the text view for the search snippet, creating it if necessary.
951      */
getSnippetView()952     public TextView getSnippetView() {
953         if (mSnippetView == null) {
954             mSnippetView = new TextView(mContext);
955             mSnippetView.setSingleLine(true);
956             mSnippetView.setEllipsize(getTextEllipsis());
957             mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
958             mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
959             mSnippetView.setActivated(isActivated());
960             addView(mSnippetView);
961         }
962         return mSnippetView;
963     }
964 
965     /**
966      * Returns the text view for the status, creating it if necessary.
967      */
getStatusView()968     public TextView getStatusView() {
969         if (mStatusView == null) {
970             mStatusView = new TextView(mContext);
971             mStatusView.setSingleLine(true);
972             mStatusView.setEllipsize(getTextEllipsis());
973             mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
974             mStatusView.setTextColor(mSecondaryTextColor);
975             mStatusView.setActivated(isActivated());
976             addView(mStatusView);
977         }
978         return mStatusView;
979     }
980 
981     /**
982      * Returns the text view for the contacts count, creating it if necessary.
983      */
getCountView()984     public TextView getCountView() {
985         if (mCountView == null) {
986             mCountView = new TextView(mContext);
987             mCountView.setSingleLine(true);
988             mCountView.setEllipsize(getTextEllipsis());
989             mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
990             mCountView.setTextColor(R.color.contact_count_text_color);
991             addView(mCountView);
992         }
993         return mCountView;
994     }
995 
996     /**
997      * Adds or updates a text view for the contacts count.
998      */
setCountView(CharSequence text)999     public void setCountView(CharSequence text) {
1000         if (TextUtils.isEmpty(text)) {
1001             if (mCountView != null) {
1002                 mCountView.setVisibility(View.GONE);
1003             }
1004         } else {
1005             getCountView();
1006             setMarqueeText(mCountView, text);
1007             mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
1008             mCountView.setGravity(Gravity.CENTER_VERTICAL);
1009             mCountView.setTextColor(mContactsCountTextColor);
1010             mCountView.setVisibility(VISIBLE);
1011         }
1012     }
1013 
1014     /**
1015      * Adds or updates a text view for the status.
1016      */
setStatus(CharSequence text)1017     public void setStatus(CharSequence text) {
1018         if (TextUtils.isEmpty(text)) {
1019             if (mStatusView != null) {
1020                 mStatusView.setVisibility(View.GONE);
1021             }
1022         } else {
1023             getStatusView();
1024             setMarqueeText(mStatusView, text);
1025             mStatusView.setVisibility(VISIBLE);
1026         }
1027     }
1028 
1029     /**
1030      * Adds or updates the presence icon view.
1031      */
setPresence(Drawable icon)1032     public void setPresence(Drawable icon) {
1033         if (icon != null) {
1034             if (mPresenceIcon == null) {
1035                 mPresenceIcon = new ImageView(mContext);
1036                 addView(mPresenceIcon);
1037             }
1038             mPresenceIcon.setImageDrawable(icon);
1039             mPresenceIcon.setScaleType(ScaleType.CENTER);
1040             mPresenceIcon.setVisibility(View.VISIBLE);
1041         } else {
1042             if (mPresenceIcon != null) {
1043                 mPresenceIcon.setVisibility(View.GONE);
1044             }
1045         }
1046     }
1047 
getTextEllipsis()1048     private TruncateAt getTextEllipsis() {
1049         return TruncateAt.MARQUEE;
1050     }
1051 
showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1052     public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1053         CharSequence name = cursor.getString(nameColumnIndex);
1054         if (!TextUtils.isEmpty(name)) {
1055             name = mPrefixHighlighter.apply(name, mHighlightedPrefix);
1056         } else {
1057             name = mUnknownNameText;
1058         }
1059         setMarqueeText(getNameTextView(), name);
1060 
1061         // Since the quick contact content description is derived from the display name and there is
1062         // no guarantee that when the quick contact is initialized the display name is already set,
1063         // do it here too.
1064         if (mQuickContact != null) {
1065             mQuickContact.setContentDescription(mContext.getString(
1066                     R.string.description_quick_contact_for, mNameTextView.getText()));
1067         }
1068     }
1069 
hideDisplayName()1070     public void hideDisplayName() {
1071         if (mNameTextView != null) {
1072             removeView(mNameTextView);
1073             mNameTextView = null;
1074         }
1075     }
1076 
showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1077     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1078         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1079         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1080         if (phoneticNameSize != 0) {
1081             setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1082         } else {
1083             setPhoneticName(null, 0);
1084         }
1085     }
1086 
hidePhoneticName()1087     public void hidePhoneticName() {
1088         if (mPhoneticNameTextView != null) {
1089             removeView(mPhoneticNameTextView);
1090             mPhoneticNameTextView = null;
1091         }
1092     }
1093 
1094     /**
1095      * Sets the proper icon (star or presence or nothing) and/or status message.
1096      */
showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1097     public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1098             int contactStatusColumnIndex) {
1099         Drawable icon = null;
1100         int presence = 0;
1101         if (!cursor.isNull(presenceColumnIndex)) {
1102             presence = cursor.getInt(presenceColumnIndex);
1103             icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1104         }
1105         setPresence(icon);
1106 
1107         String statusMessage = null;
1108         if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1109             statusMessage = cursor.getString(contactStatusColumnIndex);
1110         }
1111         // If there is no status message from the contact, but there was a presence value, then use
1112         // the default status message string
1113         if (statusMessage == null && presence != 0) {
1114             statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1115         }
1116         setStatus(statusMessage);
1117     }
1118 
1119     /**
1120      * Shows search snippet.
1121      */
showSnippet(Cursor cursor, int summarySnippetColumnIndex)1122     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
1123         if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
1124             setSnippet(null);
1125             return;
1126         }
1127         String snippet;
1128         String columnContent = cursor.getString(summarySnippetColumnIndex);
1129 
1130         // Do client side snippeting if provider didn't do it
1131         Bundle extras = cursor.getExtras();
1132         if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
1133             int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1134 
1135             snippet = ContactsContract.snippetize(columnContent,
1136                     displayNameIndex < 0 ? null : cursor.getString(displayNameIndex),
1137                             extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY),
1138                             DefaultContactListAdapter.SNIPPET_START_MATCH,
1139                             DefaultContactListAdapter.SNIPPET_END_MATCH,
1140                             DefaultContactListAdapter.SNIPPET_ELLIPSIS,
1141                             DefaultContactListAdapter.SNIPPET_MAX_TOKENS);
1142         } else {
1143             snippet = columnContent;
1144         }
1145 
1146         if (snippet != null) {
1147             int from = 0;
1148             int to = snippet.length();
1149             int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1150             if (start == -1) {
1151                 snippet = null;
1152             } else {
1153                 int firstNl = snippet.lastIndexOf('\n', start);
1154                 if (firstNl != -1) {
1155                     from = firstNl + 1;
1156                 }
1157                 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1158                 if (end != -1) {
1159                     int lastNl = snippet.indexOf('\n', end);
1160                     if (lastNl != -1) {
1161                         to = lastNl;
1162                     }
1163                 }
1164 
1165                 StringBuilder sb = new StringBuilder();
1166                 for (int i = from; i < to; i++) {
1167                     char c = snippet.charAt(i);
1168                     if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1169                             c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1170                         sb.append(c);
1171                     }
1172                 }
1173                 snippet = sb.toString();
1174             }
1175         }
1176         setSnippet(snippet);
1177     }
1178 
1179     /**
1180      * Shows data element (e.g. phone number).
1181      */
showData(Cursor cursor, int dataColumnIndex)1182     public void showData(Cursor cursor, int dataColumnIndex) {
1183         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1184         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1185     }
1186 
setActivatedStateSupported(boolean flag)1187     public void setActivatedStateSupported(boolean flag) {
1188         this.mActivatedStateSupported = flag;
1189     }
1190 
1191     @Override
requestLayout()1192     public void requestLayout() {
1193         // We will assume that once measured this will not need to resize
1194         // itself, so there is no need to pass the layout request to the parent
1195         // view (ListView).
1196         forceLayout();
1197     }
1198 
setPhotoPosition(PhotoPosition photoPosition)1199     public void setPhotoPosition(PhotoPosition photoPosition) {
1200         mPhotoPosition = photoPosition;
1201     }
1202 
getPhotoPosition()1203     public PhotoPosition getPhotoPosition() {
1204         return mPhotoPosition;
1205     }
1206 
1207     /**
1208      * Specifies left and right margin for selection bounds. See also
1209      * {@link #adjustListItemSelectionBounds(Rect)}.
1210      */
setSelectionBoundsHorizontalMargin(int left, int right)1211     public void setSelectionBoundsHorizontalMargin(int left, int right) {
1212         mSelectionBoundsMarginLeft = left;
1213         mSelectionBoundsMarginRight = right;
1214     }
1215 }
1216