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