CheckedTextView.java revision 94a6d15ede149189bba9e5f474ed853c98230e75
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        mCheckMarkResource = resId;
156
157        Drawable d = null;
158        if (mCheckMarkResource != 0) {
159            d = getContext().getDrawable(mCheckMarkResource);
160        }
161        setCheckMarkDrawable(d);
162    }
163
164    /**
165     * Set the check mark to the specified drawable.
166     * <p>
167     * When this view is checked, the drawable's state set will include
168     * {@link android.R.attr#state_checked}.
169     *
170     * @param d the drawable to use for the check mark
171     * @attr ref android.R.styleable#CheckedTextView_checkMark
172     * @see #setCheckMarkDrawable(int)
173     * @see #getCheckMarkDrawable()
174     */
175    public void setCheckMarkDrawable(Drawable d) {
176        if (mCheckMarkDrawable != null) {
177            mCheckMarkDrawable.setCallback(null);
178            unscheduleDrawable(mCheckMarkDrawable);
179        }
180        mNeedRequestlayout = (d != mCheckMarkDrawable);
181        if (d != null) {
182            d.setCallback(this);
183            d.setVisible(getVisibility() == VISIBLE, false);
184            d.setState(CHECKED_STATE_SET);
185            setMinHeight(d.getIntrinsicHeight());
186
187            mCheckMarkWidth = d.getIntrinsicWidth();
188            d.setState(getDrawableState());
189            applyCheckMarkTint();
190        } else {
191            mCheckMarkWidth = 0;
192        }
193        mCheckMarkDrawable = d;
194
195        // Do padding resolution. This will call internalSetPadding() and do a
196        // requestLayout() if needed.
197        resolvePadding();
198    }
199
200    /**
201     * Applies a tint to the check mark drawable. Does not modify the
202     * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
203     * <p>
204     * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will
205     * automatically mutate the drawable and apply the specified tint and
206     * tint mode using
207     * {@link Drawable#setTintList(ColorStateList)}.
208     *
209     * @param tint the tint to apply, may be {@code null} to clear tint
210     *
211     * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
212     * @see #getCheckMarkTintList()
213     * @see Drawable#setTintList(ColorStateList)
214     */
215    public void setCheckMarkTintList(@Nullable ColorStateList tint) {
216        mCheckMarkTintList = tint;
217        mHasCheckMarkTint = true;
218
219        applyCheckMarkTint();
220    }
221
222    /**
223     * Returns the tint applied to the check mark drawable, if specified.
224     *
225     * @return the tint applied to the check mark drawable
226     * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
227     * @see #setCheckMarkTintList(ColorStateList)
228     */
229    @Nullable
230    public ColorStateList getCheckMarkTintList() {
231        return mCheckMarkTintList;
232    }
233
234    /**
235     * Specifies the blending mode used to apply the tint specified by
236     * {@link #setCheckMarkTintList(ColorStateList)} to the check mark
237     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
238     *
239     * @param tintMode the blending mode used to apply the tint, may be
240     *                 {@code null} to clear tint
241     * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
242     * @see #setCheckMarkTintList(ColorStateList)
243     * @see Drawable#setTintMode(PorterDuff.Mode)
244     */
245    public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
246        mCheckMarkTintMode = tintMode;
247        mHasCheckMarkTintMode = true;
248
249        applyCheckMarkTint();
250    }
251
252    /**
253     * Returns the blending mode used to apply the tint to the check mark
254     * drawable, if specified.
255     *
256     * @return the blending mode used to apply the tint to the check mark
257     *         drawable
258     * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
259     * @see #setCheckMarkTintMode(PorterDuff.Mode)
260     */
261    @Nullable
262    public PorterDuff.Mode getCheckMarkTintMode() {
263        return mCheckMarkTintMode;
264    }
265
266    private void applyCheckMarkTint() {
267        if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) {
268            mCheckMarkDrawable = mCheckMarkDrawable.mutate();
269
270            if (mHasCheckMarkTint) {
271                mCheckMarkDrawable.setTintList(mCheckMarkTintList);
272            }
273
274            if (mHasCheckMarkTintMode) {
275                mCheckMarkDrawable.setTintMode(mCheckMarkTintMode);
276            }
277
278            // The drawable (or one of its children) may not have been
279            // stateful before applying the tint, so let's try again.
280            if (mCheckMarkDrawable.isStateful()) {
281                mCheckMarkDrawable.setState(getDrawableState());
282            }
283        }
284    }
285
286    @RemotableViewMethod
287    @Override
288    public void setVisibility(int visibility) {
289        super.setVisibility(visibility);
290
291        if (mCheckMarkDrawable != null) {
292            mCheckMarkDrawable.setVisible(visibility == VISIBLE, false);
293        }
294    }
295
296    @Override
297    public void jumpDrawablesToCurrentState() {
298        super.jumpDrawablesToCurrentState();
299
300        if (mCheckMarkDrawable != null) {
301            mCheckMarkDrawable.jumpToCurrentState();
302        }
303    }
304
305    @Override
306    protected boolean verifyDrawable(Drawable who) {
307        return who == mCheckMarkDrawable || super.verifyDrawable(who);
308    }
309
310    /**
311     * Gets the checkmark drawable
312     *
313     * @return The drawable use to represent the checkmark, if any.
314     *
315     * @see #setCheckMarkDrawable(Drawable)
316     * @see #setCheckMarkDrawable(int)
317     *
318     * @attr ref android.R.styleable#CheckedTextView_checkMark
319     */
320    public Drawable getCheckMarkDrawable() {
321        return mCheckMarkDrawable;
322    }
323
324    /**
325     * @hide
326     */
327    @Override
328    protected void internalSetPadding(int left, int top, int right, int bottom) {
329        super.internalSetPadding(left, top, right, bottom);
330        setBasePadding(isCheckMarkAtStart());
331    }
332
333    @Override
334    public void onRtlPropertiesChanged(int layoutDirection) {
335        super.onRtlPropertiesChanged(layoutDirection);
336        updatePadding();
337    }
338
339    private void updatePadding() {
340        resetPaddingToInitialValues();
341        int newPadding = (mCheckMarkDrawable != null) ?
342                mCheckMarkWidth + mBasePadding : mBasePadding;
343        if (isCheckMarkAtStart()) {
344            mNeedRequestlayout |= (mPaddingLeft != newPadding);
345            mPaddingLeft = newPadding;
346        } else {
347            mNeedRequestlayout |= (mPaddingRight != newPadding);
348            mPaddingRight = newPadding;
349        }
350        if (mNeedRequestlayout) {
351            requestLayout();
352            mNeedRequestlayout = false;
353        }
354    }
355
356    private void setBasePadding(boolean checkmarkAtStart) {
357        if (checkmarkAtStart) {
358            mBasePadding = mPaddingLeft;
359        } else {
360            mBasePadding = mPaddingRight;
361        }
362    }
363
364    private boolean isCheckMarkAtStart() {
365        final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection());
366        final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
367        return hgrav == Gravity.LEFT;
368    }
369
370    @Override
371    protected void onDraw(Canvas canvas) {
372        super.onDraw(canvas);
373
374        final Drawable checkMarkDrawable = mCheckMarkDrawable;
375        if (checkMarkDrawable != null) {
376            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
377            final int height = checkMarkDrawable.getIntrinsicHeight();
378
379            int y = 0;
380
381            switch (verticalGravity) {
382                case Gravity.BOTTOM:
383                    y = getHeight() - height;
384                    break;
385                case Gravity.CENTER_VERTICAL:
386                    y = (getHeight() - height) / 2;
387                    break;
388            }
389
390            final boolean checkMarkAtStart = isCheckMarkAtStart();
391            final int width = getWidth();
392            final int top = y;
393            final int bottom = top + height;
394            final int left;
395            final int right;
396            if (checkMarkAtStart) {
397                left = mBasePadding;
398                right = left + mCheckMarkWidth;
399            } else {
400                right = width - mBasePadding;
401                left = right - mCheckMarkWidth;
402            }
403            checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom);
404            checkMarkDrawable.draw(canvas);
405
406            final Drawable background = getBackground();
407            if (background != null) {
408                background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom);
409            }
410        }
411    }
412
413    @Override
414    protected int[] onCreateDrawableState(int extraSpace) {
415        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
416        if (isChecked()) {
417            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
418        }
419        return drawableState;
420    }
421
422    @Override
423    protected void drawableStateChanged() {
424        super.drawableStateChanged();
425
426        if (mCheckMarkDrawable != null) {
427            int[] myDrawableState = getDrawableState();
428
429            // Set the state of the Drawable
430            mCheckMarkDrawable.setState(myDrawableState);
431
432            invalidate();
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