2 * Copyright (C) 2012 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.camera.ui;
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.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.Typeface;
28 import android.graphics.drawable.Drawable;
29 import android.text.Layout;
30 import android.text.StaticLayout;
31 import android.text.TextPaint;
32 import android.text.TextUtils;
33 import android.util.AttributeSet;
34 import android.util.DisplayMetrics;
35 import android.util.Log;
36 import android.util.TypedValue;
37 import android.view.Gravity;
38 import android.view.MotionEvent;
39 import android.view.VelocityTracker;
40 import android.view.ViewConfiguration;
41 import android.view.accessibility.AccessibilityEvent;
42 import android.view.accessibility.AccessibilityNodeInfo;
43 import android.widget.CompoundButton;
45 import com.android.gallery3d.R;
46 import com.android.gallery3d.common.ApiHelper;
48 import java.util.Arrays;
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.
55 public 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;
60 private Drawable mThumbDrawable;
61 private Drawable mTrackDrawable;
62 private int mThumbTextPadding;
63 private int mSwitchMinWidth;
64 private int mSwitchTextMaxWidth;
65 private int mSwitchPadding;
66 private CharSequence mTextOn;
67 private CharSequence mTextOff;
69 private int mTouchMode;
70 private int mTouchSlop;
71 private float mTouchX;
72 private float mTouchY;
73 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
74 private int mMinFlingVelocity;
76 private float mThumbPosition;
77 private int mSwitchWidth;
78 private int mSwitchHeight;
79 private int mThumbWidth; // Does not include padding
81 private int mSwitchLeft;
82 private int mSwitchTop;
83 private int mSwitchRight;
84 private int mSwitchBottom;
86 private TextPaint mTextPaint;
87 private ColorStateList mTextColors;
88 private Layout mOnLayout;
89 private Layout mOffLayout;
91 @SuppressWarnings("hiding")
92 private final Rect mTempRect = new Rect();
94 private static final int[] CHECKED_STATE_SET = {
95 android.R.attr.state_checked
99 * Construct a new Switch with default styling, overriding specific style
100 * attributes as requested.
102 * @param context The Context that will determine this widget's theming.
103 * @param attrs Specification of attributes that should deviate from default styling.
105 public Switch(Context context, AttributeSet attrs) {
106 this(context, attrs, R.attr.switchStyle);
110 * Construct a new Switch with a default style determined by the given theme attribute,
111 * overriding specific style attributes as requested.
113 * @param context The Context that will determine this widget's theming.
114 * @param attrs Specification of attributes that should deviate from the default styling.
115 * @param defStyle An attribute ID within the active theme containing a reference to the
116 * default style for this widget. e.g. android.R.attr.switchStyle.
118 public Switch(Context context, AttributeSet attrs, int defStyle) {
119 super(context, attrs, defStyle);
121 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
122 Resources res = getResources();
123 DisplayMetrics dm = res.getDisplayMetrics();
124 mTextPaint.density = dm.density;
125 mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark);
126 mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark);
127 mTextOn = res.getString(R.string.capital_on);
128 mTextOff = res.getString(R.string.capital_off);
129 mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding);
130 mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width);
131 mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width);
132 mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding);
133 setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small);
135 ViewConfiguration config = ViewConfiguration.get(context);
136 mTouchSlop = config.getScaledTouchSlop();
137 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
139 // Refresh display with current params
140 refreshDrawableState();
141 setChecked(isChecked());
145 * Sets the switch text color, size, style, hint color, and highlight color
146 * from the specified TextAppearance resource.
148 public void setSwitchTextAppearance(Context context, int resid) {
149 Resources res = getResources();
150 mTextColors = getTextColors();
151 int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size);
152 if (ts != mTextPaint.getTextSize()) {
153 mTextPaint.setTextSize(ts);
159 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
160 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
161 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
162 if (mOnLayout == null) {
163 mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth);
165 if (mOffLayout == null) {
166 mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth);
169 mTrackDrawable.getPadding(mTempRect);
170 final int maxTextWidth = Math.min(mSwitchTextMaxWidth,
171 Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()));
172 final int switchWidth = Math.max(mSwitchMinWidth,
173 maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
174 final int switchHeight = mTrackDrawable.getIntrinsicHeight();
176 mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
178 mSwitchWidth = switchWidth;
179 mSwitchHeight = switchHeight;
181 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
182 final int measuredHeight = getMeasuredHeight();
183 final int measuredWidth = getMeasuredWidth();
184 if (measuredHeight < switchHeight) {
185 setMeasuredDimension(measuredWidth, switchHeight);
189 @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
191 public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
192 super.onPopulateAccessibilityEvent(event);
193 CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
194 if (!TextUtils.isEmpty(text)) {
195 event.getText().add(text);
199 private Layout makeLayout(CharSequence text, int maxWidth) {
200 int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint));
201 StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint,
203 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true,
204 TextUtils.TruncateAt.END,
205 (int) Math.min(actual_width, maxWidth));
210 * @return true if (x, y) is within the target area of the switch thumb
212 private boolean hitThumb(float x, float y) {
213 mThumbDrawable.getPadding(mTempRect);
214 final int thumbTop = mSwitchTop - mTouchSlop;
215 final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
216 final int thumbRight = thumbLeft + mThumbWidth +
217 mTempRect.left + mTempRect.right + mTouchSlop;
218 final int thumbBottom = mSwitchBottom + mTouchSlop;
219 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
223 public boolean onTouchEvent(MotionEvent ev) {
224 mVelocityTracker.addMovement(ev);
225 final int action = ev.getActionMasked();
227 case MotionEvent.ACTION_DOWN: {
228 final float x = ev.getX();
229 final float y = ev.getY();
230 if (isEnabled() && hitThumb(x, y)) {
231 mTouchMode = TOUCH_MODE_DOWN;
238 case MotionEvent.ACTION_MOVE: {
239 switch (mTouchMode) {
240 case TOUCH_MODE_IDLE:
241 // Didn't target the thumb, treat normally.
244 case TOUCH_MODE_DOWN: {
245 final float x = ev.getX();
246 final float y = ev.getY();
247 if (Math.abs(x - mTouchX) > mTouchSlop ||
248 Math.abs(y - mTouchY) > mTouchSlop) {
249 mTouchMode = TOUCH_MODE_DRAGGING;
250 getParent().requestDisallowInterceptTouchEvent(true);
258 case TOUCH_MODE_DRAGGING: {
259 final float x = ev.getX();
260 final float dx = x - mTouchX;
261 float newPos = Math.max(0,
262 Math.min(mThumbPosition + dx, getThumbScrollRange()));
263 if (newPos != mThumbPosition) {
264 mThumbPosition = newPos;
274 case MotionEvent.ACTION_UP:
275 case MotionEvent.ACTION_CANCEL: {
276 if (mTouchMode == TOUCH_MODE_DRAGGING) {
280 mTouchMode = TOUCH_MODE_IDLE;
281 mVelocityTracker.clear();
286 return super.onTouchEvent(ev);
289 private void cancelSuperTouch(MotionEvent ev) {
290 MotionEvent cancel = MotionEvent.obtain(ev);
291 cancel.setAction(MotionEvent.ACTION_CANCEL);
292 super.onTouchEvent(cancel);
297 * Called from onTouchEvent to end a drag operation.
299 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
301 private void stopDrag(MotionEvent ev) {
302 mTouchMode = TOUCH_MODE_IDLE;
303 // Up and not canceled, also checks the switch has not been disabled during the drag
304 boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
306 cancelSuperTouch(ev);
310 mVelocityTracker.computeCurrentVelocity(1000);
311 float xvel = mVelocityTracker.getXVelocity();
312 if (Math.abs(xvel) > mMinFlingVelocity) {
315 newState = getTargetCheckedState();
317 animateThumbToCheckedState(newState);
319 animateThumbToCheckedState(isChecked());
323 private void animateThumbToCheckedState(boolean newCheckedState) {
324 setChecked(newCheckedState);
327 private boolean getTargetCheckedState() {
328 return mThumbPosition >= getThumbScrollRange() / 2;
331 private void setThumbPosition(boolean checked) {
332 mThumbPosition = checked ? getThumbScrollRange() : 0;
336 public void setChecked(boolean checked) {
337 super.setChecked(checked);
338 setThumbPosition(checked);
343 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
344 super.onLayout(changed, left, top, right, bottom);
346 setThumbPosition(isChecked());
351 switchRight = getWidth() - getPaddingRight();
352 switchLeft = switchRight - mSwitchWidth;
355 int switchBottom = 0;
356 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
359 switchTop = getPaddingTop();
360 switchBottom = switchTop + mSwitchHeight;
363 case Gravity.CENTER_VERTICAL:
364 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
366 switchBottom = switchTop + mSwitchHeight;
370 switchBottom = getHeight() - getPaddingBottom();
371 switchTop = switchBottom - mSwitchHeight;
375 mSwitchLeft = switchLeft;
376 mSwitchTop = switchTop;
377 mSwitchBottom = switchBottom;
378 mSwitchRight = switchRight;
382 protected void onDraw(Canvas canvas) {
383 super.onDraw(canvas);
386 int switchLeft = mSwitchLeft;
387 int switchTop = mSwitchTop;
388 int switchRight = mSwitchRight;
389 int switchBottom = mSwitchBottom;
391 mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
392 mTrackDrawable.draw(canvas);
396 mTrackDrawable.getPadding(mTempRect);
397 int switchInnerLeft = switchLeft + mTempRect.left;
398 int switchInnerTop = switchTop + mTempRect.top;
399 int switchInnerRight = switchRight - mTempRect.right;
400 int switchInnerBottom = switchBottom - mTempRect.bottom;
401 canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
403 mThumbDrawable.getPadding(mTempRect);
404 final int thumbPos = (int) (mThumbPosition + 0.5f);
405 int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
406 int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
408 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
409 mThumbDrawable.draw(canvas);
411 // mTextColors should not be null, but just in case
412 if (mTextColors != null) {
413 mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
414 mTextColors.getDefaultColor()));
416 mTextPaint.drawableState = getDrawableState();
418 Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
420 canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2,
421 (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
422 switchText.draw(canvas);
428 public int getCompoundPaddingRight() {
429 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
430 if (!TextUtils.isEmpty(getText())) {
431 padding += mSwitchPadding;
436 private int getThumbScrollRange() {
437 if (mTrackDrawable == null) {
440 mTrackDrawable.getPadding(mTempRect);
441 return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
445 protected int[] onCreateDrawableState(int extraSpace) {
446 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
449 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
451 return drawableState;
455 protected void drawableStateChanged() {
456 super.drawableStateChanged();
458 int[] myDrawableState = getDrawableState();
460 // Set the state of the Drawable
461 // Drawable may be null when checked state is set from XML, from super constructor
462 if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
463 if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
469 protected boolean verifyDrawable(Drawable who) {
470 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
473 @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
475 public void jumpDrawablesToCurrentState() {
476 super.jumpDrawablesToCurrentState();
477 mThumbDrawable.jumpToCurrentState();
478 mTrackDrawable.jumpToCurrentState();
481 @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
483 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
484 super.onInitializeAccessibilityEvent(event);
485 event.setClassName(Switch.class.getName());
488 @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
490 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
491 super.onInitializeAccessibilityNodeInfo(info);
492 info.setClassName(Switch.class.getName());
493 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
494 if (!TextUtils.isEmpty(switchText)) {
495 CharSequence oldText = info.getText();
496 if (TextUtils.isEmpty(oldText)) {
497 info.setText(switchText);
499 StringBuilder newText = new StringBuilder();
500 newText.append(oldText).append(' ').append(switchText);
501 info.setText(newText);