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