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             // The drawable (or one of its children) may not have been
    272             // stateful before applying the tint, so let's try again.
    273             if (mCheckMarkDrawable.isStateful()) {
    274                 mCheckMarkDrawable.setState(getDrawableState());
    275             }
    276         }
    277     }
    278 
    279     @RemotableViewMethod
    280     @Override
    281     public void setVisibility(int visibility) {
    282         super.setVisibility(visibility);
    283 
    284         if (mCheckMarkDrawable != null) {
    285             mCheckMarkDrawable.setVisible(visibility == VISIBLE, false);
    286         }
    287     }
    288 
    289     @Override
    290     public void jumpDrawablesToCurrentState() {
    291         super.jumpDrawablesToCurrentState();
    292 
    293         if (mCheckMarkDrawable != null) {
    294             mCheckMarkDrawable.jumpToCurrentState();
    295         }
    296     }
    297 
    298     @Override
    299     protected boolean verifyDrawable(Drawable who) {
    300         return who == mCheckMarkDrawable || super.verifyDrawable(who);
    301     }
    302 
    303     /**
    304      * Gets the checkmark drawable
    305      *
    306      * @return The drawable use to represent the checkmark, if any.
    307      *
    308      * @see #setCheckMarkDrawable(Drawable)
    309      * @see #setCheckMarkDrawable(int)
    310      *
    311      * @attr ref android.R.styleable#CheckedTextView_checkMark
    312      */
    313     public Drawable getCheckMarkDrawable() {
    314         return mCheckMarkDrawable;
    315     }
    316 
    317     /**
    318      * @hide
    319      */
    320     @Override
    321     protected void internalSetPadding(int left, int top, int right, int bottom) {
    322         super.internalSetPadding(left, top, right, bottom);
    323         setBasePadding(isCheckMarkAtStart());
    324     }
    325 
    326     @Override
    327     public void onRtlPropertiesChanged(int layoutDirection) {
    328         super.onRtlPropertiesChanged(layoutDirection);
    329         updatePadding();
    330     }
    331 
    332     private void updatePadding() {
    333         resetPaddingToInitialValues();
    334         int newPadding = (mCheckMarkDrawable != null) ?
    335                 mCheckMarkWidth + mBasePadding : mBasePadding;
    336         if (isCheckMarkAtStart()) {
    337             mNeedRequestlayout |= (mPaddingLeft != newPadding);
    338             mPaddingLeft = newPadding;
    339         } else {
    340             mNeedRequestlayout |= (mPaddingRight != newPadding);
    341             mPaddingRight = newPadding;
    342         }
    343         if (mNeedRequestlayout) {
    344             requestLayout();
    345             mNeedRequestlayout = false;
    346         }
    347     }
    348 
    349     private void setBasePadding(boolean checkmarkAtStart) {
    350         if (checkmarkAtStart) {
    351             mBasePadding = mPaddingLeft;
    352         } else {
    353             mBasePadding = mPaddingRight;
    354         }
    355     }
    356 
    357     private boolean isCheckMarkAtStart() {
    358         final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection());
    359         final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
    360         return hgrav == Gravity.LEFT;
    361     }
    362 
    363     @Override
    364     protected void onDraw(Canvas canvas) {
    365         super.onDraw(canvas);
    366 
    367         final Drawable checkMarkDrawable = mCheckMarkDrawable;
    368         if (checkMarkDrawable != null) {
    369             final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
    370             final int height = checkMarkDrawable.getIntrinsicHeight();
    371 
    372             int y = 0;
    373 
    374             switch (verticalGravity) {
    375                 case Gravity.BOTTOM:
    376                     y = getHeight() - height;
    377                     break;
    378                 case Gravity.CENTER_VERTICAL:
    379                     y = (getHeight() - height) / 2;
    380                     break;
    381             }
    382 
    383             final boolean checkMarkAtStart = isCheckMarkAtStart();
    384             final int width = getWidth();
    385             final int top = y;
    386             final int bottom = top + height;
    387             final int left;
    388             final int right;
    389             if (checkMarkAtStart) {
    390                 left = mBasePadding;
    391                 right = left + mCheckMarkWidth;
    392             } else {
    393                 right = width - mBasePadding;
    394                 left = right - mCheckMarkWidth;
    395             }
    396             checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom);
    397             checkMarkDrawable.draw(canvas);
    398 
    399             final Drawable background = getBackground();
    400             if (background != null) {
    401                 background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom);
    402             }
    403         }
    404     }
    405 
    406     @Override
    407     protected int[] onCreateDrawableState(int extraSpace) {
    408         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
    409         if (isChecked()) {
    410             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
    411         }
    412         return drawableState;
    413     }
    414 
    415     @Override
    416     protected void drawableStateChanged() {
    417         super.drawableStateChanged();
    418 
    419         if (mCheckMarkDrawable != null) {
    420             int[] myDrawableState = getDrawableState();
    421 
    422             // Set the state of the Drawable
    423             mCheckMarkDrawable.setState(myDrawableState);
    424 
    425             invalidate();
    426         }
    427     }
    428 
    429     @Override
    430     public void drawableHotspotChanged(float x, float y) {
    431         super.drawableHotspotChanged(x, y);
    432 
    433         if (mCheckMarkDrawable != null) {
    434             mCheckMarkDrawable.setHotspot(x, y);
    435         }
    436     }
    437 
    438     @Override
    439     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    440         super.onInitializeAccessibilityEvent(event);
    441         event.setClassName(CheckedTextView.class.getName());
    442         event.setChecked(mChecked);
    443     }
    444 
    445     @Override
    446     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    447         super.onInitializeAccessibilityNodeInfo(info);
    448         info.setClassName(CheckedTextView.class.getName());
    449         info.setCheckable(true);
    450         info.setChecked(mChecked);
    451     }
    452 }
    453