blob: 79dc6708f9c3558e11f7a741f4ab33ca231a52ec [file] [log] [blame]
Adam Powell12190b32010-11-28 19:07:53 -08001/*
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
Alan Viverettecc2688d2013-09-17 17:00:12 -070019import android.animation.ObjectAnimator;
Tor Norbye7b9c9122013-05-30 16:48:33 -070020import android.annotation.DrawableRes;
Alan Viverettef6d87ec2016-03-11 10:09:14 -050021import android.annotation.NonNull;
Alan Viverettee7eee642015-01-29 14:27:49 -080022import android.annotation.Nullable;
Tor Norbye7b9c9122013-05-30 16:48:33 -070023import android.annotation.StyleRes;
Mathew Inwood978c6e22018-08-21 15:58:55 +010024import android.annotation.UnsupportedAppUsage;
Adam Powell12190b32010-11-28 19:07:53 -080025import android.content.Context;
26import android.content.res.ColorStateList;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.graphics.Canvas;
Alan Viverette661e6362014-05-12 10:55:37 -070030import android.graphics.Insets;
Chris Craik6a49dde2015-05-12 10:28:14 -070031import android.graphics.Paint;
Alan Viverettee7eee642015-01-29 14:27:49 -080032import android.graphics.PorterDuff;
Adam Powell12190b32010-11-28 19:07:53 -080033import android.graphics.Rect;
Alan Viverette661e6362014-05-12 10:55:37 -070034import android.graphics.Region.Op;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070035import android.graphics.Typeface;
Adam Powell12190b32010-11-28 19:07:53 -080036import android.graphics.drawable.Drawable;
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -070037import android.os.Build.VERSION_CODES;
Adam Powell12190b32010-11-28 19:07:53 -080038import android.text.Layout;
39import android.text.StaticLayout;
40import android.text.TextPaint;
41import android.text.TextUtils;
Daniel Sandler4c3308d2012-04-19 11:04:39 -040042import android.text.method.AllCapsTransformationMethod;
43import android.text.method.TransformationMethod2;
Adam Powell12190b32010-11-28 19:07:53 -080044import android.util.AttributeSet;
Alan Viverettecc2688d2013-09-17 17:00:12 -070045import android.util.FloatProperty;
46import android.util.MathUtils;
Adam Powell12190b32010-11-28 19:07:53 -080047import android.view.Gravity;
48import android.view.MotionEvent;
Alan Viveretted4e77902014-10-27 17:50:51 -070049import android.view.SoundEffectConstants;
Adam Powell12190b32010-11-28 19:07:53 -080050import android.view.VelocityTracker;
51import android.view.ViewConfiguration;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070052import android.view.ViewStructure;
Svetoslav Ganov63bce032011-07-23 19:52:17 -070053import android.view.accessibility.AccessibilityEvent;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080054import android.view.accessibility.AccessibilityNodeInfo;
Adam Powell12190b32010-11-28 19:07:53 -080055
Adam Powellbe0a4532010-11-29 17:47:48 -080056import com.android.internal.R;
57
Adam Powell12190b32010-11-28 19:07:53 -080058/**
59 * A Switch is a two-state toggle switch widget that can select between two
60 * options. The user may drag the "thumb" back and forth to choose the selected option,
Chet Haase150176d2011-08-26 09:54:06 -070061 * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
62 * property controls the text displayed in the label for the switch, whereas the
63 * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
64 * controls the text on the thumb. Similarly, the
65 * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
66 * setTypeface() methods control the typeface and style of label text, whereas the
67 * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
Andrew Solovay07e70212015-07-08 12:49:21 -070068 * the related setSwitchTypeface() methods control that of the thumb.
Adam Powell12190b32010-11-28 19:07:53 -080069 *
Mark Lu34040322016-08-30 18:12:30 -070070 * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
71 * the Switch widget which runs on devices back to API 7.</p>
72 *
Scott Main4c359b72012-07-24 15:51:27 -070073 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
74 * guide.</p>
75 *
76 * @attr ref android.R.styleable#Switch_textOn
77 * @attr ref android.R.styleable#Switch_textOff
78 * @attr ref android.R.styleable#Switch_switchMinWidth
79 * @attr ref android.R.styleable#Switch_switchPadding
80 * @attr ref android.R.styleable#Switch_switchTextAppearance
81 * @attr ref android.R.styleable#Switch_thumb
82 * @attr ref android.R.styleable#Switch_thumbTextPadding
83 * @attr ref android.R.styleable#Switch_track
Adam Powell12190b32010-11-28 19:07:53 -080084 */
85public class Switch extends CompoundButton {
Alan Viverettecc2688d2013-09-17 17:00:12 -070086 private static final int THUMB_ANIMATION_DURATION = 250;
87
Adam Powell12190b32010-11-28 19:07:53 -080088 private static final int TOUCH_MODE_IDLE = 0;
89 private static final int TOUCH_MODE_DOWN = 1;
90 private static final int TOUCH_MODE_DRAGGING = 2;
91
92 // Enum for the "typeface" XML parameter.
93 private static final int SANS = 1;
94 private static final int SERIF = 2;
95 private static final int MONOSPACE = 3;
96
Mathew Inwood978c6e22018-08-21 15:58:55 +010097 @UnsupportedAppUsage
Adam Powell12190b32010-11-28 19:07:53 -080098 private Drawable mThumbDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -080099 private ColorStateList mThumbTintList = null;
100 private PorterDuff.Mode mThumbTintMode = null;
101 private boolean mHasThumbTint = false;
102 private boolean mHasThumbTintMode = false;
103
Mathew Inwood978c6e22018-08-21 15:58:55 +0100104 @UnsupportedAppUsage
Adam Powell12190b32010-11-28 19:07:53 -0800105 private Drawable mTrackDrawable;
Alan Viverettee7eee642015-01-29 14:27:49 -0800106 private ColorStateList mTrackTintList = null;
107 private PorterDuff.Mode mTrackTintMode = null;
108 private boolean mHasTrackTint = false;
109 private boolean mHasTrackTintMode = false;
110
Adam Powell12190b32010-11-28 19:07:53 -0800111 private int mThumbTextPadding;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100112 @UnsupportedAppUsage
Adam Powell12190b32010-11-28 19:07:53 -0800113 private int mSwitchMinWidth;
114 private int mSwitchPadding;
Alan Viverette661e6362014-05-12 10:55:37 -0700115 private boolean mSplitTrack;
Adam Powell12190b32010-11-28 19:07:53 -0800116 private CharSequence mTextOn;
117 private CharSequence mTextOff;
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700118 private boolean mShowText;
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700119 private boolean mUseFallbackLineSpacing;
Adam Powell12190b32010-11-28 19:07:53 -0800120
121 private int mTouchMode;
122 private int mTouchSlop;
123 private float mTouchX;
124 private float mTouchY;
125 private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
126 private int mMinFlingVelocity;
127
128 private float mThumbPosition;
Alan Viverette8bb39902014-07-29 17:22:30 -0700129
Alan Viverette0c0dde72014-07-30 13:29:39 -0700130 /**
131 * Width required to draw the switch track and thumb. Includes padding and
132 * optical bounds for both the track and thumb.
133 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100134 @UnsupportedAppUsage
Alan Viverette0c0dde72014-07-30 13:29:39 -0700135 private int mSwitchWidth;
136
137 /**
138 * Height required to draw the switch track and thumb. Includes padding and
139 * optical bounds for both the track and thumb.
140 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100141 @UnsupportedAppUsage
Alan Viverette0c0dde72014-07-30 13:29:39 -0700142 private int mSwitchHeight;
143
144 /**
145 * Width of the thumb's content region. Does not include padding or
146 * optical bounds.
147 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100148 @UnsupportedAppUsage
Alan Viverette0c0dde72014-07-30 13:29:39 -0700149 private int mThumbWidth;
150
151 /** Left bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800152 private int mSwitchLeft;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700153
154 /** Top bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800155 private int mSwitchTop;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700156
157 /** Right bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800158 private int mSwitchRight;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700159
160 /** Bottom bound for drawing the switch track and thumb. */
Adam Powell12190b32010-11-28 19:07:53 -0800161 private int mSwitchBottom;
162
163 private TextPaint mTextPaint;
164 private ColorStateList mTextColors;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100165 @UnsupportedAppUsage
Adam Powell12190b32010-11-28 19:07:53 -0800166 private Layout mOnLayout;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100167 @UnsupportedAppUsage
Adam Powell12190b32010-11-28 19:07:53 -0800168 private Layout mOffLayout;
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400169 private TransformationMethod2 mSwitchTransformationMethod;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700170 private ObjectAnimator mPositionAnimator;
Adam Powell12190b32010-11-28 19:07:53 -0800171
Adam Powellbe0a4532010-11-29 17:47:48 -0800172 @SuppressWarnings("hiding")
Adam Powell12190b32010-11-28 19:07:53 -0800173 private final Rect mTempRect = new Rect();
174
175 private static final int[] CHECKED_STATE_SET = {
176 R.attr.state_checked
177 };
178
179 /**
180 * Construct a new Switch with default styling.
181 *
182 * @param context The Context that will determine this widget's theming.
183 */
184 public Switch(Context context) {
185 this(context, null);
186 }
187
188 /**
189 * Construct a new Switch with default styling, overriding specific style
190 * attributes as requested.
191 *
192 * @param context The Context that will determine this widget's theming.
193 * @param attrs Specification of attributes that should deviate from default styling.
194 */
195 public Switch(Context context, AttributeSet attrs) {
196 this(context, attrs, com.android.internal.R.attr.switchStyle);
197 }
198
199 /**
200 * Construct a new Switch with a default style determined by the given theme attribute,
201 * overriding specific style attributes as requested.
202 *
203 * @param context The Context that will determine this widget's theming.
204 * @param attrs Specification of attributes that should deviate from the default styling.
Alan Viverette617feb92013-09-09 18:09:13 -0700205 * @param defStyleAttr An attribute in the current theme that contains a
206 * reference to a style resource that supplies default values for
207 * the view. Can be 0 to not look for defaults.
Adam Powell12190b32010-11-28 19:07:53 -0800208 */
Alan Viverette617feb92013-09-09 18:09:13 -0700209 public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
210 this(context, attrs, defStyleAttr, 0);
211 }
212
213
214 /**
215 * Construct a new Switch with a default style determined by the given theme
216 * attribute or style resource, overriding specific style attributes as
217 * requested.
218 *
219 * @param context The Context that will determine this widget's theming.
220 * @param attrs Specification of attributes that should deviate from the
221 * default styling.
222 * @param defStyleAttr An attribute in the current theme that contains a
223 * reference to a style resource that supplies default values for
224 * the view. Can be 0 to not look for defaults.
225 * @param defStyleRes A resource identifier of a style resource that
226 * supplies default values for the view, used only if
227 * defStyleAttr is 0 or can not be found in the theme. Can be 0
228 * to not look for defaults.
229 */
230 public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
231 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell12190b32010-11-28 19:07:53 -0800232
Chris Craik6a49dde2015-05-12 10:28:14 -0700233 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
Alan Viverette661e6362014-05-12 10:55:37 -0700234
235 final Resources res = getResources();
Adam Powell12190b32010-11-28 19:07:53 -0800236 mTextPaint.density = res.getDisplayMetrics().density;
237 mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
238
Alan Viverette617feb92013-09-09 18:09:13 -0700239 final TypedArray a = context.obtainStyledAttributes(
240 attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
Chet Haase150176d2011-08-26 09:54:06 -0700241 mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
Alan Viveretteb0674052014-09-26 16:12:16 -0700242 if (mThumbDrawable != null) {
243 mThumbDrawable.setCallback(this);
244 }
Chet Haase150176d2011-08-26 09:54:06 -0700245 mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
Alan Viveretteb0674052014-09-26 16:12:16 -0700246 if (mTrackDrawable != null) {
247 mTrackDrawable.setCallback(this);
248 }
Adam Powell12190b32010-11-28 19:07:53 -0800249 mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
250 mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700251 mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
Adam Powell12190b32010-11-28 19:07:53 -0800252 mThumbTextPadding = a.getDimensionPixelSize(
253 com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
254 mSwitchMinWidth = a.getDimensionPixelSize(
255 com.android.internal.R.styleable.Switch_switchMinWidth, 0);
256 mSwitchPadding = a.getDimensionPixelSize(
257 com.android.internal.R.styleable.Switch_switchPadding, 0);
Alan Viverette661e6362014-05-12 10:55:37 -0700258 mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
Adam Powell12190b32010-11-28 19:07:53 -0800259
Roozbeh Pournader60f555c2017-09-21 12:54:55 -0700260 mUseFallbackLineSpacing = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.P;
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700261
Jun Mukai9fb302c2015-06-24 18:31:06 -0700262 ColorStateList thumbTintList = a.getColorStateList(
263 com.android.internal.R.styleable.Switch_thumbTint);
264 if (thumbTintList != null) {
265 mThumbTintList = thumbTintList;
266 mHasThumbTint = true;
267 }
268 PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
269 a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
270 if (mThumbTintMode != thumbTintMode) {
271 mThumbTintMode = thumbTintMode;
272 mHasThumbTintMode = true;
273 }
274 if (mHasThumbTint || mHasThumbTintMode) {
275 applyThumbTint();
276 }
277
278 ColorStateList trackTintList = a.getColorStateList(
279 com.android.internal.R.styleable.Switch_trackTint);
280 if (trackTintList != null) {
281 mTrackTintList = trackTintList;
282 mHasTrackTint = true;
283 }
284 PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
285 a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
286 if (mTrackTintMode != trackTintMode) {
287 mTrackTintMode = trackTintMode;
288 mHasTrackTintMode = true;
289 }
290 if (mHasTrackTint || mHasTrackTintMode) {
291 applyTrackTint();
292 }
293
Alan Viverette661e6362014-05-12 10:55:37 -0700294 final int appearance = a.getResourceId(
Adam Powell12190b32010-11-28 19:07:53 -0800295 com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
296 if (appearance != 0) {
Chet Haase150176d2011-08-26 09:54:06 -0700297 setSwitchTextAppearance(context, appearance);
Adam Powell12190b32010-11-28 19:07:53 -0800298 }
299 a.recycle();
300
Alan Viverette661e6362014-05-12 10:55:37 -0700301 final ViewConfiguration config = ViewConfiguration.get(context);
Adam Powell12190b32010-11-28 19:07:53 -0800302 mTouchSlop = config.getScaledTouchSlop();
303 mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
304
305 // Refresh display with current params
Gilles Debunnee724ee42011-08-31 11:20:27 -0700306 refreshDrawableState();
Adam Powell12190b32010-11-28 19:07:53 -0800307 setChecked(isChecked());
308 }
309
310 /**
311 * Sets the switch text color, size, style, hint color, and highlight color
312 * from the specified TextAppearance resource.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800313 *
314 * @attr ref android.R.styleable#Switch_switchTextAppearance
Adam Powell12190b32010-11-28 19:07:53 -0800315 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700316 public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
Adam Powell12190b32010-11-28 19:07:53 -0800317 TypedArray appearance =
Chet Haase150176d2011-08-26 09:54:06 -0700318 context.obtainStyledAttributes(resid,
Adam Powell12190b32010-11-28 19:07:53 -0800319 com.android.internal.R.styleable.TextAppearance);
320
321 ColorStateList colors;
322 int ts;
323
324 colors = appearance.getColorStateList(com.android.internal.R.styleable.
325 TextAppearance_textColor);
326 if (colors != null) {
327 mTextColors = colors;
Chet Haase150176d2011-08-26 09:54:06 -0700328 } else {
329 // If no color set in TextAppearance, default to the view's textColor
330 mTextColors = getTextColors();
Adam Powell12190b32010-11-28 19:07:53 -0800331 }
332
333 ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
334 TextAppearance_textSize, 0);
335 if (ts != 0) {
336 if (ts != mTextPaint.getTextSize()) {
337 mTextPaint.setTextSize(ts);
338 requestLayout();
339 }
340 }
341
342 int typefaceIndex, styleIndex;
343
344 typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
345 TextAppearance_typeface, -1);
346 styleIndex = appearance.getInt(com.android.internal.R.styleable.
347 TextAppearance_textStyle, -1);
348
349 setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
350
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400351 boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
352 TextAppearance_textAllCaps, false);
353 if (allCaps) {
354 mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
355 mSwitchTransformationMethod.setLengthChangesAllowed(true);
356 } else {
357 mSwitchTransformationMethod = null;
358 }
359
Adam Powell12190b32010-11-28 19:07:53 -0800360 appearance.recycle();
361 }
362
363 private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
364 Typeface tf = null;
365 switch (typefaceIndex) {
366 case SANS:
367 tf = Typeface.SANS_SERIF;
368 break;
369
370 case SERIF:
371 tf = Typeface.SERIF;
372 break;
373
374 case MONOSPACE:
375 tf = Typeface.MONOSPACE;
376 break;
377 }
378
379 setSwitchTypeface(tf, styleIndex);
380 }
381
382 /**
383 * Sets the typeface and style in which the text should be displayed on the
384 * switch, and turns on the fake bold and italic bits in the Paint if the
385 * Typeface that you provided does not have all the bits in the
386 * style that you specified.
387 */
388 public void setSwitchTypeface(Typeface tf, int style) {
389 if (style > 0) {
390 if (tf == null) {
391 tf = Typeface.defaultFromStyle(style);
392 } else {
393 tf = Typeface.create(tf, style);
394 }
395
396 setSwitchTypeface(tf);
397 // now compute what (if any) algorithmic styling is needed
398 int typefaceStyle = tf != null ? tf.getStyle() : 0;
399 int need = style & ~typefaceStyle;
400 mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
401 mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
402 } else {
Victoria Leaseaa0980a2012-06-11 14:46:04 -0700403 mTextPaint.setFakeBoldText(false);
Adam Powell12190b32010-11-28 19:07:53 -0800404 mTextPaint.setTextSkewX(0);
405 setSwitchTypeface(tf);
406 }
407 }
408
409 /**
Chet Haase150176d2011-08-26 09:54:06 -0700410 * Sets the typeface in which the text should be displayed on the switch.
Adam Powell12190b32010-11-28 19:07:53 -0800411 * Note that not all Typeface families actually have bold and italic
412 * variants, so you may need to use
413 * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
414 * that you actually want.
415 *
416 * @attr ref android.R.styleable#TextView_typeface
417 * @attr ref android.R.styleable#TextView_textStyle
418 */
419 public void setSwitchTypeface(Typeface tf) {
420 if (mTextPaint.getTypeface() != tf) {
421 mTextPaint.setTypeface(tf);
422
423 requestLayout();
424 invalidate();
425 }
426 }
427
428 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800429 * Set the amount of horizontal padding between the switch and the associated text.
430 *
431 * @param pixels Amount of padding in pixels
432 *
433 * @attr ref android.R.styleable#Switch_switchPadding
434 */
435 public void setSwitchPadding(int pixels) {
436 mSwitchPadding = pixels;
437 requestLayout();
438 }
439
440 /**
441 * Get the amount of horizontal padding between the switch and the associated text.
442 *
443 * @return Amount of padding in pixels
444 *
445 * @attr ref android.R.styleable#Switch_switchPadding
446 */
447 public int getSwitchPadding() {
448 return mSwitchPadding;
449 }
450
451 /**
452 * Set the minimum width of the switch in pixels. The switch's width will be the maximum
453 * of this value and its measured width as determined by the switch drawables and text used.
454 *
455 * @param pixels Minimum width of the switch in pixels
456 *
457 * @attr ref android.R.styleable#Switch_switchMinWidth
458 */
459 public void setSwitchMinWidth(int pixels) {
460 mSwitchMinWidth = pixels;
461 requestLayout();
462 }
463
464 /**
465 * Get the minimum width of the switch in pixels. The switch's width will be the maximum
466 * of this value and its measured width as determined by the switch drawables and text used.
467 *
468 * @return Minimum width of the switch in pixels
469 *
470 * @attr ref android.R.styleable#Switch_switchMinWidth
471 */
472 public int getSwitchMinWidth() {
473 return mSwitchMinWidth;
474 }
475
476 /**
477 * Set the horizontal padding around the text drawn on the switch itself.
478 *
479 * @param pixels Horizontal padding for switch thumb text in pixels
480 *
481 * @attr ref android.R.styleable#Switch_thumbTextPadding
482 */
483 public void setThumbTextPadding(int pixels) {
484 mThumbTextPadding = pixels;
485 requestLayout();
486 }
487
488 /**
489 * Get the horizontal padding around the text drawn on the switch itself.
490 *
491 * @return Horizontal padding for switch thumb text in pixels
492 *
493 * @attr ref android.R.styleable#Switch_thumbTextPadding
494 */
495 public int getThumbTextPadding() {
496 return mThumbTextPadding;
497 }
498
499 /**
500 * Set the drawable used for the track that the switch slides within.
501 *
502 * @param track Track drawable
503 *
504 * @attr ref android.R.styleable#Switch_track
505 */
506 public void setTrackDrawable(Drawable track) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700507 if (mTrackDrawable != null) {
508 mTrackDrawable.setCallback(null);
509 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800510 mTrackDrawable = track;
Alan Viveretteb0674052014-09-26 16:12:16 -0700511 if (track != null) {
512 track.setCallback(this);
513 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800514 requestLayout();
515 }
516
517 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800518 * Set the drawable used for the track that the switch slides within.
519 *
Adam Powelldca510e2012-03-08 20:06:39 -0800520 * @param resId Resource ID of a track drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800521 *
522 * @attr ref android.R.styleable#Switch_track
523 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700524 public void setTrackResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800525 setTrackDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800526 }
527
528 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800529 * Get the drawable used for the track that the switch slides within.
530 *
531 * @return Track drawable
532 *
533 * @attr ref android.R.styleable#Switch_track
534 */
535 public Drawable getTrackDrawable() {
536 return mTrackDrawable;
537 }
538
539 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800540 * Applies a tint to the track drawable. Does not modify the current
541 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
542 * <p>
543 * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
544 * automatically mutate the drawable and apply the specified tint and tint
545 * mode using {@link Drawable#setTintList(ColorStateList)}.
546 *
547 * @param tint the tint to apply, may be {@code null} to clear tint
548 *
549 * @attr ref android.R.styleable#Switch_trackTint
550 * @see #getTrackTintList()
551 * @see Drawable#setTintList(ColorStateList)
552 */
553 public void setTrackTintList(@Nullable ColorStateList tint) {
554 mTrackTintList = tint;
555 mHasTrackTint = true;
556
557 applyTrackTint();
558 }
559
560 /**
561 * @return the tint applied to the track drawable
562 * @attr ref android.R.styleable#Switch_trackTint
563 * @see #setTrackTintList(ColorStateList)
564 */
565 @Nullable
566 public ColorStateList getTrackTintList() {
567 return mTrackTintList;
568 }
569
570 /**
571 * Specifies the blending mode used to apply the tint specified by
572 * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
573 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
574 *
575 * @param tintMode the blending mode used to apply the tint, may be
576 * {@code null} to clear tint
577 * @attr ref android.R.styleable#Switch_trackTintMode
578 * @see #getTrackTintMode()
579 * @see Drawable#setTintMode(PorterDuff.Mode)
580 */
581 public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
582 mTrackTintMode = tintMode;
583 mHasTrackTintMode = true;
584
585 applyTrackTint();
586 }
587
588 /**
589 * @return the blending mode used to apply the tint to the track
590 * drawable
591 * @attr ref android.R.styleable#Switch_trackTintMode
592 * @see #setTrackTintMode(PorterDuff.Mode)
593 */
594 @Nullable
595 public PorterDuff.Mode getTrackTintMode() {
596 return mTrackTintMode;
597 }
598
599 private void applyTrackTint() {
600 if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
601 mTrackDrawable = mTrackDrawable.mutate();
602
603 if (mHasTrackTint) {
604 mTrackDrawable.setTintList(mTrackTintList);
605 }
606
607 if (mHasTrackTintMode) {
608 mTrackDrawable.setTintMode(mTrackTintMode);
609 }
610
611 // The drawable (or one of its children) may not have been
612 // stateful before applying the tint, so let's try again.
613 if (mTrackDrawable.isStateful()) {
614 mTrackDrawable.setState(getDrawableState());
615 }
616 }
617 }
618
619 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800620 * Set the drawable used for the switch "thumb" - the piece that the user
621 * can physically touch and drag along the track.
622 *
623 * @param thumb Thumb drawable
624 *
625 * @attr ref android.R.styleable#Switch_thumb
626 */
627 public void setThumbDrawable(Drawable thumb) {
Alan Viveretteb0674052014-09-26 16:12:16 -0700628 if (mThumbDrawable != null) {
629 mThumbDrawable.setCallback(null);
630 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800631 mThumbDrawable = thumb;
Alan Viveretteb0674052014-09-26 16:12:16 -0700632 if (thumb != null) {
633 thumb.setCallback(this);
634 }
Adam Powell6c86e1b2012-03-08 15:11:46 -0800635 requestLayout();
636 }
637
638 /**
Adam Powelld9c7be62012-03-08 19:43:43 -0800639 * Set the drawable used for the switch "thumb" - the piece that the user
640 * can physically touch and drag along the track.
641 *
Adam Powelldca510e2012-03-08 20:06:39 -0800642 * @param resId Resource ID of a thumb drawable
Adam Powelld9c7be62012-03-08 19:43:43 -0800643 *
644 * @attr ref android.R.styleable#Switch_thumb
645 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700646 public void setThumbResource(@DrawableRes int resId) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -0800647 setThumbDrawable(getContext().getDrawable(resId));
Adam Powelld9c7be62012-03-08 19:43:43 -0800648 }
649
650 /**
Adam Powell6c86e1b2012-03-08 15:11:46 -0800651 * Get the drawable used for the switch "thumb" - the piece that the user
652 * can physically touch and drag along the track.
653 *
654 * @return Thumb drawable
655 *
656 * @attr ref android.R.styleable#Switch_thumb
657 */
658 public Drawable getThumbDrawable() {
659 return mThumbDrawable;
660 }
661
662 /**
Alan Viverettee7eee642015-01-29 14:27:49 -0800663 * Applies a tint to the thumb drawable. Does not modify the current
664 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
665 * <p>
666 * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
667 * automatically mutate the drawable and apply the specified tint and tint
668 * mode using {@link Drawable#setTintList(ColorStateList)}.
669 *
670 * @param tint the tint to apply, may be {@code null} to clear tint
671 *
672 * @attr ref android.R.styleable#Switch_thumbTint
673 * @see #getThumbTintList()
674 * @see Drawable#setTintList(ColorStateList)
675 */
676 public void setThumbTintList(@Nullable ColorStateList tint) {
677 mThumbTintList = tint;
678 mHasThumbTint = true;
679
680 applyThumbTint();
681 }
682
683 /**
684 * @return the tint applied to the thumb drawable
685 * @attr ref android.R.styleable#Switch_thumbTint
686 * @see #setThumbTintList(ColorStateList)
687 */
688 @Nullable
689 public ColorStateList getThumbTintList() {
690 return mThumbTintList;
691 }
692
693 /**
694 * Specifies the blending mode used to apply the tint specified by
695 * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
696 * The default mode is {@link PorterDuff.Mode#SRC_IN}.
697 *
698 * @param tintMode the blending mode used to apply the tint, may be
699 * {@code null} to clear tint
700 * @attr ref android.R.styleable#Switch_thumbTintMode
701 * @see #getThumbTintMode()
702 * @see Drawable#setTintMode(PorterDuff.Mode)
703 */
704 public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
705 mThumbTintMode = tintMode;
706 mHasThumbTintMode = true;
707
708 applyThumbTint();
709 }
710
711 /**
712 * @return the blending mode used to apply the tint to the thumb
713 * drawable
714 * @attr ref android.R.styleable#Switch_thumbTintMode
715 * @see #setThumbTintMode(PorterDuff.Mode)
716 */
717 @Nullable
718 public PorterDuff.Mode getThumbTintMode() {
719 return mThumbTintMode;
720 }
721
722 private void applyThumbTint() {
723 if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
724 mThumbDrawable = mThumbDrawable.mutate();
725
726 if (mHasThumbTint) {
727 mThumbDrawable.setTintList(mThumbTintList);
728 }
729
730 if (mHasThumbTintMode) {
731 mThumbDrawable.setTintMode(mThumbTintMode);
732 }
733
734 // The drawable (or one of its children) may not have been
735 // stateful before applying the tint, so let's try again.
736 if (mThumbDrawable.isStateful()) {
737 mThumbDrawable.setState(getDrawableState());
738 }
739 }
740 }
741
742 /**
Alan Viverette661e6362014-05-12 10:55:37 -0700743 * Specifies whether the track should be split by the thumb. When true,
744 * the thumb's optical bounds will be clipped out of the track drawable,
745 * then the thumb will be drawn into the resulting gap.
746 *
747 * @param splitTrack Whether the track should be split by the thumb
748 *
749 * @attr ref android.R.styleable#Switch_splitTrack
750 */
751 public void setSplitTrack(boolean splitTrack) {
752 mSplitTrack = splitTrack;
753 invalidate();
754 }
755
756 /**
757 * Returns whether the track should be split by the thumb.
758 *
759 * @attr ref android.R.styleable#Switch_splitTrack
760 */
761 public boolean getSplitTrack() {
762 return mSplitTrack;
763 }
764
765 /**
Chet Haase150176d2011-08-26 09:54:06 -0700766 * Returns the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800767 *
768 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800769 */
770 public CharSequence getTextOn() {
771 return mTextOn;
772 }
773
774 /**
Chet Haase150176d2011-08-26 09:54:06 -0700775 * Sets the text displayed when the button is in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800776 *
777 * @attr ref android.R.styleable#Switch_textOn
Adam Powell12190b32010-11-28 19:07:53 -0800778 */
779 public void setTextOn(CharSequence textOn) {
780 mTextOn = textOn;
781 requestLayout();
782 }
783
784 /**
Chet Haase150176d2011-08-26 09:54:06 -0700785 * Returns the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800786 *
787 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800788 */
789 public CharSequence getTextOff() {
790 return mTextOff;
791 }
792
793 /**
Chet Haase150176d2011-08-26 09:54:06 -0700794 * Sets the text displayed when the button is not in the checked state.
Adam Powell6c86e1b2012-03-08 15:11:46 -0800795 *
796 * @attr ref android.R.styleable#Switch_textOff
Adam Powell12190b32010-11-28 19:07:53 -0800797 */
798 public void setTextOff(CharSequence textOff) {
799 mTextOff = textOff;
800 requestLayout();
801 }
802
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700803 /**
804 * Sets whether the on/off text should be displayed.
805 *
806 * @param showText {@code true} to display on/off text
Alan Viverette0c0dde72014-07-30 13:29:39 -0700807 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700808 */
809 public void setShowText(boolean showText) {
810 if (mShowText != showText) {
811 mShowText = showText;
812 requestLayout();
813 }
814 }
815
816 /**
817 * @return whether the on/off text should be displayed
Alan Viverette0c0dde72014-07-30 13:29:39 -0700818 * @attr ref android.R.styleable#Switch_showText
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700819 */
820 public boolean getShowText() {
821 return mShowText;
822 }
823
Adam Powell12190b32010-11-28 19:07:53 -0800824 @Override
825 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700826 if (mShowText) {
827 if (mOnLayout == null) {
828 mOnLayout = makeLayout(mTextOn);
829 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800830
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700831 if (mOffLayout == null) {
832 mOffLayout = makeLayout(mTextOff);
833 }
Adam Powell12190b32010-11-28 19:07:53 -0800834 }
835
Alan Viverette9b38f6c2014-07-30 02:39:07 +0000836 final Rect padding = mTempRect;
Alan Viverette0c0dde72014-07-30 13:29:39 -0700837 final int thumbWidth;
838 final int thumbHeight;
839 if (mThumbDrawable != null) {
840 // Cached thumb width does not include padding.
841 mThumbDrawable.getPadding(padding);
842 thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
843 thumbHeight = mThumbDrawable.getIntrinsicHeight();
844 } else {
845 thumbWidth = 0;
846 thumbHeight = 0;
847 }
848
849 final int maxTextWidth;
850 if (mShowText) {
851 maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
852 + mThumbTextPadding * 2;
853 } else {
854 maxTextWidth = 0;
855 }
856
857 mThumbWidth = Math.max(maxTextWidth, thumbWidth);
858
859 final int trackHeight;
Alan Viverette4d065a02014-07-11 15:28:38 -0700860 if (mTrackDrawable != null) {
861 mTrackDrawable.getPadding(padding);
862 trackHeight = mTrackDrawable.getIntrinsicHeight();
863 } else {
864 padding.setEmpty();
865 trackHeight = 0;
866 }
867
Alan Viverette0c0dde72014-07-30 13:29:39 -0700868 // Adjust left and right padding to ensure there's enough room for the
869 // thumb's padding (when present).
870 int paddingLeft = padding.left;
871 int paddingRight = padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -0700872 if (mThumbDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -0700873 final Insets inset = mThumbDrawable.getOpticalInsets();
874 paddingLeft = Math.max(paddingLeft, inset.left);
875 paddingRight = Math.max(paddingRight, inset.right);
Alan Viverette4d065a02014-07-11 15:28:38 -0700876 }
Alan Viverette5876ff42014-03-03 17:40:46 -0800877
Adam Powell12190b32010-11-28 19:07:53 -0800878 final int switchWidth = Math.max(mSwitchMinWidth,
Alan Viverette0c0dde72014-07-30 13:29:39 -0700879 2 * mThumbWidth + paddingLeft + paddingRight);
Alan Viverette4d065a02014-07-11 15:28:38 -0700880 final int switchHeight = Math.max(trackHeight, thumbHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800881 mSwitchWidth = switchWidth;
882 mSwitchHeight = switchHeight;
883
884 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Alan Viverette4d065a02014-07-11 15:28:38 -0700885
Adam Powell12190b32010-11-28 19:07:53 -0800886 final int measuredHeight = getMeasuredHeight();
887 if (measuredHeight < switchHeight) {
Dianne Hackborn189ee182010-12-02 21:48:53 -0800888 setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
Adam Powell12190b32010-11-28 19:07:53 -0800889 }
890 }
891
Alan Viverettea54956a2015-01-07 16:05:02 -0800892 /** @hide */
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700893 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800894 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
895 super.onPopulateAccessibilityEventInternal(event);
Alan Viverette2a37cf8d2014-06-19 17:09:10 -0700896
897 final CharSequence text = isChecked() ? mTextOn : mTextOff;
898 if (text != null) {
899 event.getText().add(text);
Svetoslav Ganov76502592011-07-29 10:44:59 -0700900 }
Svetoslav Ganov63bce032011-07-23 19:52:17 -0700901 }
902
Adam Powell12190b32010-11-28 19:07:53 -0800903 private Layout makeLayout(CharSequence text) {
Daniel Sandler4c3308d2012-04-19 11:04:39 -0400904 final CharSequence transformed = (mSwitchTransformationMethod != null)
905 ? mSwitchTransformationMethod.getTransformation(text, this)
906 : text;
907
Siyamed Sinir79bf9d12016-05-18 19:57:52 -0700908 int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
909 transformed.length(), mTextPaint, getTextDirectionHeuristic()));
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700910 return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
911 .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
912 .build();
Adam Powell12190b32010-11-28 19:07:53 -0800913 }
914
915 /**
916 * @return true if (x, y) is within the target area of the switch thumb
917 */
918 private boolean hitThumb(float x, float y) {
Alan Viverette01a09632014-12-08 13:02:06 -0800919 if (mThumbDrawable == null) {
920 return false;
921 }
922
Alan Viverettecc2688d2013-09-17 17:00:12 -0700923 // Relies on mTempRect, MUST be called first!
924 final int thumbOffset = getThumbOffset();
925
Adam Powell12190b32010-11-28 19:07:53 -0800926 mThumbDrawable.getPadding(mTempRect);
927 final int thumbTop = mSwitchTop - mTouchSlop;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700928 final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
Adam Powell12190b32010-11-28 19:07:53 -0800929 final int thumbRight = thumbLeft + mThumbWidth +
930 mTempRect.left + mTempRect.right + mTouchSlop;
931 final int thumbBottom = mSwitchBottom + mTouchSlop;
932 return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
933 }
934
935 @Override
936 public boolean onTouchEvent(MotionEvent ev) {
937 mVelocityTracker.addMovement(ev);
938 final int action = ev.getActionMasked();
939 switch (action) {
940 case MotionEvent.ACTION_DOWN: {
941 final float x = ev.getX();
942 final float y = ev.getY();
Gilles Debunnec2ab0d62011-06-13 12:52:48 -0700943 if (isEnabled() && hitThumb(x, y)) {
Adam Powell12190b32010-11-28 19:07:53 -0800944 mTouchMode = TOUCH_MODE_DOWN;
945 mTouchX = x;
946 mTouchY = y;
947 }
948 break;
949 }
950
951 case MotionEvent.ACTION_MOVE: {
952 switch (mTouchMode) {
953 case TOUCH_MODE_IDLE:
954 // Didn't target the thumb, treat normally.
955 break;
956
957 case TOUCH_MODE_DOWN: {
958 final float x = ev.getX();
959 final float y = ev.getY();
960 if (Math.abs(x - mTouchX) > mTouchSlop ||
961 Math.abs(y - mTouchY) > mTouchSlop) {
962 mTouchMode = TOUCH_MODE_DRAGGING;
963 getParent().requestDisallowInterceptTouchEvent(true);
964 mTouchX = x;
965 mTouchY = y;
966 return true;
967 }
968 break;
969 }
970
971 case TOUCH_MODE_DRAGGING: {
972 final float x = ev.getX();
Alan Viverettecc2688d2013-09-17 17:00:12 -0700973 final int thumbScrollRange = getThumbScrollRange();
974 final float thumbScrollOffset = x - mTouchX;
975 float dPos;
976 if (thumbScrollRange != 0) {
977 dPos = thumbScrollOffset / thumbScrollRange;
978 } else {
979 // If the thumb scroll range is empty, just use the
980 // movement direction to snap on or off.
981 dPos = thumbScrollOffset > 0 ? 1 : -1;
982 }
983 if (isLayoutRtl()) {
984 dPos = -dPos;
985 }
986 final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
Adam Powell12190b32010-11-28 19:07:53 -0800987 if (newPos != mThumbPosition) {
Adam Powell12190b32010-11-28 19:07:53 -0800988 mTouchX = x;
Alan Viverettecc2688d2013-09-17 17:00:12 -0700989 setThumbPosition(newPos);
Adam Powell12190b32010-11-28 19:07:53 -0800990 }
991 return true;
992 }
993 }
994 break;
995 }
996
997 case MotionEvent.ACTION_UP:
998 case MotionEvent.ACTION_CANCEL: {
999 if (mTouchMode == TOUCH_MODE_DRAGGING) {
1000 stopDrag(ev);
Alan Viverettead2f8e32014-05-16 13:28:33 -07001001 // Allow super class to handle pressed state, etc.
1002 super.onTouchEvent(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001003 return true;
1004 }
1005 mTouchMode = TOUCH_MODE_IDLE;
1006 mVelocityTracker.clear();
1007 break;
1008 }
1009 }
1010
1011 return super.onTouchEvent(ev);
1012 }
1013
1014 private void cancelSuperTouch(MotionEvent ev) {
1015 MotionEvent cancel = MotionEvent.obtain(ev);
1016 cancel.setAction(MotionEvent.ACTION_CANCEL);
1017 super.onTouchEvent(cancel);
1018 cancel.recycle();
1019 }
1020
1021 /**
1022 * Called from onTouchEvent to end a drag operation.
1023 *
1024 * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
1025 */
1026 private void stopDrag(MotionEvent ev) {
1027 mTouchMode = TOUCH_MODE_IDLE;
Adam Powell12190b32010-11-28 19:07:53 -08001028
Alan Viverette86453ff2013-09-26 14:46:08 -07001029 // Commit the change if the event is up and not canceled and the switch
1030 // has not been disabled during the drag.
1031 final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
Alan Viveretted4e77902014-10-27 17:50:51 -07001032 final boolean oldState = isChecked();
Alan Viverette86453ff2013-09-26 14:46:08 -07001033 final boolean newState;
Adam Powell12190b32010-11-28 19:07:53 -08001034 if (commitChange) {
Adam Powell12190b32010-11-28 19:07:53 -08001035 mVelocityTracker.computeCurrentVelocity(1000);
Alan Viverette86453ff2013-09-26 14:46:08 -07001036 final float xvel = mVelocityTracker.getXVelocity();
Adam Powell12190b32010-11-28 19:07:53 -08001037 if (Math.abs(xvel) > mMinFlingVelocity) {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001038 newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
Adam Powell12190b32010-11-28 19:07:53 -08001039 } else {
1040 newState = getTargetCheckedState();
1041 }
Adam Powell12190b32010-11-28 19:07:53 -08001042 } else {
Alan Viveretted4e77902014-10-27 17:50:51 -07001043 newState = oldState;
Adam Powell12190b32010-11-28 19:07:53 -08001044 }
Alan Viverette86453ff2013-09-26 14:46:08 -07001045
Alan Viveretted4e77902014-10-27 17:50:51 -07001046 if (newState != oldState) {
1047 playSoundEffect(SoundEffectConstants.CLICK);
Alan Viveretted4e77902014-10-27 17:50:51 -07001048 }
Alan Viverette03306a02015-08-07 11:34:25 -04001049 // Always call setChecked so that the thumb is moved back to the correct edge
1050 setChecked(newState);
Alan Viverette86453ff2013-09-26 14:46:08 -07001051 cancelSuperTouch(ev);
Adam Powell12190b32010-11-28 19:07:53 -08001052 }
1053
1054 private void animateThumbToCheckedState(boolean newCheckedState) {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001055 final float targetPosition = newCheckedState ? 1 : 0;
1056 mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
1057 mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
1058 mPositionAnimator.setAutoCancel(true);
1059 mPositionAnimator.start();
1060 }
1061
Mathew Inwood978c6e22018-08-21 15:58:55 +01001062 @UnsupportedAppUsage
Alan Viverettecc2688d2013-09-17 17:00:12 -07001063 private void cancelPositionAnimator() {
1064 if (mPositionAnimator != null) {
1065 mPositionAnimator.cancel();
1066 }
Adam Powell12190b32010-11-28 19:07:53 -08001067 }
1068
1069 private boolean getTargetCheckedState() {
Alan Viverettecc2688d2013-09-17 17:00:12 -07001070 return mThumbPosition > 0.5f;
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001071 }
1072
Alan Viverettecc2688d2013-09-17 17:00:12 -07001073 /**
1074 * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
1075 *
1076 * @param position new position between [0,1]
1077 */
Mathew Inwood978c6e22018-08-21 15:58:55 +01001078 @UnsupportedAppUsage
Alan Viverettecc2688d2013-09-17 17:00:12 -07001079 private void setThumbPosition(float position) {
1080 mThumbPosition = position;
1081 invalidate();
1082 }
1083
1084 @Override
1085 public void toggle() {
Alan Viverette86453ff2013-09-26 14:46:08 -07001086 setChecked(!isChecked());
Adam Powell12190b32010-11-28 19:07:53 -08001087 }
1088
1089 @Override
1090 public void setChecked(boolean checked) {
1091 super.setChecked(checked);
Alan Viverettecc2688d2013-09-17 17:00:12 -07001092
Alan Viverette467d6292014-08-12 15:13:19 -07001093 // Calling the super method may result in setChecked() getting called
1094 // recursively with a different value, so load the REAL value...
1095 checked = isChecked();
1096
Alan Viverette86453ff2013-09-26 14:46:08 -07001097 if (isAttachedToWindow() && isLaidOut()) {
1098 animateThumbToCheckedState(checked);
1099 } else {
1100 // Immediately move the thumb to the new position.
1101 cancelPositionAnimator();
1102 setThumbPosition(checked ? 1 : 0);
1103 }
Adam Powell12190b32010-11-28 19:07:53 -08001104 }
1105
1106 @Override
1107 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1108 super.onLayout(changed, left, top, right, bottom);
1109
Alan Viverette0c0dde72014-07-30 13:29:39 -07001110 int opticalInsetLeft = 0;
1111 int opticalInsetRight = 0;
1112 if (mThumbDrawable != null) {
1113 final Rect trackPadding = mTempRect;
1114 if (mTrackDrawable != null) {
1115 mTrackDrawable.getPadding(trackPadding);
1116 } else {
1117 trackPadding.setEmpty();
1118 }
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001119
Alan Viverette0c0dde72014-07-30 13:29:39 -07001120 final Insets insets = mThumbDrawable.getOpticalInsets();
1121 opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
1122 opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
Alan Viverette8bb39902014-07-29 17:22:30 -07001123 }
1124
Alan Viverette0c0dde72014-07-30 13:29:39 -07001125 final int switchRight;
1126 final int switchLeft;
1127 if (isLayoutRtl()) {
1128 switchLeft = getPaddingLeft() + opticalInsetLeft;
1129 switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
1130 } else {
1131 switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
1132 switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
1133 }
1134
1135 final int switchTop;
1136 final int switchBottom;
Adam Powell12190b32010-11-28 19:07:53 -08001137 switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
1138 default:
1139 case Gravity.TOP:
1140 switchTop = getPaddingTop();
1141 switchBottom = switchTop + mSwitchHeight;
1142 break;
1143
1144 case Gravity.CENTER_VERTICAL:
1145 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
1146 mSwitchHeight / 2;
1147 switchBottom = switchTop + mSwitchHeight;
1148 break;
1149
1150 case Gravity.BOTTOM:
1151 switchBottom = getHeight() - getPaddingBottom();
1152 switchTop = switchBottom - mSwitchHeight;
1153 break;
1154 }
1155
1156 mSwitchLeft = switchLeft;
1157 mSwitchTop = switchTop;
1158 mSwitchBottom = switchBottom;
1159 mSwitchRight = switchRight;
1160 }
1161
1162 @Override
Alan Viverettead2f8e32014-05-16 13:28:33 -07001163 public void draw(Canvas c) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001164 final Rect padding = mTempRect;
Alan Viverette5876ff42014-03-03 17:40:46 -08001165 final int switchLeft = mSwitchLeft;
1166 final int switchTop = mSwitchTop;
1167 final int switchRight = mSwitchRight;
1168 final int switchBottom = mSwitchBottom;
Alan Viverette0c0dde72014-07-30 13:29:39 -07001169
1170 int thumbInitialLeft = switchLeft + getThumbOffset();
1171
1172 final Insets thumbInsets;
1173 if (mThumbDrawable != null) {
1174 thumbInsets = mThumbDrawable.getOpticalInsets();
1175 } else {
1176 thumbInsets = Insets.NONE;
Alan Viverette8bb39902014-07-29 17:22:30 -07001177 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001178
Alan Viverette0c0dde72014-07-30 13:29:39 -07001179 // Layout the track.
1180 if (mTrackDrawable != null) {
1181 mTrackDrawable.getPadding(padding);
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001182
Alan Viverette0c0dde72014-07-30 13:29:39 -07001183 // Adjust thumb position for track padding.
1184 thumbInitialLeft += padding.left;
1185
1186 // If necessary, offset by the optical insets of the thumb asset.
1187 int trackLeft = switchLeft;
1188 int trackTop = switchTop;
1189 int trackRight = switchRight;
1190 int trackBottom = switchBottom;
1191 if (thumbInsets != Insets.NONE) {
1192 if (thumbInsets.left > padding.left) {
1193 trackLeft += thumbInsets.left - padding.left;
1194 }
1195 if (thumbInsets.top > padding.top) {
1196 trackTop += thumbInsets.top - padding.top;
1197 }
1198 if (thumbInsets.right > padding.right) {
1199 trackRight -= thumbInsets.right - padding.right;
1200 }
1201 if (thumbInsets.bottom > padding.bottom) {
1202 trackBottom -= thumbInsets.bottom - padding.bottom;
1203 }
1204 }
1205 mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
1206 }
Alan Viverette9b38f6c2014-07-30 02:39:07 +00001207
Alan Viverette661e6362014-05-12 10:55:37 -07001208 // Layout the thumb.
Alan Viverette4d065a02014-07-11 15:28:38 -07001209 if (mThumbDrawable != null) {
1210 mThumbDrawable.getPadding(padding);
Alan Viverette0c0dde72014-07-30 13:29:39 -07001211
1212 final int thumbLeft = thumbInitialLeft - padding.left;
1213 final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001214 mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
Alan Viverette61956602014-04-22 19:07:06 -07001215
Alan Viverette4d065a02014-07-11 15:28:38 -07001216 final Drawable background = getBackground();
1217 if (background != null) {
1218 background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
1219 }
Alan Viverette61956602014-04-22 19:07:06 -07001220 }
1221
Alan Viverettead2f8e32014-05-16 13:28:33 -07001222 // Draw the background.
1223 super.draw(c);
1224 }
1225
1226 @Override
1227 protected void onDraw(Canvas canvas) {
Alan Viverette61956602014-04-22 19:07:06 -07001228 super.onDraw(canvas);
1229
Alan Viverette4d065a02014-07-11 15:28:38 -07001230 final Rect padding = mTempRect;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001231 final Drawable trackDrawable = mTrackDrawable;
Alan Viverette4d065a02014-07-11 15:28:38 -07001232 if (trackDrawable != null) {
1233 trackDrawable.getPadding(padding);
1234 } else {
1235 padding.setEmpty();
1236 }
Alan Viverettead2f8e32014-05-16 13:28:33 -07001237
1238 final int switchTop = mSwitchTop;
1239 final int switchBottom = mSwitchBottom;
Alan Viverette4d065a02014-07-11 15:28:38 -07001240 final int switchInnerTop = switchTop + padding.top;
Alan Viverette4d065a02014-07-11 15:28:38 -07001241 final int switchInnerBottom = switchBottom - padding.bottom;
Alan Viverettead2f8e32014-05-16 13:28:33 -07001242
Alan Viverette4d065a02014-07-11 15:28:38 -07001243 final Drawable thumbDrawable = mThumbDrawable;
1244 if (trackDrawable != null) {
1245 if (mSplitTrack && thumbDrawable != null) {
1246 final Insets insets = thumbDrawable.getOpticalInsets();
1247 thumbDrawable.copyBounds(padding);
1248 padding.left += insets.left;
1249 padding.right -= insets.right;
Alan Viverette661e6362014-05-12 10:55:37 -07001250
Alan Viverette4d065a02014-07-11 15:28:38 -07001251 final int saveCount = canvas.save();
1252 canvas.clipRect(padding, Op.DIFFERENCE);
1253 trackDrawable.draw(canvas);
1254 canvas.restoreToCount(saveCount);
1255 } else {
1256 trackDrawable.draw(canvas);
1257 }
Alan Viverette661e6362014-05-12 10:55:37 -07001258 }
Alan Viverette61956602014-04-22 19:07:06 -07001259
1260 final int saveCount = canvas.save();
Alan Viverette4d065a02014-07-11 15:28:38 -07001261
1262 if (thumbDrawable != null) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001263 thumbDrawable.draw(canvas);
1264 }
Adam Powell12190b32010-11-28 19:07:53 -08001265
Alan Viverette5876ff42014-03-03 17:40:46 -08001266 final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001267 if (switchText != null) {
Alan Viverette661e6362014-05-12 10:55:37 -07001268 final int drawableState[] = getDrawableState();
1269 if (mTextColors != null) {
1270 mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
1271 }
1272 mTextPaint.drawableState = drawableState;
1273
Alan Viverette4d065a02014-07-11 15:28:38 -07001274 final int cX;
1275 if (thumbDrawable != null) {
1276 final Rect bounds = thumbDrawable.getBounds();
1277 cX = bounds.left + bounds.right;
1278 } else {
Alan Viverettedec17292014-07-12 00:26:36 -07001279 cX = getWidth();
Alan Viverette4d065a02014-07-11 15:28:38 -07001280 }
1281
1282 final int left = cX / 2 - switchText.getWidth() / 2;
Alan Viverette5876ff42014-03-03 17:40:46 -08001283 final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
1284 canvas.translate(left, top);
Fabrice Di Megliobe06e322012-09-11 17:42:45 -07001285 switchText.draw(canvas);
1286 }
Adam Powell12190b32010-11-28 19:07:53 -08001287
Alan Viverette5876ff42014-03-03 17:40:46 -08001288 canvas.restoreToCount(saveCount);
Adam Powell12190b32010-11-28 19:07:53 -08001289 }
1290
1291 @Override
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001292 public int getCompoundPaddingLeft() {
1293 if (!isLayoutRtl()) {
1294 return super.getCompoundPaddingLeft();
1295 }
1296 int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
1297 if (!TextUtils.isEmpty(getText())) {
1298 padding += mSwitchPadding;
1299 }
1300 return padding;
1301 }
1302
1303 @Override
Adam Powell12190b32010-11-28 19:07:53 -08001304 public int getCompoundPaddingRight() {
Fabrice Di Meglio28efba32012-06-01 16:52:31 -07001305 if (isLayoutRtl()) {
1306 return super.getCompoundPaddingRight();
1307 }
Adam Powell12190b32010-11-28 19:07:53 -08001308 int padding = super.getCompoundPaddingRight() + mSwitchWidth;
1309 if (!TextUtils.isEmpty(getText())) {
1310 padding += mSwitchPadding;
1311 }
1312 return padding;
1313 }
1314
Alan Viverettecc2688d2013-09-17 17:00:12 -07001315 /**
1316 * Translates thumb position to offset according to current RTL setting and
Alan Viverette0c0dde72014-07-30 13:29:39 -07001317 * thumb scroll range. Accounts for both track and thumb padding.
Alan Viverettecc2688d2013-09-17 17:00:12 -07001318 *
1319 * @return thumb offset
1320 */
1321 private int getThumbOffset() {
1322 final float thumbPosition;
1323 if (isLayoutRtl()) {
1324 thumbPosition = 1 - mThumbPosition;
1325 } else {
1326 thumbPosition = mThumbPosition;
1327 }
1328 return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
1329 }
1330
Adam Powell12190b32010-11-28 19:07:53 -08001331 private int getThumbScrollRange() {
Alan Viverette4d065a02014-07-11 15:28:38 -07001332 if (mTrackDrawable != null) {
Alan Viverette0c0dde72014-07-30 13:29:39 -07001333 final Rect padding = mTempRect;
1334 mTrackDrawable.getPadding(padding);
1335
1336 final Insets insets;
1337 if (mThumbDrawable != null) {
1338 insets = mThumbDrawable.getOpticalInsets();
1339 } else {
1340 insets = Insets.NONE;
1341 }
1342
1343 return mSwitchWidth - mThumbWidth - padding.left - padding.right
1344 - insets.left - insets.right;
Alan Viverette4d065a02014-07-11 15:28:38 -07001345 } else {
Adam Powell12190b32010-11-28 19:07:53 -08001346 return 0;
1347 }
Adam Powell12190b32010-11-28 19:07:53 -08001348 }
1349
1350 @Override
1351 protected int[] onCreateDrawableState(int extraSpace) {
1352 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1353 if (isChecked()) {
1354 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
1355 }
1356 return drawableState;
1357 }
1358
1359 @Override
1360 protected void drawableStateChanged() {
1361 super.drawableStateChanged();
1362
Alan Viverettead0020f2015-09-04 10:10:42 -04001363 final int[] state = getDrawableState();
1364 boolean changed = false;
Adam Powell12190b32010-11-28 19:07:53 -08001365
Alan Viverettead0020f2015-09-04 10:10:42 -04001366 final Drawable thumbDrawable = mThumbDrawable;
1367 if (thumbDrawable != null && thumbDrawable.isStateful()) {
1368 changed |= thumbDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001369 }
1370
Alan Viverettead0020f2015-09-04 10:10:42 -04001371 final Drawable trackDrawable = mTrackDrawable;
1372 if (trackDrawable != null && trackDrawable.isStateful()) {
1373 changed |= trackDrawable.setState(state);
Alan Viverette661e6362014-05-12 10:55:37 -07001374 }
Adam Powell12190b32010-11-28 19:07:53 -08001375
Alan Viverettead0020f2015-09-04 10:10:42 -04001376 if (changed) {
1377 invalidate();
1378 }
Adam Powell12190b32010-11-28 19:07:53 -08001379 }
1380
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001381 @Override
Alan Viverette8de14942014-06-18 18:05:15 -07001382 public void drawableHotspotChanged(float x, float y) {
1383 super.drawableHotspotChanged(x, y);
Alan Viverettecebc6ba2014-06-13 15:52:13 -07001384
1385 if (mThumbDrawable != null) {
1386 mThumbDrawable.setHotspot(x, y);
1387 }
1388
1389 if (mTrackDrawable != null) {
1390 mTrackDrawable.setHotspot(x, y);
1391 }
1392 }
1393
Adam Powell12190b32010-11-28 19:07:53 -08001394 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -05001395 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell12190b32010-11-28 19:07:53 -08001396 return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
1397 }
1398
1399 @Override
1400 public void jumpDrawablesToCurrentState() {
1401 super.jumpDrawablesToCurrentState();
Alan Viverette4d065a02014-07-11 15:28:38 -07001402
1403 if (mThumbDrawable != null) {
1404 mThumbDrawable.jumpToCurrentState();
1405 }
1406
1407 if (mTrackDrawable != null) {
1408 mTrackDrawable.jumpToCurrentState();
1409 }
1410
Alan Viveretteee20d772015-09-23 14:23:21 -04001411 if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
Alan Viverette4d065a02014-07-11 15:28:38 -07001412 mPositionAnimator.end();
1413 mPositionAnimator = null;
1414 }
Adam Powell12190b32010-11-28 19:07:53 -08001415 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001416
1417 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001418 public CharSequence getAccessibilityClassName() {
1419 return Switch.class.getName();
1420 }
1421
Felipe Leme92ab3852018-11-15 11:30:40 -08001422 /** @hide */
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001423 @Override
Felipe Leme92ab3852018-11-15 11:30:40 -08001424 protected void onProvideStructure(@NonNull ViewStructure structure,
1425 @ViewStructureType int viewFor, int flags) {
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001426 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1427 if (!TextUtils.isEmpty(switchText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001428 CharSequence oldText = structure.getText();
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001429 if (TextUtils.isEmpty(oldText)) {
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001430 structure.setText(switchText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001431 } else {
1432 StringBuilder newText = new StringBuilder();
1433 newText.append(oldText).append(' ').append(switchText);
Dianne Hackborna83ce1d2015-03-11 15:16:13 -07001434 structure.setText(newText);
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001435 }
James Cook5cfaae42015-05-28 15:52:44 -07001436 // The style of the label text is provided via the base TextView class. This is more
1437 // relevant than the style of the (optional) on/off text on the switch button itself,
1438 // so ignore the size/color/style stored this.mTextPaint.
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001439 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001440 }
1441
Alan Viverettea54956a2015-01-07 16:05:02 -08001442 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001443 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001444 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1445 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov78bcc152012-04-12 17:17:19 -07001446 CharSequence switchText = isChecked() ? mTextOn : mTextOff;
1447 if (!TextUtils.isEmpty(switchText)) {
1448 CharSequence oldText = info.getText();
1449 if (TextUtils.isEmpty(oldText)) {
1450 info.setText(switchText);
1451 } else {
1452 StringBuilder newText = new StringBuilder();
1453 newText.append(oldText).append(' ').append(switchText);
1454 info.setText(newText);
1455 }
1456 }
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001457 }
Alan Viverettecc2688d2013-09-17 17:00:12 -07001458
1459 private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
1460 @Override
1461 public Float get(Switch object) {
1462 return object.mThumbPosition;
1463 }
1464
1465 @Override
1466 public void setValue(Switch object, float value) {
1467 object.setThumbPosition(value);
1468 }
1469 };
Adam Powell12190b32010-11-28 19:07:53 -08001470}