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