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