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