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
17package android.widget;
18
19import android.annotation.NonNull;
20import android.view.ViewHierarchyEncoder;
21import com.android.internal.R;
22
23import android.annotation.DrawableRes;
24import android.annotation.Nullable;
25import android.content.Context;
26import android.content.res.ColorStateList;
27import android.content.res.TypedArray;
28import android.graphics.Canvas;
29import android.graphics.PorterDuff;
30import android.graphics.drawable.Drawable;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.util.AttributeSet;
34import android.view.Gravity;
35import android.view.RemotableViewMethod;
36import android.view.ViewDebug;
37import android.view.accessibility.AccessibilityEvent;
38import 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 */
52public 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