Switch.java revision 2e6df147d54efb9c076647cb0fa3728322bdd16a
1/*
2 * Copyright (C) 2012 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 com.android.camera.ui;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29import android.text.Layout;
30import android.text.StaticLayout;
31import android.text.TextPaint;
32import android.text.TextUtils;
33import android.util.AttributeSet;
34import android.util.DisplayMetrics;
35import android.util.Log;
36import android.util.TypedValue;
37import android.view.Gravity;
38import android.view.MotionEvent;
39import android.view.VelocityTracker;
40import android.view.ViewConfiguration;
41import android.view.accessibility.AccessibilityEvent;
42import android.view.accessibility.AccessibilityNodeInfo;
43import android.widget.CompoundButton;
44
45import com.android.camera.R;
46import com.android.gallery3d.common.ApiHelper;
47
48import java.util.Arrays;
49
50/**
51 * A Switch is a two-state toggle switch widget that can select between two
52 * options. The user may drag the "thumb" back and forth to choose the selected option,
53 * or simply tap to toggle as if it were a checkbox.
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    private Drawable mThumbDrawable;
61    private Drawable mTrackDrawable;
62    private int mThumbTextPadding;
63    private int mSwitchMinWidth;
64    private int mSwitchPadding;
65    private CharSequence mTextOn;
66    private CharSequence mTextOff;
67
68    private int mTouchMode;
69    private int mTouchSlop;
70    private float mTouchX;
71    private float mTouchY;
72    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
73    private int mMinFlingVelocity;
74
75    private float mThumbPosition;
76    private int mSwitchWidth;
77    private int mSwitchHeight;
78    private int mThumbWidth; // Does not include padding
79
80    private int mSwitchLeft;
81    private int mSwitchTop;
82    private int mSwitchRight;
83    private int mSwitchBottom;
84
85    private TextPaint mTextPaint;
86    private ColorStateList mTextColors;
87    private Layout mOnLayout;
88    private Layout mOffLayout;
89
90    @SuppressWarnings("hiding")
91    private final Rect mTempRect = new Rect();
92
93    private static final int[] CHECKED_STATE_SET = {
94        android.R.attr.state_checked
95    };
96
97    /**
98     * Construct a new Switch with default styling, overriding specific style
99     * attributes as requested.
100     *
101     * @param context The Context that will determine this widget's theming.
102     * @param attrs Specification of attributes that should deviate from default styling.
103     */
104    public Switch(Context context, AttributeSet attrs) {
105        this(context, attrs, R.attr.switchStyle);
106    }
107
108    /**
109     * Construct a new Switch with a default style determined by the given theme attribute,
110     * overriding specific style attributes as requested.
111     *
112     * @param context The Context that will determine this widget's theming.
113     * @param attrs Specification of attributes that should deviate from the default styling.
114     * @param defStyle An attribute ID within the active theme containing a reference to the
115     *                 default style for this widget. e.g. android.R.attr.switchStyle.
116     */
117    public Switch(Context context, AttributeSet attrs, int defStyle) {
118        super(context, attrs, defStyle);
119
120        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
121        Resources res = getResources();
122        DisplayMetrics dm = res.getDisplayMetrics();
123        mTextPaint.density = dm.density;
124        mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark);
125        mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark);
126        mTextOn = res.getString(R.string.capital_on);
127        mTextOff = res.getString(R.string.capital_off);
128        mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding);
129        mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width);
130        mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding);
131        setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small);
132
133        ViewConfiguration config = ViewConfiguration.get(context);
134        mTouchSlop = config.getScaledTouchSlop();
135        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
136
137        // Refresh display with current params
138        refreshDrawableState();
139        setChecked(isChecked());
140    }
141
142    /**
143     * Sets the switch text color, size, style, hint color, and highlight color
144     * from the specified TextAppearance resource.
145     */
146    public void setSwitchTextAppearance(Context context, int resid) {
147        Resources res = getResources();
148        mTextColors = getTextColors();
149        int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size);
150        if (ts != mTextPaint.getTextSize()) {
151            mTextPaint.setTextSize(ts);
152            requestLayout();
153        }
154    }
155
156    @Override
157    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
158        if (mOnLayout == null) {
159            mOnLayout = makeLayout(mTextOn);
160        }
161        if (mOffLayout == null) {
162            mOffLayout = makeLayout(mTextOff);
163        }
164
165        mTrackDrawable.getPadding(mTempRect);
166        final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth());
167        final int switchWidth = Math.max(mSwitchMinWidth,
168                maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
169        final int switchHeight = mTrackDrawable.getIntrinsicHeight();
170
171        mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
172
173        mSwitchWidth = switchWidth;
174        mSwitchHeight = switchHeight;
175
176        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
177        final int measuredHeight = getMeasuredHeight();
178        if (measuredHeight < switchHeight) {
179            setMeasuredDimension(getMeasuredWidth(), switchHeight);
180        }
181    }
182
183    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
184    @Override
185    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
186        super.onPopulateAccessibilityEvent(event);
187        CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
188        if (!TextUtils.isEmpty(text)) {
189            event.getText().add(text);
190        }
191    }
192
193    private Layout makeLayout(CharSequence text) {
194        return new StaticLayout(text, mTextPaint,
195                (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)),
196                Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
197    }
198
199    /**
200     * @return true if (x, y) is within the target area of the switch thumb
201     */
202    private boolean hitThumb(float x, float y) {
203        mThumbDrawable.getPadding(mTempRect);
204        final int thumbTop = mSwitchTop - mTouchSlop;
205        final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
206        final int thumbRight = thumbLeft + mThumbWidth +
207                mTempRect.left + mTempRect.right + mTouchSlop;
208        final int thumbBottom = mSwitchBottom + mTouchSlop;
209        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
210    }
211
212    @Override
213    public boolean onTouchEvent(MotionEvent ev) {
214        mVelocityTracker.addMovement(ev);
215        final int action = ev.getActionMasked();
216        switch (action) {
217            case MotionEvent.ACTION_DOWN: {
218                final float x = ev.getX();
219                final float y = ev.getY();
220                if (isEnabled() && hitThumb(x, y)) {
221                    mTouchMode = TOUCH_MODE_DOWN;
222                    mTouchX = x;
223                    mTouchY = y;
224                }
225                break;
226            }
227
228            case MotionEvent.ACTION_MOVE: {
229                switch (mTouchMode) {
230                    case TOUCH_MODE_IDLE:
231                        // Didn't target the thumb, treat normally.
232                        break;
233
234                    case TOUCH_MODE_DOWN: {
235                        final float x = ev.getX();
236                        final float y = ev.getY();
237                        if (Math.abs(x - mTouchX) > mTouchSlop ||
238                                Math.abs(y - mTouchY) > mTouchSlop) {
239                            mTouchMode = TOUCH_MODE_DRAGGING;
240                            getParent().requestDisallowInterceptTouchEvent(true);
241                            mTouchX = x;
242                            mTouchY = y;
243                            return true;
244                        }
245                        break;
246                    }
247
248                    case TOUCH_MODE_DRAGGING: {
249                        final float x = ev.getX();
250                        final float dx = x - mTouchX;
251                        float newPos = Math.max(0,
252                                Math.min(mThumbPosition + dx, getThumbScrollRange()));
253                        if (newPos != mThumbPosition) {
254                            mThumbPosition = newPos;
255                            mTouchX = x;
256                            invalidate();
257                        }
258                        return true;
259                    }
260                }
261                break;
262            }
263
264            case MotionEvent.ACTION_UP:
265            case MotionEvent.ACTION_CANCEL: {
266                if (mTouchMode == TOUCH_MODE_DRAGGING) {
267                    stopDrag(ev);
268                    return true;
269                }
270                mTouchMode = TOUCH_MODE_IDLE;
271                mVelocityTracker.clear();
272                break;
273            }
274        }
275
276        return super.onTouchEvent(ev);
277    }
278
279    private void cancelSuperTouch(MotionEvent ev) {
280        MotionEvent cancel = MotionEvent.obtain(ev);
281        cancel.setAction(MotionEvent.ACTION_CANCEL);
282        super.onTouchEvent(cancel);
283        cancel.recycle();
284    }
285
286    /**
287     * Called from onTouchEvent to end a drag operation.
288     *
289     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
290     */
291    private void stopDrag(MotionEvent ev) {
292        mTouchMode = TOUCH_MODE_IDLE;
293        // Up and not canceled, also checks the switch has not been disabled during the drag
294        boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
295
296        cancelSuperTouch(ev);
297
298        if (commitChange) {
299            boolean newState;
300            mVelocityTracker.computeCurrentVelocity(1000);
301            float xvel = mVelocityTracker.getXVelocity();
302            if (Math.abs(xvel) > mMinFlingVelocity) {
303                newState = xvel > 0;
304            } else {
305                newState = getTargetCheckedState();
306            }
307            animateThumbToCheckedState(newState);
308        } else {
309            animateThumbToCheckedState(isChecked());
310        }
311    }
312
313    private void animateThumbToCheckedState(boolean newCheckedState) {
314        setChecked(newCheckedState);
315    }
316
317    private boolean getTargetCheckedState() {
318        return mThumbPosition >= getThumbScrollRange() / 2;
319    }
320
321    private void setThumbPosition(boolean checked) {
322        mThumbPosition = checked ? getThumbScrollRange() : 0;
323    }
324
325    @Override
326    public void setChecked(boolean checked) {
327        super.setChecked(checked);
328        setThumbPosition(checked);
329        invalidate();
330    }
331
332    @Override
333    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
334        super.onLayout(changed, left, top, right, bottom);
335
336        setThumbPosition(isChecked());
337
338        int switchRight;
339        int switchLeft;
340
341        switchRight = getWidth() - getPaddingRight();
342        switchLeft = switchRight - mSwitchWidth;
343
344        int switchTop = 0;
345        int switchBottom = 0;
346        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
347            default:
348            case Gravity.TOP:
349                switchTop = getPaddingTop();
350                switchBottom = switchTop + mSwitchHeight;
351                break;
352
353            case Gravity.CENTER_VERTICAL:
354                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
355                        mSwitchHeight / 2;
356                switchBottom = switchTop + mSwitchHeight;
357                break;
358
359            case Gravity.BOTTOM:
360                switchBottom = getHeight() - getPaddingBottom();
361                switchTop = switchBottom - mSwitchHeight;
362                break;
363        }
364
365        mSwitchLeft = switchLeft;
366        mSwitchTop = switchTop;
367        mSwitchBottom = switchBottom;
368        mSwitchRight = switchRight;
369    }
370
371    @Override
372    protected void onDraw(Canvas canvas) {
373        super.onDraw(canvas);
374
375        // Draw the switch
376        int switchLeft = mSwitchLeft;
377        int switchTop = mSwitchTop;
378        int switchRight = mSwitchRight;
379        int switchBottom = mSwitchBottom;
380
381        mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
382        mTrackDrawable.draw(canvas);
383
384        canvas.save();
385
386        mTrackDrawable.getPadding(mTempRect);
387        int switchInnerLeft = switchLeft + mTempRect.left;
388        int switchInnerTop = switchTop + mTempRect.top;
389        int switchInnerRight = switchRight - mTempRect.right;
390        int switchInnerBottom = switchBottom - mTempRect.bottom;
391        canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
392
393        mThumbDrawable.getPadding(mTempRect);
394        final int thumbPos = (int) (mThumbPosition + 0.5f);
395        int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
396        int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
397
398        mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
399        mThumbDrawable.draw(canvas);
400
401        // mTextColors should not be null, but just in case
402        if (mTextColors != null) {
403            mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
404                    mTextColors.getDefaultColor()));
405        }
406        mTextPaint.drawableState = getDrawableState();
407
408        Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
409
410        canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2,
411                (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
412        switchText.draw(canvas);
413
414        canvas.restore();
415    }
416
417    @Override
418    public int getCompoundPaddingRight() {
419        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
420        if (!TextUtils.isEmpty(getText())) {
421            padding += mSwitchPadding;
422        }
423        return padding;
424    }
425
426    private int getThumbScrollRange() {
427        if (mTrackDrawable == null) {
428            return 0;
429        }
430        mTrackDrawable.getPadding(mTempRect);
431        return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
432    }
433
434    @Override
435    protected int[] onCreateDrawableState(int extraSpace) {
436        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
437
438        if (isChecked()) {
439            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
440        }
441        return drawableState;
442    }
443
444    @Override
445    protected void drawableStateChanged() {
446        super.drawableStateChanged();
447
448        int[] myDrawableState = getDrawableState();
449
450        // Set the state of the Drawable
451        // Drawable may be null when checked state is set from XML, from super constructor
452        if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
453        if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
454
455        invalidate();
456    }
457
458    @Override
459    protected boolean verifyDrawable(Drawable who) {
460        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
461    }
462
463    @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
464    @Override
465    public void jumpDrawablesToCurrentState() {
466        super.jumpDrawablesToCurrentState();
467        mThumbDrawable.jumpToCurrentState();
468        mTrackDrawable.jumpToCurrentState();
469    }
470
471    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
472    @Override
473    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
474        super.onInitializeAccessibilityEvent(event);
475        event.setClassName(Switch.class.getName());
476    }
477
478    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
479    @Override
480    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
481        super.onInitializeAccessibilityNodeInfo(info);
482        info.setClassName(Switch.class.getName());
483        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
484        if (!TextUtils.isEmpty(switchText)) {
485            CharSequence oldText = info.getText();
486            if (TextUtils.isEmpty(oldText)) {
487                info.setText(switchText);
488            } else {
489                StringBuilder newText = new StringBuilder();
490                newText.append(oldText).append(' ').append(switchText);
491                info.setText(newText);
492            }
493        }
494    }
495}
496