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