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