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.compat.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 
CheckedTextView(Context context)77     public CheckedTextView(Context context) {
78         this(context, null);
79     }
80 
CheckedTextView(Context context, AttributeSet attrs)81     public CheckedTextView(Context context, AttributeSet attrs) {
82         this(context, attrs, R.attr.checkedTextViewStyle);
83     }
84 
CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr)85     public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
86         this(context, attrs, defStyleAttr, 0);
87     }
88 
CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 
toggle()124     public void toggle() {
125         setChecked(!mChecked);
126     }
127 
128     @ViewDebug.ExportedProperty
129     @InspectableProperty
isChecked()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      */
setChecked(boolean checked)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      */
setCheckMarkDrawable(@rawableRes int resId)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      */
setCheckMarkDrawable(@ullable Drawable d)181     public void setCheckMarkDrawable(@Nullable Drawable d) {
182         setCheckMarkDrawableInternal(d, 0);
183     }
184 
setCheckMarkDrawableInternal(@ullable Drawable d, @DrawableRes int resId)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      */
setCheckMarkTintList(@ullable ColorStateList tint)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
getCheckMarkTintList()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      */
setCheckMarkTintMode(@ullable PorterDuff.Mode tintMode)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      */
setCheckMarkTintBlendMode(@ullable BlendMode tintMode)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
getCheckMarkTintMode()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
getCheckMarkTintBlendMode()313     public BlendMode getCheckMarkTintBlendMode() {
314         return mCheckMarkBlendMode;
315     }
316 
applyCheckMarkTint()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
setVisibility(int visibility)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
jumpDrawablesToCurrentState()348     public void jumpDrawablesToCurrentState() {
349         super.jumpDrawablesToCurrentState();
350 
351         if (mCheckMarkDrawable != null) {
352             mCheckMarkDrawable.jumpToCurrentState();
353         }
354     }
355 
356     @Override
verifyDrawable(@onNull Drawable who)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")
getCheckMarkDrawable()372     public Drawable getCheckMarkDrawable() {
373         return mCheckMarkDrawable;
374     }
375 
376     /**
377      * @hide
378      */
379     @Override
internalSetPadding(int left, int top, int right, int bottom)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
onRtlPropertiesChanged(int layoutDirection)386     public void onRtlPropertiesChanged(int layoutDirection) {
387         super.onRtlPropertiesChanged(layoutDirection);
388         updatePadding();
389     }
390 
updatePadding()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 
setBasePadding(boolean checkmarkAtStart)408     private void setBasePadding(boolean checkmarkAtStart) {
409         if (checkmarkAtStart) {
410             mBasePadding = mPaddingLeft;
411         } else {
412             mBasePadding = mPaddingRight;
413         }
414     }
415 
isCheckMarkAtStart()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
onDraw(Canvas canvas)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
onCreateDrawableState(int extraSpace)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
drawableStateChanged()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
drawableHotspotChanged(float x, float y)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
getAccessibilityClassName()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          */
SavedState(Parcelable superState)505         SavedState(Parcelable superState) {
506             super(superState);
507         }
508 
509         /**
510          * Constructor called from {@link #CREATOR}
511          */
SavedState(Parcel in)512         private SavedState(Parcel in) {
513             super(in);
514             checked = (Boolean)in.readValue(null);
515         }
516 
517         @Override
writeToParcel(Parcel out, int flags)518         public void writeToParcel(Parcel out, int flags) {
519             super.writeToParcel(out, flags);
520             out.writeValue(checked);
521         }
522 
523         @Override
toString()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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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
onInitializeAccessibilityEventInternal(AccessibilityEvent event)563     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
564         super.onInitializeAccessibilityEventInternal(event);
565         event.setChecked(mChecked);
566     }
567 
568     /** @hide */
569     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)570     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
571         super.onInitializeAccessibilityNodeInfoInternal(info);
572         info.setCheckable(true);
573         info.setChecked(mChecked);
574     }
575 
576     /** @hide */
577     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)578     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
579         super.encodeProperties(stream);
580         stream.addProperty("text:checked", isChecked());
581     }
582 }
583