Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 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 android.widget;
     18 
     19 import com.android.internal.R;
     20 
     21 import android.annotation.Nullable;
     22 import android.content.Context;
     23 import android.content.res.ColorStateList;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.PorterDuff;
     27 import android.graphics.drawable.Drawable;
     28 import android.util.AttributeSet;
     29 import android.view.Gravity;
     30 import android.view.RemotableViewMethod;
     31 import android.view.ViewDebug;
     32 import android.view.accessibility.AccessibilityEvent;
     33 import android.view.accessibility.AccessibilityNodeInfo;
     34 
     35 
     36 /**
     37  * An extension to TextView that supports the {@link android.widget.Checkable} interface.
     38  * This is useful when used in a {@link android.widget.ListView ListView} where the it's
     39  * {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has been set to
     40  * something other than {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}.
     41  *
     42  * @attr ref android.R.styleable#CheckedTextView_checked
     43  * @attr ref android.R.styleable#CheckedTextView_checkMark
     44  */
     45 public class CheckedTextView extends TextView implements Checkable {
     46     private boolean mChecked;
     47 
     48     private int mCheckMarkResource;
     49     private Drawable mCheckMarkDrawable;
     50     private ColorStateList mCheckMarkTintList = null;
     51     private PorterDuff.Mode mCheckMarkTintMode = null;
     52     private boolean mHasCheckMarkTint = false;
     53     private boolean mHasCheckMarkTintMode = false;
     54 
     55     private int mBasePadding;
     56     private int mCheckMarkWidth;
     57     private int mCheckMarkGravity = Gravity.END;
     58 
     59     private boolean mNeedRequestlayout;
     60 
     61     private static final int[] CHECKED_STATE_SET = {
     62         R.attr.state_checked
     63     };
     64 
     65     public CheckedTextView(Context context) {
     66         this(context, null);
     67     }
     68 
     69     public CheckedTextView(Context context, AttributeSet attrs) {
     70         this(context, attrs, R.attr.checkedTextViewStyle);
     71     }
     72 
     73     public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
     74         this(context, attrs, defStyleAttr, 0);
     75     }
     76 
     77     public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     78         super(context, attrs, defStyleAttr, defStyleRes);
     79 
     80         final TypedArray a = context.obtainStyledAttributes(
     81                 attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes);
     82 
     83         final Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark);
     84         if (d != null) {
     85             setCheckMarkDrawable(d);
     86         }
     87 
     88         if (a.hasValue(R.styleable.CheckedTextView_checkMarkTintMode)) {
     89             mCheckMarkTintMode = Drawable.parseTintMode(a.getInt(
     90                     R.styleable.CheckedTextView_checkMarkTintMode, -1), mCheckMarkTintMode);
     91             mHasCheckMarkTintMode = true;
     92         }
     93 
     94         if (a.hasValue(R.styleable.CheckedTextView_checkMarkTint)) {
     95             mCheckMarkTintList = a.getColorStateList(R.styleable.CheckedTextView_checkMarkTint);
     96             mHasCheckMarkTint = true;
     97         }
     98 
     99         mCheckMarkGravity = a.getInt(R.styleable.CheckedTextView_checkMarkGravity, Gravity.END);
    100 
    101         final boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false);
    102         setChecked(checked);
    103 
    104         a.recycle();
    105 
    106         applyCheckMarkTint();
    107     }
    108 
    109     public void toggle() {
    110         setChecked(!mChecked);
    111     }
    112 
    113     @ViewDebug.ExportedProperty
    114     public boolean isChecked() {
    115         return mChecked;
    116     }
    117 
    118     /**
    119      * <p>Changes the checked state of this text view.</p>
    120      *
    121      * @param checked true to check the text, false to uncheck it
    122      */
    123     public void setChecked(boolean checked) {
    124         if (mChecked != checked) {
    125             mChecked = checked;
    126             refreshDrawableState();
    127             notifyViewAccessibilityStateChangedIfNeeded(
    128                     AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
    129         }
    130     }
    131 
    132 
    133     /**
    134      * Set the checkmark to a given Drawable, identified by its resourece id. This will be drawn
    135      * when {@link #isChecked()} is true.
    136      *
    137      * @param resid The Drawable to use for the checkmark.
    138      *
    139      * @see #setCheckMarkDrawable(Drawable)
    140      * @see #getCheckMarkDrawable()
    141      *
    142      * @attr ref android.R.styleable#CheckedTextView_checkMark
    143      */
    144     public void setCheckMarkDrawable(int resid) {
    145         if (resid != 0 && resid == mCheckMarkResource) {
    146             return;
    147         }
    148 
    149         mCheckMarkResource = resid;
    150 
    151         Drawable d = null;
    152         if (mCheckMarkResource != 0) {
    153             d = getContext().getDrawable(mCheckMarkResource);
    154         }
    155         setCheckMarkDrawable(d);
    156     }
    157 
    158     /**
    159      * Set the checkmark to a given Drawable. This will be drawn when {@link #isChecked()} is true.
    160      *
    161      * @param d The Drawable to use for the checkmark.
    162      *
    163      * @see #setCheckMarkDrawable(int)
    164      * @see #getCheckMarkDrawable()
    165      *
    166      * @attr ref android.R.styleable#CheckedTextView_checkMark
    167      */
    168     public void setCheckMarkDrawable(Drawable d) {
    169         if (mCheckMarkDrawable != null) {
    170             mCheckMarkDrawable.setCallback(null);
    171             unscheduleDrawable(mCheckMarkDrawable);
    172         }
    173         mNeedRequestlayout = (d != mCheckMarkDrawable);
    174         if (d != null) {
    175             d.setCallback(this);
    176             d.setVisible(getVisibility() == VISIBLE, false);
    177             d.setState(CHECKED_STATE_SET);
    178             setMinHeight(d.getIntrinsicHeight());
    179 
    180             mCheckMarkWidth = d.getIntrinsicWidth();
    181             d.setState(getDrawableState());
    182             applyCheckMarkTint();
    183         } else {
    184             mCheckMarkWidth = 0;
    185         }
    186         mCheckMarkDrawable = d;
    187 
    188         // Do padding resolution. This will call internalSetPadding() and do a
    189         // requestLayout() if needed.
    190         resolvePadding();
    191     }
    192 
    193     /**
    194      * Applies a tint to the check mark drawable. Does not modify the
    195      * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
    196      * <p>
    197      * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will
    198      * automatically mutate the drawable and apply the specified tint and
    199      * tint mode using
    200      * {@link Drawable#setTintList(ColorStateList)}.
    201      *
    202      * @param tint the tint to apply, may be {@code null} to clear tint
    203      *
    204      * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
    205      * @see #getCheckMarkTintList()
    206      * @see Drawable#setTintList(ColorStateList)
    207      */
    208     public void setCheckMarkTintList(@Nullable ColorStateList tint) {
    209         mCheckMarkTintList = tint;
    210         mHasCheckMarkTint = true;
    211 
    212         applyCheckMarkTint();
    213     }
    214 
    215     /**
    216      * Returns the tint applied to the check mark drawable, if specified.
    217      *
    218      * @return the tint applied to the check mark drawable
    219      * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
    220      * @see #setCheckMarkTintList(ColorStateList)
    221      */
    222     @Nullable
    223     public ColorStateList getCheckMarkTintList() {
    224         return mCheckMarkTintList;
    225     }
    226 
    227     /**
    228      * Specifies the blending mode used to apply the tint specified by
    229      * {@link #setCheckMarkTintList(ColorStateList)} to the check mark
    230      * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
    231      *
    232      * @param tintMode the blending mode used to apply the tint, may be
    233      *                 {@code null} to clear tint
    234      * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
    235      * @see #setCheckMarkTintList(ColorStateList)
    236      * @see Drawable#setTintMode(PorterDuff.Mode)
    237      */
    238     public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
    239         mCheckMarkTintMode = tintMode;
    240         mHasCheckMarkTintMode = true;
    241 
    242         applyCheckMarkTint();
    243     }
    244 
    245     /**
    246      * Returns the blending mode used to apply the tint to the check mark
    247      * drawable, if specified.
    248      *
    249      * @return the blending mode used to apply the tint to the check mark
    250      *         drawable
    251      * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
    252      * @see #setCheckMarkTintMode(PorterDuff.Mode)
    253      */
    254     @Nullable
    255     public PorterDuff.Mode getCheckMarkTintMode() {
    256         return mCheckMarkTintMode;
    257     }
    258 
    259     private void applyCheckMarkTint() {
    260         if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) {
    261             mCheckMarkDrawable = mCheckMarkDrawable.mutate();
    262 
    263             if (mHasCheckMarkTint) {
    264                 mCheckMarkDrawable.setTintList(mCheckMarkTintList);
    265             }
    266 
    267             if (mHasCheckMarkTintMode) {
    268                 mCheckMarkDrawable.setTintMode(mCheckMarkTintMode);
    269             }
    270         }
    271     }
    272 
    273     @RemotableViewMethod
    274     @Override
    275     public void setVisibility(int visibility) {
    276         super.setVisibility(visibility);
    277 
    278         if (mCheckMarkDrawable != null) {
    279             mCheckMarkDrawable.setVisible(visibility == VISIBLE, false);
    280         }
    281     }
    282 
    283     @Override
    284     public void jumpDrawablesToCurrentState() {
    285         super.jumpDrawablesToCurrentState();
    286 
    287         if (mCheckMarkDrawable != null) {
    288             mCheckMarkDrawable.jumpToCurrentState();
    289         }
    290     }
    291 
    292     @Override
    293     protected boolean verifyDrawable(Drawable who) {
    294         return who == mCheckMarkDrawable || super.verifyDrawable(who);
    295     }
    296 
    297     /**
    298      * Gets the checkmark drawable
    299      *
    300      * @return The drawable use to represent the checkmark, if any.
    301      *
    302      * @see #setCheckMarkDrawable(Drawable)
    303      * @see #setCheckMarkDrawable(int)
    304      *
    305      * @attr ref android.R.styleable#CheckedTextView_checkMark
    306      */
    307     public Drawable getCheckMarkDrawable() {
    308         return mCheckMarkDrawable;
    309     }
    310 
    311     /**
    312      * @hide
    313      */
    314     @Override
    315     protected void internalSetPadding(int left, int top, int right, int bottom) {
    316         super.internalSetPadding(left, top, right, bottom);
    317         setBasePadding(isCheckMarkAtStart());
    318     }
    319 
    320     @Override
    321     public void onRtlPropertiesChanged(int layoutDirection) {
    322         super.onRtlPropertiesChanged(layoutDirection);
    323         updatePadding();
    324     }
    325 
    326     private void updatePadding() {
    327         resetPaddingToInitialValues();
    328         int newPadding = (mCheckMarkDrawable != null) ?
    329                 mCheckMarkWidth + mBasePadding : mBasePadding;
    330         if (isCheckMarkAtStart()) {
    331             mNeedRequestlayout |= (mPaddingLeft != newPadding);
    332             mPaddingLeft = newPadding;
    333         } else {
    334             mNeedRequestlayout |= (mPaddingRight != newPadding);
    335             mPaddingRight = newPadding;
    336         }
    337         if (mNeedRequestlayout) {
    338             requestLayout();
    339             mNeedRequestlayout = false;
    340         }
    341     }
    342 
    343     private void setBasePadding(boolean checkmarkAtStart) {
    344         if (checkmarkAtStart) {
    345             mBasePadding = mPaddingLeft;
    346         } else {
    347             mBasePadding = mPaddingRight;
    348         }
    349     }
    350 
    351     private boolean isCheckMarkAtStart() {
    352         final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection());
    353         final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
    354         return hgrav == Gravity.LEFT;
    355     }
    356 
    357     @Override
    358     protected void onDraw(Canvas canvas) {
    359         super.onDraw(canvas);
    360 
    361         final Drawable checkMarkDrawable = mCheckMarkDrawable;
    362         if (checkMarkDrawable != null) {
    363             final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
    364             final int height = checkMarkDrawable.getIntrinsicHeight();
    365 
    366             int y = 0;
    367 
    368             switch (verticalGravity) {
    369                 case Gravity.BOTTOM:
    370                     y = getHeight() - height;
    371                     break;
    372                 case Gravity.CENTER_VERTICAL:
    373                     y = (getHeight() - height) / 2;
    374                     break;
    375             }
    376 
    377             final boolean checkMarkAtStart = isCheckMarkAtStart();
    378             final int width = getWidth();
    379             final int top = y;
    380             final int bottom = top + height;
    381             final int left;
    382             final int right;
    383             if (checkMarkAtStart) {
    384                 left = mBasePadding;
    385                 right = left + mCheckMarkWidth;
    386             } else {
    387                 right = width - mBasePadding;
    388                 left = right - mCheckMarkWidth;
    389             }
    390             checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom);
    391             checkMarkDrawable.draw(canvas);
    392 
    393             final Drawable background = getBackground();
    394             if (background != null) {
    395                 background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom);
    396             }
    397         }
    398     }
    399 
    400     @Override
    401     protected int[] onCreateDrawableState(int extraSpace) {
    402         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
    403         if (isChecked()) {
    404             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
    405         }
    406         return drawableState;
    407     }
    408 
    409     @Override
    410     protected void drawableStateChanged() {
    411         super.drawableStateChanged();
    412 
    413         if (mCheckMarkDrawable != null) {
    414             int[] myDrawableState = getDrawableState();
    415 
    416             // Set the state of the Drawable
    417             mCheckMarkDrawable.setState(myDrawableState);
    418 
    419             invalidate();
    420         }
    421     }
    422 
    423     @Override
    424     public void drawableHotspotChanged(float x, float y) {
    425         super.drawableHotspotChanged(x, y);
    426 
    427         if (mCheckMarkDrawable != null) {
    428             mCheckMarkDrawable.setHotspot(x, y);
    429         }
    430     }
    431 
    432     @Override
    433     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    434         super.onInitializeAccessibilityEvent(event);
    435         event.setClassName(CheckedTextView.class.getName());
    436         event.setChecked(mChecked);
    437     }
    438 
    439     @Override
    440     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    441         super.onInitializeAccessibilityNodeInfo(info);
    442         info.setClassName(CheckedTextView.class.getName());
    443         info.setCheckable(true);
    444         info.setChecked(mChecked);
    445     }
    446 }
    447