Home | History | Annotate | Download | only in contacts
      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;
     18 
     19 import com.android.contacts.ui.widget.DontPressWithParentImageView;
     20 
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Canvas;
     25 import android.graphics.Typeface;
     26 import android.graphics.drawable.Drawable;
     27 import android.provider.ContactsContract.Contacts;
     28 import android.text.TextUtils;
     29 import android.text.TextUtils.TruncateAt;
     30 import android.util.AttributeSet;
     31 import android.view.Gravity;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.widget.ImageView;
     35 import android.widget.QuickContactBadge;
     36 import android.widget.TextView;
     37 import android.widget.ImageView.ScaleType;
     38 
     39 /**
     40  * A custom view for an item in the contact list.
     41  */
     42 public class ContactListItemView extends ViewGroup {
     43 
     44     private static final int QUICK_CONTACT_BADGE_STYLE =
     45             com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
     46 
     47     private final Context mContext;
     48 
     49     private final int mPreferredHeight;
     50     private final int mVerticalDividerMargin;
     51     private final int mPaddingTop;
     52     private final int mPaddingRight;
     53     private final int mPaddingBottom;
     54     private final int mPaddingLeft;
     55     private final int mGapBetweenImageAndText;
     56     private final int mGapBetweenLabelAndData;
     57     private final int mCallButtonPadding;
     58     private final int mPresenceIconMargin;
     59     private final int mHeaderTextWidth;
     60 
     61     private boolean mHorizontalDividerVisible;
     62     private Drawable mHorizontalDividerDrawable;
     63     private int mHorizontalDividerHeight;
     64 
     65     private boolean mVerticalDividerVisible;
     66     private Drawable mVerticalDividerDrawable;
     67     private int mVerticalDividerWidth;
     68 
     69     private boolean mHeaderVisible;
     70     private Drawable mHeaderBackgroundDrawable;
     71     private int mHeaderBackgroundHeight;
     72     private TextView mHeaderTextView;
     73 
     74     private QuickContactBadge mQuickContact;
     75     private ImageView mPhotoView;
     76     private TextView mNameTextView;
     77     private DontPressWithParentImageView mCallButton;
     78     private TextView mLabelView;
     79     private TextView mDataView;
     80     private TextView mSnippetView;
     81     private ImageView mPresenceIcon;
     82 
     83     private int mPhotoViewWidth;
     84     private int mPhotoViewHeight;
     85     private int mLine1Height;
     86     private int mLine2Height;
     87     private int mLine3Height;
     88 
     89     private OnClickListener mCallButtonClickListener;
     90 
     91     public ContactListItemView(Context context, AttributeSet attrs) {
     92         super(context, attrs);
     93         mContext = context;
     94 
     95         // Obtain preferred item height from the current theme
     96         TypedArray a = context.obtainStyledAttributes(null, com.android.internal.R.styleable.Theme);
     97         mPreferredHeight =
     98                 a.getDimensionPixelSize(android.R.styleable.Theme_listPreferredItemHeight, 0);
     99         a.recycle();
    100 
    101         Resources resources = context.getResources();
    102         mVerticalDividerMargin =
    103                 resources.getDimensionPixelOffset(R.dimen.list_item_vertical_divider_margin);
    104         mPaddingTop =
    105                 resources.getDimensionPixelOffset(R.dimen.list_item_padding_top);
    106         mPaddingBottom =
    107                 resources.getDimensionPixelOffset(R.dimen.list_item_padding_bottom);
    108         mPaddingLeft =
    109                 resources.getDimensionPixelOffset(R.dimen.list_item_padding_left);
    110         mPaddingRight =
    111                 resources.getDimensionPixelOffset(R.dimen.list_item_padding_right);
    112         mGapBetweenImageAndText =
    113                 resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_image_and_text);
    114         mGapBetweenLabelAndData =
    115                 resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_label_and_data);
    116         mCallButtonPadding =
    117                 resources.getDimensionPixelOffset(R.dimen.list_item_call_button_padding);
    118         mPresenceIconMargin =
    119                 resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin);
    120         mHeaderTextWidth =
    121                 resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width);
    122     }
    123 
    124     /**
    125      * Installs a call button listener.
    126      */
    127     public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) {
    128         mCallButtonClickListener = callButtonClickListener;
    129     }
    130 
    131     @Override
    132     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    133         // We will match parent's width and wrap content vertically, but make sure
    134         // height is no less than listPreferredItemHeight.
    135         int width = resolveSize(0, widthMeasureSpec);
    136         int height = 0;
    137 
    138         mLine1Height = 0;
    139         mLine2Height = 0;
    140         mLine3Height = 0;
    141 
    142         // Obtain the natural dimensions of the name text (we only care about height)
    143         mNameTextView.measure(0, 0);
    144 
    145         mLine1Height = mNameTextView.getMeasuredHeight();
    146 
    147         if (isVisible(mLabelView)) {
    148             mLabelView.measure(0, 0);
    149             mLine2Height = mLabelView.getMeasuredHeight();
    150         }
    151 
    152         if (isVisible(mDataView)) {
    153             mDataView.measure(0, 0);
    154             mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight());
    155         }
    156 
    157         if (isVisible(mSnippetView)) {
    158             mSnippetView.measure(0, 0);
    159             mLine3Height = mSnippetView.getMeasuredHeight();
    160         }
    161 
    162         height += mLine1Height + mLine2Height + mLine3Height;
    163 
    164         if (isVisible(mCallButton)) {
    165             mCallButton.measure(0, 0);
    166         }
    167         if (isVisible(mPresenceIcon)) {
    168             mPresenceIcon.measure(0, 0);
    169         }
    170 
    171         ensurePhotoViewSize();
    172 
    173         height = Math.max(height, mPhotoViewHeight);
    174         height = Math.max(height, mPreferredHeight);
    175 
    176         if (mHeaderVisible) {
    177             ensureHeaderBackground();
    178             mHeaderTextView.measure(
    179                     MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY),
    180                     MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
    181             height += mHeaderBackgroundDrawable.getIntrinsicHeight();
    182         }
    183 
    184         setMeasuredDimension(width, height);
    185     }
    186 
    187     @Override
    188     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    189         int height = bottom - top;
    190         int width = right - left;
    191 
    192         // Determine the vertical bounds by laying out the header first.
    193         int topBound = 0;
    194 
    195         if (mHeaderVisible) {
    196             mHeaderBackgroundDrawable.setBounds(
    197                     0,
    198                     0,
    199                     width,
    200                     mHeaderBackgroundHeight);
    201             mHeaderTextView.layout(0, 0, width, mHeaderBackgroundHeight);
    202             topBound += mHeaderBackgroundHeight;
    203         }
    204 
    205         // Positions of views on the left are fixed and so are those on the right side.
    206         // The stretchable part of the layout is in the middle.  So, we will start off
    207         // by laying out the left and right sides. Then we will allocate the remainder
    208         // to the text fields in the middle.
    209 
    210         // Left side
    211         int leftBound = mPaddingLeft;
    212         View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
    213         if (photoView != null) {
    214             // Center the photo vertically
    215             int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
    216             photoView.layout(
    217                     leftBound,
    218                     photoTop,
    219                     leftBound + mPhotoViewWidth,
    220                     photoTop + mPhotoViewHeight);
    221             leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
    222         }
    223 
    224         // Right side
    225         int rightBound = right;
    226         if (isVisible(mCallButton)) {
    227             int buttonWidth = mCallButton.getMeasuredWidth();
    228             rightBound -= buttonWidth;
    229             mCallButton.layout(
    230                     rightBound,
    231                     topBound,
    232                     rightBound + buttonWidth,
    233                     height);
    234             mVerticalDividerVisible = true;
    235             ensureVerticalDivider();
    236             rightBound -= mVerticalDividerWidth;
    237             mVerticalDividerDrawable.setBounds(
    238                     rightBound,
    239                     topBound + mVerticalDividerMargin,
    240                     rightBound + mVerticalDividerWidth,
    241                     height - mVerticalDividerMargin);
    242         } else {
    243             mVerticalDividerVisible = false;
    244         }
    245 
    246         if (isVisible(mPresenceIcon)) {
    247             int iconWidth = mPresenceIcon.getMeasuredWidth();
    248             rightBound -= mPresenceIconMargin + iconWidth;
    249             mPresenceIcon.layout(
    250                     rightBound,
    251                     topBound,
    252                     rightBound + iconWidth,
    253                     height);
    254         }
    255 
    256         if (mHorizontalDividerVisible) {
    257             ensureHorizontalDivider();
    258             mHorizontalDividerDrawable.setBounds(
    259                     0,
    260                     height - mHorizontalDividerHeight,
    261                     width,
    262                     height);
    263         }
    264 
    265         topBound += mPaddingTop;
    266         int bottomBound = height - mPaddingBottom;
    267 
    268         // Text lines, centered vertically
    269         rightBound -= mPaddingRight;
    270 
    271         // Center text vertically
    272         int totalTextHeight = mLine1Height + mLine2Height + mLine3Height;
    273         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
    274 
    275         mNameTextView.layout(leftBound,
    276                 textTopBound,
    277                 rightBound,
    278                 textTopBound + mLine1Height);
    279 
    280         int dataLeftBound = leftBound;
    281         if (isVisible(mLabelView)) {
    282             dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
    283             mLabelView.layout(leftBound,
    284                     textTopBound + mLine1Height,
    285                     dataLeftBound,
    286                     textTopBound + mLine1Height + mLine2Height);
    287             dataLeftBound += mGapBetweenLabelAndData;
    288         }
    289 
    290         if (isVisible(mDataView)) {
    291             mDataView.layout(dataLeftBound,
    292                     textTopBound + mLine1Height,
    293                     rightBound,
    294                     textTopBound + mLine1Height + mLine2Height);
    295         }
    296 
    297         if (isVisible(mSnippetView)) {
    298             mSnippetView.layout(leftBound,
    299                     textTopBound + mLine1Height + mLine2Height,
    300                     rightBound,
    301                     textTopBound + mLine1Height + mLine2Height + mLine3Height);
    302         }
    303     }
    304 
    305     private boolean isVisible(View view) {
    306         return view != null && view.getVisibility() == View.VISIBLE;
    307     }
    308 
    309     /**
    310      * Loads the drawable for the vertical divider if it has not yet been loaded.
    311      */
    312     private void ensureVerticalDivider() {
    313         if (mVerticalDividerDrawable == null) {
    314             mVerticalDividerDrawable = mContext.getResources().getDrawable(
    315                     R.drawable.divider_vertical_dark);
    316             mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
    317         }
    318     }
    319 
    320     /**
    321      * Loads the drawable for the horizontal divider if it has not yet been loaded.
    322      */
    323     private void ensureHorizontalDivider() {
    324         if (mHorizontalDividerDrawable == null) {
    325             mHorizontalDividerDrawable = mContext.getResources().getDrawable(
    326                     com.android.internal.R.drawable.divider_horizontal_dark_opaque);
    327             mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
    328         }
    329     }
    330 
    331     /**
    332      * Loads the drawable for the header background if it has not yet been loaded.
    333      */
    334     private void ensureHeaderBackground() {
    335         if (mHeaderBackgroundDrawable == null) {
    336             mHeaderBackgroundDrawable = mContext.getResources().getDrawable(
    337                     android.R.drawable.dark_header);
    338             mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight();
    339         }
    340     }
    341 
    342     /**
    343      * Extracts width and height from the style
    344      */
    345     private void ensurePhotoViewSize() {
    346         if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) {
    347             TypedArray a = mContext.obtainStyledAttributes(null,
    348                     com.android.internal.R.styleable.ViewGroup_Layout,
    349                     QUICK_CONTACT_BADGE_STYLE, 0);
    350             mPhotoViewWidth = a.getLayoutDimension(
    351                     android.R.styleable.ViewGroup_Layout_layout_width,
    352                     ViewGroup.LayoutParams.WRAP_CONTENT);
    353             mPhotoViewHeight = a.getLayoutDimension(
    354                     android.R.styleable.ViewGroup_Layout_layout_height,
    355                     ViewGroup.LayoutParams.WRAP_CONTENT);
    356             a.recycle();
    357         }
    358     }
    359 
    360     @Override
    361     public void dispatchDraw(Canvas canvas) {
    362         if (mHeaderVisible) {
    363             mHeaderBackgroundDrawable.draw(canvas);
    364         }
    365         if (mHorizontalDividerVisible) {
    366             mHorizontalDividerDrawable.draw(canvas);
    367         }
    368         if (mVerticalDividerVisible) {
    369             mVerticalDividerDrawable.draw(canvas);
    370         }
    371         super.dispatchDraw(canvas);
    372     }
    373 
    374     /**
    375      * Sets the flag that determines whether a divider should drawn at the bottom
    376      * of the view.
    377      */
    378     public void setDividerVisible(boolean visible) {
    379         mHorizontalDividerVisible = visible;
    380     }
    381 
    382     /**
    383      * Sets section header or makes it invisible if the title is null.
    384      */
    385     public void setSectionHeader(String title) {
    386         if (!TextUtils.isEmpty(title)) {
    387             if (mHeaderTextView == null) {
    388                 mHeaderTextView = new TextView(mContext);
    389                 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
    390                 mHeaderTextView.setTextColor(mContext.getResources()
    391                         .getColor(com.android.internal.R.color.dim_foreground_dark));
    392                 mHeaderTextView.setTextSize(14);
    393                 mHeaderTextView.setGravity(Gravity.CENTER);
    394                 addView(mHeaderTextView);
    395             }
    396             mHeaderTextView.setText(title);
    397             mHeaderTextView.setVisibility(View.VISIBLE);
    398             mHeaderVisible = true;
    399         } else {
    400             if (mHeaderTextView != null) {
    401                 mHeaderTextView.setVisibility(View.GONE);
    402             }
    403             mHeaderVisible = false;
    404         }
    405     }
    406 
    407     /**
    408      * Returns the quick contact badge, creating it if necessary.
    409      */
    410     public QuickContactBadge getQuickContact() {
    411         if (mQuickContact == null) {
    412             mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
    413             mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
    414             addView(mQuickContact);
    415         }
    416         return mQuickContact;
    417     }
    418 
    419     /**
    420      * Returns the photo view, creating it if necessary.
    421      */
    422     public ImageView getPhotoView() {
    423         if (mPhotoView == null) {
    424             mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
    425             // Quick contact style used above will set a background - remove it
    426             mPhotoView.setBackgroundDrawable(null);
    427             addView(mPhotoView);
    428         }
    429         return mPhotoView;
    430     }
    431 
    432     /**
    433      * Returns the text view for the contact name, creating it if necessary.
    434      */
    435     public TextView getNameTextView() {
    436         if (mNameTextView == null) {
    437             mNameTextView = new TextView(mContext);
    438             mNameTextView.setSingleLine(true);
    439             mNameTextView.setEllipsize(TruncateAt.MARQUEE);
    440             mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Large);
    441             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
    442             addView(mNameTextView);
    443         }
    444         return mNameTextView;
    445     }
    446 
    447     /**
    448      * Adds a call button using the supplied arguments as an id and tag.
    449      */
    450     public void showCallButton(int id, int tag) {
    451         if (mCallButton == null) {
    452             mCallButton = new DontPressWithParentImageView(mContext, null);
    453             mCallButton.setId(id);
    454             mCallButton.setOnClickListener(mCallButtonClickListener);
    455             mCallButton.setBackgroundResource(R.drawable.call_background);
    456             mCallButton.setImageResource(android.R.drawable.sym_action_call);
    457             mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0);
    458             mCallButton.setScaleType(ScaleType.CENTER);
    459             addView(mCallButton);
    460         }
    461 
    462         mCallButton.setTag(tag);
    463         mCallButton.setVisibility(View.VISIBLE);
    464     }
    465 
    466     public void hideCallButton() {
    467         if (mCallButton != null) {
    468             mCallButton.setVisibility(View.GONE);
    469         }
    470     }
    471 
    472     /**
    473      * Adds or updates a text view for the data label.
    474      */
    475     public void setLabel(CharSequence text) {
    476         if (TextUtils.isEmpty(text)) {
    477             if (mLabelView != null) {
    478                 mLabelView.setVisibility(View.GONE);
    479             }
    480         } else {
    481             getLabelView();
    482             mLabelView.setText(text);
    483             mLabelView.setVisibility(VISIBLE);
    484         }
    485     }
    486 
    487     /**
    488      * Adds or updates a text view for the data label.
    489      */
    490     public void setLabel(char[] text, int size) {
    491         if (text == null || size == 0) {
    492             if (mLabelView != null) {
    493                 mLabelView.setVisibility(View.GONE);
    494             }
    495         } else {
    496             getLabelView();
    497             mLabelView.setText(text, 0, size);
    498             mLabelView.setVisibility(VISIBLE);
    499         }
    500     }
    501 
    502     /**
    503      * Returns the text view for the data label, creating it if necessary.
    504      */
    505     public TextView getLabelView() {
    506         if (mLabelView == null) {
    507             mLabelView = new TextView(mContext);
    508             mLabelView.setSingleLine(true);
    509             mLabelView.setEllipsize(TruncateAt.MARQUEE);
    510             mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
    511             mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
    512             addView(mLabelView);
    513         }
    514         return mLabelView;
    515     }
    516 
    517     /**
    518      * Adds or updates a text view for the data element.
    519      */
    520     public void setData(char[] text, int size) {
    521         if (text == null || size == 0) {
    522             if (mDataView != null) {
    523                 mDataView.setVisibility(View.GONE);
    524             }
    525             return;
    526         } else {
    527             getDataView();
    528             mDataView.setText(text, 0, size);
    529             mDataView.setVisibility(VISIBLE);
    530         }
    531     }
    532 
    533     /**
    534      * Returns the text view for the data text, creating it if necessary.
    535      */
    536     public TextView getDataView() {
    537         if (mDataView == null) {
    538             mDataView = new TextView(mContext);
    539             mDataView.setSingleLine(true);
    540             mDataView.setEllipsize(TruncateAt.MARQUEE);
    541             mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
    542             addView(mDataView);
    543         }
    544         return mDataView;
    545     }
    546 
    547     /**
    548      * Adds or updates a text view for the search snippet.
    549      */
    550     public void setSnippet(CharSequence text) {
    551         if (TextUtils.isEmpty(text)) {
    552             if (mSnippetView != null) {
    553                 mSnippetView.setVisibility(View.GONE);
    554             }
    555         } else {
    556             getSnippetView();
    557             mSnippetView.setText(text);
    558             mSnippetView.setVisibility(VISIBLE);
    559         }
    560     }
    561 
    562     /**
    563      * Returns the text view for the search snippet, creating it if necessary.
    564      */
    565     public TextView getSnippetView() {
    566         if (mSnippetView == null) {
    567             mSnippetView = new TextView(mContext);
    568             mSnippetView.setSingleLine(true);
    569             mSnippetView.setEllipsize(TruncateAt.MARQUEE);
    570             mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
    571             mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
    572             addView(mSnippetView);
    573         }
    574         return mSnippetView;
    575     }
    576 
    577     /**
    578      * Adds or updates the presence icon view.
    579      */
    580     public void setPresence(Drawable icon) {
    581         if (icon != null) {
    582             if (mPresenceIcon == null) {
    583                 mPresenceIcon = new ImageView(mContext);
    584                 addView(mPresenceIcon);
    585             }
    586             mPresenceIcon.setImageDrawable(icon);
    587             mPresenceIcon.setScaleType(ScaleType.CENTER);
    588             mPresenceIcon.setVisibility(View.VISIBLE);
    589         } else {
    590             if (mPresenceIcon != null) {
    591                 mPresenceIcon.setVisibility(View.GONE);
    592             }
    593         }
    594     }
    595 }
    596