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