1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.annotation.NonNull;
20 import android.view.ViewHierarchyEncoder;
21 import com.android.internal.R;
22 
23 import android.annotation.DrawableRes;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.res.ColorStateList;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.PorterDuff;
30 import android.graphics.drawable.Drawable;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.util.AttributeSet;
34 import android.view.Gravity;
35 import android.view.RemotableViewMethod;
36 import android.view.ViewDebug;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.view.accessibility.AccessibilityNodeInfo;
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 
CheckedTextView(Context context)72     public CheckedTextView(Context context) {
73         this(context, null);
74     }
75 
CheckedTextView(Context context, AttributeSet attrs)76     public CheckedTextView(Context context, AttributeSet attrs) {
77         this(context, attrs, R.attr.checkedTextViewStyle);
78     }
79 
CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr)80     public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
81         this(context, attrs, defStyleAttr, 0);
82     }
83 
CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 
toggle()116     public void toggle() {
117         setChecked(!mChecked);
118     }
119 
120     @ViewDebug.ExportedProperty
isChecked()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      */
setChecked(boolean checked)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      */
setCheckMarkDrawable(@rawableRes int resId)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      */
setCheckMarkDrawable(@ullable Drawable d)172     public void setCheckMarkDrawable(@Nullable Drawable d) {
173         setCheckMarkDrawableInternal(d, 0);
174     }
175 
setCheckMarkDrawableInternal(@ullable Drawable d, @DrawableRes int resId)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      */
setCheckMarkTintList(@ullable ColorStateList tint)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
getCheckMarkTintList()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      */
setCheckMarkTintMode(@ullable PorterDuff.Mode tintMode)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
getCheckMarkTintMode()270     public PorterDuff.Mode getCheckMarkTintMode() {
271         return mCheckMarkTintMode;
272     }
273 
applyCheckMarkTint()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
setVisibility(int visibility)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
jumpDrawablesToCurrentState()305     public void jumpDrawablesToCurrentState() {
306         super.jumpDrawablesToCurrentState();
307 
308         if (mCheckMarkDrawable != null) {
309             mCheckMarkDrawable.jumpToCurrentState();
310         }
311     }
312 
313     @Override
verifyDrawable(@onNull Drawable who)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      */
getCheckMarkDrawable()328     public Drawable getCheckMarkDrawable() {
329         return mCheckMarkDrawable;
330     }
331 
332     /**
333      * @hide
334      */
335     @Override
internalSetPadding(int left, int top, int right, int bottom)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
onRtlPropertiesChanged(int layoutDirection)342     public void onRtlPropertiesChanged(int layoutDirection) {
343         super.onRtlPropertiesChanged(layoutDirection);
344         updatePadding();
345     }
346 
updatePadding()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 
setBasePadding(boolean checkmarkAtStart)364     private void setBasePadding(boolean checkmarkAtStart) {
365         if (checkmarkAtStart) {
366             mBasePadding = mPaddingLeft;
367         } else {
368             mBasePadding = mPaddingRight;
369         }
370     }
371 
isCheckMarkAtStart()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
onDraw(Canvas canvas)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
onCreateDrawableState(int extraSpace)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
drawableStateChanged()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
drawableHotspotChanged(float x, float y)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
getAccessibilityClassName()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          */
SavedState(Parcelable superState)461         SavedState(Parcelable superState) {
462             super(superState);
463         }
464 
465         /**
466          * Constructor called from {@link #CREATOR}
467          */
SavedState(Parcel in)468         private SavedState(Parcel in) {
469             super(in);
470             checked = (Boolean)in.readValue(null);
471         }
472 
473         @Override
writeToParcel(Parcel out, int flags)474         public void writeToParcel(Parcel out, int flags) {
475             super.writeToParcel(out, flags);
476             out.writeValue(checked);
477         }
478 
479         @Override
toString()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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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
onInitializeAccessibilityEventInternal(AccessibilityEvent event)519     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
520         super.onInitializeAccessibilityEventInternal(event);
521         event.setChecked(mChecked);
522     }
523 
524     /** @hide */
525     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)526     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
527         super.onInitializeAccessibilityNodeInfoInternal(info);
528         info.setCheckable(true);
529         info.setChecked(mChecked);
530     }
531 
532     /** @hide */
533     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)534     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
535         super.encodeProperties(stream);
536         stream.addProperty("text:checked", isChecked());
537     }
538 }
539