Switch.java revision 8a78fd4d9572dff95432fcc4ba0e87563415b728
1/*
2 * Copyright (C) 2010 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.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.drawable.Drawable;
28import android.text.Layout;
29import android.text.StaticLayout;
30import android.text.TextPaint;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.view.Gravity;
34import android.view.MotionEvent;
35import android.view.VelocityTracker;
36import android.view.ViewConfiguration;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
39
40import com.android.internal.R;
41
42/**
43 * A Switch is a two-state toggle switch widget that can select between two
44 * options. The user may drag the "thumb" back and forth to choose the selected option,
45 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
46 * property controls the text displayed in the label for the switch, whereas the
47 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
48 * controls the text on the thumb. Similarly, the
49 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
50 * setTypeface() methods control the typeface and style of label text, whereas the
51 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
52 * the related seSwitchTypeface() methods control that of the thumb.
53 *
54 */
55public class Switch extends CompoundButton {
56    private static final int TOUCH_MODE_IDLE = 0;
57    private static final int TOUCH_MODE_DOWN = 1;
58    private static final int TOUCH_MODE_DRAGGING = 2;
59
60    // Enum for the "typeface" XML parameter.
61    private static final int SANS = 1;
62    private static final int SERIF = 2;
63    private static final int MONOSPACE = 3;
64
65    private Drawable mThumbDrawable;
66    private Drawable mTrackDrawable;
67    private int mThumbTextPadding;
68    private int mSwitchMinWidth;
69    private int mSwitchPadding;
70    private CharSequence mTextOn;
71    private CharSequence mTextOff;
72
73    private int mTouchMode;
74    private int mTouchSlop;
75    private float mTouchX;
76    private float mTouchY;
77    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
78    private int mMinFlingVelocity;
79
80    private float mThumbPosition;
81    private int mSwitchWidth;
82    private int mSwitchHeight;
83    private int mThumbWidth; // Does not include padding
84
85    private int mSwitchLeft;
86    private int mSwitchTop;
87    private int mSwitchRight;
88    private int mSwitchBottom;
89
90    private TextPaint mTextPaint;
91    private ColorStateList mTextColors;
92    private Layout mOnLayout;
93    private Layout mOffLayout;
94
95    @SuppressWarnings("hiding")
96    private final Rect mTempRect = new Rect();
97
98    private static final int[] CHECKED_STATE_SET = {
99        R.attr.state_checked
100    };
101
102    /**
103     * Construct a new Switch with default styling.
104     *
105     * @param context The Context that will determine this widget's theming.
106     */
107    public Switch(Context context) {
108        this(context, null);
109    }
110
111    /**
112     * Construct a new Switch with default styling, overriding specific style
113     * attributes as requested.
114     *
115     * @param context The Context that will determine this widget's theming.
116     * @param attrs Specification of attributes that should deviate from default styling.
117     */
118    public Switch(Context context, AttributeSet attrs) {
119        this(context, attrs, com.android.internal.R.attr.switchStyle);
120    }
121
122    /**
123     * Construct a new Switch with a default style determined by the given theme attribute,
124     * overriding specific style attributes as requested.
125     *
126     * @param context The Context that will determine this widget's theming.
127     * @param attrs Specification of attributes that should deviate from the default styling.
128     * @param defStyle An attribute ID within the active theme containing a reference to the
129     *                 default style for this widget. e.g. android.R.attr.switchStyle.
130     */
131    public Switch(Context context, AttributeSet attrs, int defStyle) {
132        super(context, attrs, defStyle);
133
134        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
135        Resources res = getResources();
136        mTextPaint.density = res.getDisplayMetrics().density;
137        mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
138
139        TypedArray a = context.obtainStyledAttributes(attrs,
140                com.android.internal.R.styleable.Switch, defStyle, 0);
141
142        mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
143        mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
144        mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
145        mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
146        mThumbTextPadding = a.getDimensionPixelSize(
147                com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
148        mSwitchMinWidth = a.getDimensionPixelSize(
149                com.android.internal.R.styleable.Switch_switchMinWidth, 0);
150        mSwitchPadding = a.getDimensionPixelSize(
151                com.android.internal.R.styleable.Switch_switchPadding, 0);
152
153        int appearance = a.getResourceId(
154                com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
155        if (appearance != 0) {
156            setSwitchTextAppearance(context, appearance);
157        }
158        a.recycle();
159
160        ViewConfiguration config = ViewConfiguration.get(context);
161        mTouchSlop = config.getScaledTouchSlop();
162        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
163
164        // Refresh display with current params
165        refreshDrawableState();
166        setChecked(isChecked());
167    }
168
169    /**
170     * Sets the switch text color, size, style, hint color, and highlight color
171     * from the specified TextAppearance resource.
172     */
173    public void setSwitchTextAppearance(Context context, int resid) {
174        TypedArray appearance =
175                context.obtainStyledAttributes(resid,
176                        com.android.internal.R.styleable.TextAppearance);
177
178        ColorStateList colors;
179        int ts;
180
181        colors = appearance.getColorStateList(com.android.internal.R.styleable.
182                TextAppearance_textColor);
183        if (colors != null) {
184            mTextColors = colors;
185        } else {
186            // If no color set in TextAppearance, default to the view's textColor
187            mTextColors = getTextColors();
188        }
189
190        ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
191                TextAppearance_textSize, 0);
192        if (ts != 0) {
193            if (ts != mTextPaint.getTextSize()) {
194                mTextPaint.setTextSize(ts);
195                requestLayout();
196            }
197        }
198
199        int typefaceIndex, styleIndex;
200
201        typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
202                TextAppearance_typeface, -1);
203        styleIndex = appearance.getInt(com.android.internal.R.styleable.
204                TextAppearance_textStyle, -1);
205
206        setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
207
208        appearance.recycle();
209    }
210
211    private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
212        Typeface tf = null;
213        switch (typefaceIndex) {
214            case SANS:
215                tf = Typeface.SANS_SERIF;
216                break;
217
218            case SERIF:
219                tf = Typeface.SERIF;
220                break;
221
222            case MONOSPACE:
223                tf = Typeface.MONOSPACE;
224                break;
225        }
226
227        setSwitchTypeface(tf, styleIndex);
228    }
229
230    /**
231     * Sets the typeface and style in which the text should be displayed on the
232     * switch, and turns on the fake bold and italic bits in the Paint if the
233     * Typeface that you provided does not have all the bits in the
234     * style that you specified.
235     */
236    public void setSwitchTypeface(Typeface tf, int style) {
237        if (style > 0) {
238            if (tf == null) {
239                tf = Typeface.defaultFromStyle(style);
240            } else {
241                tf = Typeface.create(tf, style);
242            }
243
244            setSwitchTypeface(tf);
245            // now compute what (if any) algorithmic styling is needed
246            int typefaceStyle = tf != null ? tf.getStyle() : 0;
247            int need = style & ~typefaceStyle;
248            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
249            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
250        } else {
251            mTextPaint.setFakeBoldText(false);
252            mTextPaint.setTextSkewX(0);
253            setSwitchTypeface(tf);
254        }
255    }
256
257    /**
258     * Sets the typeface in which the text should be displayed on the switch.
259     * Note that not all Typeface families actually have bold and italic
260     * variants, so you may need to use
261     * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
262     * that you actually want.
263     *
264     * @attr ref android.R.styleable#TextView_typeface
265     * @attr ref android.R.styleable#TextView_textStyle
266     */
267    public void setSwitchTypeface(Typeface tf) {
268        if (mTextPaint.getTypeface() != tf) {
269            mTextPaint.setTypeface(tf);
270
271            requestLayout();
272            invalidate();
273        }
274    }
275
276    /**
277     * Returns the text displayed when the button is in the checked state.
278     */
279    public CharSequence getTextOn() {
280        return mTextOn;
281    }
282
283    /**
284     * Sets the text displayed when the button is in the checked state.
285     */
286    public void setTextOn(CharSequence textOn) {
287        mTextOn = textOn;
288        requestLayout();
289    }
290
291    /**
292     * Returns the text displayed when the button is not in the checked state.
293     */
294    public CharSequence getTextOff() {
295        return mTextOff;
296    }
297
298    /**
299     * Sets the text displayed when the button is not in the checked state.
300     */
301    public void setTextOff(CharSequence textOff) {
302        mTextOff = textOff;
303        requestLayout();
304    }
305
306    @Override
307    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
308        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
309        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
310        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
311        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
312
313
314        if (mOnLayout == null) {
315            mOnLayout = makeLayout(mTextOn);
316        }
317        if (mOffLayout == null) {
318            mOffLayout = makeLayout(mTextOff);
319        }
320
321        mTrackDrawable.getPadding(mTempRect);
322        final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth());
323        final int switchWidth = Math.max(mSwitchMinWidth,
324                maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
325        final int switchHeight = mTrackDrawable.getIntrinsicHeight();
326
327        mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
328
329        switch (widthMode) {
330            case MeasureSpec.AT_MOST:
331                widthSize = Math.min(widthSize, switchWidth);
332                break;
333
334            case MeasureSpec.UNSPECIFIED:
335                widthSize = switchWidth;
336                break;
337
338            case MeasureSpec.EXACTLY:
339                // Just use what we were given
340                break;
341        }
342
343        switch (heightMode) {
344            case MeasureSpec.AT_MOST:
345                heightSize = Math.min(heightSize, switchHeight);
346                break;
347
348            case MeasureSpec.UNSPECIFIED:
349                heightSize = switchHeight;
350                break;
351
352            case MeasureSpec.EXACTLY:
353                // Just use what we were given
354                break;
355        }
356
357        mSwitchWidth = switchWidth;
358        mSwitchHeight = switchHeight;
359
360        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
361        final int measuredHeight = getMeasuredHeight();
362        if (measuredHeight < switchHeight) {
363            setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
364        }
365    }
366
367    @Override
368    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
369        super.onPopulateAccessibilityEvent(event);
370        if (isChecked()) {
371            CharSequence text = mOnLayout.getText();
372            if (TextUtils.isEmpty(text)) {
373                text = mContext.getString(R.string.switch_on);
374            }
375            event.getText().add(text);
376        } else {
377            CharSequence text = mOffLayout.getText();
378            if (TextUtils.isEmpty(text)) {
379                text = mContext.getString(R.string.switch_off);
380            }
381            event.getText().add(text);
382        }
383    }
384
385    private Layout makeLayout(CharSequence text) {
386        return new StaticLayout(text, mTextPaint,
387                (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)),
388                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
389    }
390
391    /**
392     * @return true if (x, y) is within the target area of the switch thumb
393     */
394    private boolean hitThumb(float x, float y) {
395        mThumbDrawable.getPadding(mTempRect);
396        final int thumbTop = mSwitchTop - mTouchSlop;
397        final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
398        final int thumbRight = thumbLeft + mThumbWidth +
399                mTempRect.left + mTempRect.right + mTouchSlop;
400        final int thumbBottom = mSwitchBottom + mTouchSlop;
401        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
402    }
403
404    @Override
405    public boolean onTouchEvent(MotionEvent ev) {
406        mVelocityTracker.addMovement(ev);
407        final int action = ev.getActionMasked();
408        switch (action) {
409            case MotionEvent.ACTION_DOWN: {
410                final float x = ev.getX();
411                final float y = ev.getY();
412                if (isEnabled() && hitThumb(x, y)) {
413                    mTouchMode = TOUCH_MODE_DOWN;
414                    mTouchX = x;
415                    mTouchY = y;
416                }
417                break;
418            }
419
420            case MotionEvent.ACTION_MOVE: {
421                switch (mTouchMode) {
422                    case TOUCH_MODE_IDLE:
423                        // Didn't target the thumb, treat normally.
424                        break;
425
426                    case TOUCH_MODE_DOWN: {
427                        final float x = ev.getX();
428                        final float y = ev.getY();
429                        if (Math.abs(x - mTouchX) > mTouchSlop ||
430                                Math.abs(y - mTouchY) > mTouchSlop) {
431                            mTouchMode = TOUCH_MODE_DRAGGING;
432                            getParent().requestDisallowInterceptTouchEvent(true);
433                            mTouchX = x;
434                            mTouchY = y;
435                            return true;
436                        }
437                        break;
438                    }
439
440                    case TOUCH_MODE_DRAGGING: {
441                        final float x = ev.getX();
442                        final float dx = x - mTouchX;
443                        float newPos = Math.max(0,
444                                Math.min(mThumbPosition + dx, getThumbScrollRange()));
445                        if (newPos != mThumbPosition) {
446                            mThumbPosition = newPos;
447                            mTouchX = x;
448                            invalidate();
449                        }
450                        return true;
451                    }
452                }
453                break;
454            }
455
456            case MotionEvent.ACTION_UP:
457            case MotionEvent.ACTION_CANCEL: {
458                if (mTouchMode == TOUCH_MODE_DRAGGING) {
459                    stopDrag(ev);
460                    return true;
461                }
462                mTouchMode = TOUCH_MODE_IDLE;
463                mVelocityTracker.clear();
464                break;
465            }
466        }
467
468        return super.onTouchEvent(ev);
469    }
470
471    private void cancelSuperTouch(MotionEvent ev) {
472        MotionEvent cancel = MotionEvent.obtain(ev);
473        cancel.setAction(MotionEvent.ACTION_CANCEL);
474        super.onTouchEvent(cancel);
475        cancel.recycle();
476    }
477
478    /**
479     * Called from onTouchEvent to end a drag operation.
480     *
481     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
482     */
483    private void stopDrag(MotionEvent ev) {
484        mTouchMode = TOUCH_MODE_IDLE;
485        // Up and not canceled, also checks the switch has not been disabled during the drag
486        boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
487
488        cancelSuperTouch(ev);
489
490        if (commitChange) {
491            boolean newState;
492            mVelocityTracker.computeCurrentVelocity(1000);
493            float xvel = mVelocityTracker.getXVelocity();
494            if (Math.abs(xvel) > mMinFlingVelocity) {
495                newState = xvel > 0;
496            } else {
497                newState = getTargetCheckedState();
498            }
499            animateThumbToCheckedState(newState);
500        } else {
501            animateThumbToCheckedState(isChecked());
502        }
503    }
504
505    private void animateThumbToCheckedState(boolean newCheckedState) {
506        // TODO animate!
507        //float targetPos = newCheckedState ? 0 : getThumbScrollRange();
508        //mThumbPosition = targetPos;
509        setChecked(newCheckedState);
510    }
511
512    private boolean getTargetCheckedState() {
513        return mThumbPosition >= getThumbScrollRange() / 2;
514    }
515
516    @Override
517    public void setChecked(boolean checked) {
518        super.setChecked(checked);
519        mThumbPosition = checked ? getThumbScrollRange() : 0;
520        invalidate();
521    }
522
523    @Override
524    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
525        super.onLayout(changed, left, top, right, bottom);
526
527        mThumbPosition = isChecked() ? getThumbScrollRange() : 0;
528
529        int switchRight = getWidth() - getPaddingRight();
530        int switchLeft = switchRight - mSwitchWidth;
531        int switchTop = 0;
532        int switchBottom = 0;
533        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
534            default:
535            case Gravity.TOP:
536                switchTop = getPaddingTop();
537                switchBottom = switchTop + mSwitchHeight;
538                break;
539
540            case Gravity.CENTER_VERTICAL:
541                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
542                        mSwitchHeight / 2;
543                switchBottom = switchTop + mSwitchHeight;
544                break;
545
546            case Gravity.BOTTOM:
547                switchBottom = getHeight() - getPaddingBottom();
548                switchTop = switchBottom - mSwitchHeight;
549                break;
550        }
551
552        mSwitchLeft = switchLeft;
553        mSwitchTop = switchTop;
554        mSwitchBottom = switchBottom;
555        mSwitchRight = switchRight;
556    }
557
558    @Override
559    protected void onDraw(Canvas canvas) {
560        super.onDraw(canvas);
561
562        // Draw the switch
563        int switchLeft = mSwitchLeft;
564        int switchTop = mSwitchTop;
565        int switchRight = mSwitchRight;
566        int switchBottom = mSwitchBottom;
567
568        mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
569        mTrackDrawable.draw(canvas);
570
571        canvas.save();
572
573        mTrackDrawable.getPadding(mTempRect);
574        int switchInnerLeft = switchLeft + mTempRect.left;
575        int switchInnerTop = switchTop + mTempRect.top;
576        int switchInnerRight = switchRight - mTempRect.right;
577        int switchInnerBottom = switchBottom - mTempRect.bottom;
578        canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
579
580        mThumbDrawable.getPadding(mTempRect);
581        final int thumbPos = (int) (mThumbPosition + 0.5f);
582        int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
583        int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
584
585        mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
586        mThumbDrawable.draw(canvas);
587
588        // mTextColors should not be null, but just in case
589        if (mTextColors != null) {
590            mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
591                    mTextColors.getDefaultColor()));
592        }
593        mTextPaint.drawableState = getDrawableState();
594
595        Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
596
597        canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2,
598                (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
599        switchText.draw(canvas);
600
601        canvas.restore();
602    }
603
604    @Override
605    public int getCompoundPaddingRight() {
606        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
607        if (!TextUtils.isEmpty(getText())) {
608            padding += mSwitchPadding;
609        }
610        return padding;
611    }
612
613    private int getThumbScrollRange() {
614        if (mTrackDrawable == null) {
615            return 0;
616        }
617        mTrackDrawable.getPadding(mTempRect);
618        return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
619    }
620
621    @Override
622    protected int[] onCreateDrawableState(int extraSpace) {
623        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
624        if (isChecked()) {
625            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
626        }
627        return drawableState;
628    }
629
630    @Override
631    protected void drawableStateChanged() {
632        super.drawableStateChanged();
633
634        int[] myDrawableState = getDrawableState();
635
636        // Set the state of the Drawable
637        // Drawable may be null when checked state is set from XML, from super constructor
638        if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
639        if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
640
641        invalidate();
642    }
643
644    @Override
645    protected boolean verifyDrawable(Drawable who) {
646        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
647    }
648
649    @Override
650    public void jumpDrawablesToCurrentState() {
651        super.jumpDrawablesToCurrentState();
652        mThumbDrawable.jumpToCurrentState();
653        mTrackDrawable.jumpToCurrentState();
654    }
655
656    @Override
657    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
658        super.onInitializeAccessibilityEvent(event);
659        event.setClassName(Switch.class.getName());
660    }
661
662    @Override
663    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
664        super.onInitializeAccessibilityNodeInfo(info);
665        info.setClassName(Switch.class.getName());
666    }
667}
668