/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.animation.ObjectAnimator; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StyleRes; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.BlendMode; import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Build.VERSION_CODES; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.AllCapsTransformationMethod; import android.text.method.TransformationMethod2; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.MathUtils; import android.view.Gravity; import android.view.MotionEvent; import android.view.RemotableViewMethod; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.ViewStructure; import android.view.accessibility.AccessibilityEvent; import android.view.inspector.InspectableProperty; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; /** * A Switch is a two-state toggle widget. Users can drag the switch "thumb" back * and forth to select either of two options or simply tap the switch to toggle * between options. * *
The {@link #setText(CharSequence) text} property controls * the text of the switch label. The {@link #setTextOn(CharSequence) textOn} and * {@link #setTextOff(CharSequence) textOff} properties control the text of the * thumb. The {@link #setTextAppearance(int) textAppearance} property and the * related {@link #setTypeface(android.graphics.Typeface) setTypeface()} methods * control the typeface and style of the switch label. The * {@link #setSwitchTextAppearance(android.content.Context, int) * switchTextAppearance} property and the related * {@link #setSwitchTypeface(android.graphics.Typeface) setSwitchTypeface()} * methods control the typeface and style of the thumb text.
* *Note: The thumb text is displayed only if the
 * 
 * showText attribute is set to {@code true}. See also
 * {@link #setShowText(boolean)} and {@link #getShowText()}.
{@link androidx.appcompat.widget.SwitchCompat} provides backward * compatibility down to Android 4.0 (API level 14).
* *For more information, see the * * Toggle Buttons guide.
* * @attr ref android.R.styleable#Switch_textOn * @attr ref android.R.styleable#Switch_textOff * @attr ref android.R.styleable#Switch_switchMinWidth * @attr ref android.R.styleable#Switch_switchPadding * @attr ref android.R.styleable#Switch_switchTextAppearance * @attr ref android.R.styleable#Switch_thumb * @attr ref android.R.styleable#Switch_thumbTextPadding * @attr ref android.R.styleable#Switch_track */ @RemoteView public class Switch extends CompoundButton { private static final int THUMB_ANIMATION_DURATION = 250; private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_DOWN = 1; private static final int TOUCH_MODE_DRAGGING = 2; // Enum for the "typeface" XML parameter. private static final int SANS = 1; private static final int SERIF = 2; private static final int MONOSPACE = 3; @UnsupportedAppUsage private Drawable mThumbDrawable; private ColorStateList mThumbTintList = null; private BlendMode mThumbBlendMode = null; private boolean mHasThumbTint = false; private boolean mHasThumbTintMode = false; @UnsupportedAppUsage private Drawable mTrackDrawable; private ColorStateList mTrackTintList = null; private BlendMode mTrackBlendMode = null; private boolean mHasTrackTint = false; private boolean mHasTrackTintMode = false; private int mThumbTextPadding; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private int mSwitchMinWidth; private int mSwitchPadding; private boolean mSplitTrack; private CharSequence mTextOn; private CharSequence mTextOff; private boolean mShowText; private boolean mUseFallbackLineSpacing; private int mTouchMode; private int mTouchSlop; private float mTouchX; private float mTouchY; private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); private int mMinFlingVelocity; private float mThumbPosition; /** * Width required to draw the switch track and thumb. Includes padding and * optical bounds for both the track and thumb. */ @UnsupportedAppUsage private int mSwitchWidth; /** * Height required to draw the switch track and thumb. Includes padding and * optical bounds for both the track and thumb. */ @UnsupportedAppUsage private int mSwitchHeight; /** * Width of the thumb's content region. Does not include padding or * optical bounds. */ @UnsupportedAppUsage private int mThumbWidth; /** Left bound for drawing the switch track and thumb. */ private int mSwitchLeft; /** Top bound for drawing the switch track and thumb. */ private int mSwitchTop; /** Right bound for drawing the switch track and thumb. */ private int mSwitchRight; /** Bottom bound for drawing the switch track and thumb. */ private int mSwitchBottom; private TextPaint mTextPaint; private ColorStateList mTextColors; @UnsupportedAppUsage private Layout mOnLayout; @UnsupportedAppUsage private Layout mOffLayout; private TransformationMethod2 mSwitchTransformationMethod; private ObjectAnimator mPositionAnimator; @SuppressWarnings("hiding") private final Rect mTempRect = new Rect(); private static final int[] CHECKED_STATE_SET = { R.attr.state_checked }; /** * Construct a new Switch with default styling. * * @param context The Context that will determine this widget's theming. */ public Switch(Context context) { this(context, null); } /** * Construct a new Switch with default styling, overriding specific style * attributes as requested. * * @param context The Context that will determine this widget's theming. * @param attrs Specification of attributes that should deviate from default styling. */ public Switch(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.switchStyle); } /** * Construct a new Switch with a default style determined by the given theme attribute, * overriding specific style attributes as requested. * * @param context The Context that will determine this widget's theming. * @param attrs Specification of attributes that should deviate from the default styling. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. */ public Switch(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } /** * Construct a new Switch with a default style determined by the given theme * attribute or style resource, overriding specific style attributes as * requested. * * @param context The Context that will determine this widget's theming. * @param attrs Specification of attributes that should deviate from the * default styling. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @param defStyleRes A resource identifier of a style resource that * supplies default values for the view, used only if * defStyleAttr is 0 or can not be found in the theme. Can be 0 * to not look for defaults. */ public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); final Resources res = getResources(); mTextPaint.density = res.getDisplayMetrics().density; mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes); saveAttributeDataForStyleable(context, com.android.internal.R.styleable.Switch, attrs, a, defStyleAttr, defStyleRes); mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); if (mThumbDrawable != null) { mThumbDrawable.setCallback(this); } mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); if (mTrackDrawable != null) { mTrackDrawable.setCallback(this); } mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn); mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff); mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true); mThumbTextPadding = a.getDimensionPixelSize( com.android.internal.R.styleable.Switch_thumbTextPadding, 0); mSwitchMinWidth = a.getDimensionPixelSize( com.android.internal.R.styleable.Switch_switchMinWidth, 0); mSwitchPadding = a.getDimensionPixelSize( com.android.internal.R.styleable.Switch_switchPadding, 0); mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); mUseFallbackLineSpacing = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.P; ColorStateList thumbTintList = a.getColorStateList( com.android.internal.R.styleable.Switch_thumbTint); if (thumbTintList != null) { mThumbTintList = thumbTintList; mHasThumbTint = true; } BlendMode thumbTintMode = Drawable.parseBlendMode( a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null); if (mThumbBlendMode != thumbTintMode) { mThumbBlendMode = thumbTintMode; mHasThumbTintMode = true; } if (mHasThumbTint || mHasThumbTintMode) { applyThumbTint(); } ColorStateList trackTintList = a.getColorStateList( com.android.internal.R.styleable.Switch_trackTint); if (trackTintList != null) { mTrackTintList = trackTintList; mHasTrackTint = true; } BlendMode trackTintMode = Drawable.parseBlendMode( a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null); if (mTrackBlendMode != trackTintMode) { mTrackBlendMode = trackTintMode; mHasTrackTintMode = true; } if (mHasTrackTint || mHasTrackTintMode) { applyTrackTint(); } final int appearance = a.getResourceId( com.android.internal.R.styleable.Switch_switchTextAppearance, 0); if (appearance != 0) { setSwitchTextAppearance(context, appearance); } a.recycle(); final ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); // Refresh display with current params refreshDrawableState(); // Default state is derived from on/off-text, so state has to be updated when on/off-text // are updated. setDefaultStateDescription(); setChecked(isChecked()); } /** * Sets the switch text color, size, style, hint color, and highlight color * from the specified TextAppearance resource. * * @attr ref android.R.styleable#Switch_switchTextAppearance */ public void setSwitchTextAppearance(Context context, @StyleRes int resid) { TypedArray appearance = context.obtainStyledAttributes(resid, com.android.internal.R.styleable.TextAppearance); ColorStateList colors; int ts; colors = appearance.getColorStateList(com.android.internal.R.styleable. TextAppearance_textColor); if (colors != null) { mTextColors = colors; } else { // If no color set in TextAppearance, default to the view's textColor mTextColors = getTextColors(); } ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable. TextAppearance_textSize, 0); if (ts != 0) { if (ts != mTextPaint.getTextSize()) { mTextPaint.setTextSize(ts); requestLayout(); } } int typefaceIndex, styleIndex; typefaceIndex = appearance.getInt(com.android.internal.R.styleable. TextAppearance_typeface, -1); styleIndex = appearance.getInt(com.android.internal.R.styleable. TextAppearance_textStyle, -1); setSwitchTypefaceByIndex(typefaceIndex, styleIndex); boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable. TextAppearance_textAllCaps, false); if (allCaps) { mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext()); mSwitchTransformationMethod.setLengthChangesAllowed(true); } else { mSwitchTransformationMethod = null; } appearance.recycle(); } private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) { Typeface tf = null; switch (typefaceIndex) { case SANS: tf = Typeface.SANS_SERIF; break; case SERIF: tf = Typeface.SERIF; break; case MONOSPACE: tf = Typeface.MONOSPACE; break; } setSwitchTypeface(tf, styleIndex); } /** * Sets the typeface and style in which the text should be displayed on the * switch, and turns on the fake bold and italic bits in the Paint if the * Typeface that you provided does not have all the bits in the * style that you specified. */ public void setSwitchTypeface(Typeface tf, int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); } else { tf = Typeface.create(tf, style); } setSwitchTypeface(tf); // now compute what (if any) algorithmic styling is needed int typefaceStyle = tf != null ? tf.getStyle() : 0; int need = style & ~typefaceStyle; mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); } else { mTextPaint.setFakeBoldText(false); mTextPaint.setTextSkewX(0); setSwitchTypeface(tf); } } /** * Sets the typeface in which the text should be displayed on the switch. * Note that not all Typeface families actually have bold and italic * variants, so you may need to use * {@link #setSwitchTypeface(Typeface, int)} to get the appearance * that you actually want. * * @attr ref android.R.styleable#TextView_typeface * @attr ref android.R.styleable#TextView_textStyle */ public void setSwitchTypeface(Typeface tf) { if (mTextPaint.getTypeface() != tf) { mTextPaint.setTypeface(tf); requestLayout(); invalidate(); } } /** * Set the amount of horizontal padding between the switch and the associated text. * * @param pixels Amount of padding in pixels * * @attr ref android.R.styleable#Switch_switchPadding */ @RemotableViewMethod public void setSwitchPadding(int pixels) { mSwitchPadding = pixels; requestLayout(); } /** * Get the amount of horizontal padding between the switch and the associated text. * * @return Amount of padding in pixels * * @attr ref android.R.styleable#Switch_switchPadding */ @InspectableProperty public int getSwitchPadding() { return mSwitchPadding; } /** * Set the minimum width of the switch in pixels. The switch's width will be the maximum * of this value and its measured width as determined by the switch drawables and text used. * * @param pixels Minimum width of the switch in pixels * * @attr ref android.R.styleable#Switch_switchMinWidth */ @RemotableViewMethod public void setSwitchMinWidth(int pixels) { mSwitchMinWidth = pixels; requestLayout(); } /** * Get the minimum width of the switch in pixels. The switch's width will be the maximum * of this value and its measured width as determined by the switch drawables and text used. * * @return Minimum width of the switch in pixels * * @attr ref android.R.styleable#Switch_switchMinWidth */ @InspectableProperty public int getSwitchMinWidth() { return mSwitchMinWidth; } /** * Set the horizontal padding around the text drawn on the switch itself. * * @param pixels Horizontal padding for switch thumb text in pixels * * @attr ref android.R.styleable#Switch_thumbTextPadding */ @RemotableViewMethod public void setThumbTextPadding(int pixels) { mThumbTextPadding = pixels; requestLayout(); } /** * Get the horizontal padding around the text drawn on the switch itself. * * @return Horizontal padding for switch thumb text in pixels * * @attr ref android.R.styleable#Switch_thumbTextPadding */ @InspectableProperty public int getThumbTextPadding() { return mThumbTextPadding; } /** * Set the drawable used for the track that the switch slides within. * * @param track Track drawable * * @attr ref android.R.styleable#Switch_track */ public void setTrackDrawable(Drawable track) { if (mTrackDrawable != null) { mTrackDrawable.setCallback(null); } mTrackDrawable = track; if (track != null) { track.setCallback(this); } requestLayout(); } /** * Set the drawable used for the track that the switch slides within. * * @param resId Resource ID of a track drawable * * @attr ref android.R.styleable#Switch_track */ @RemotableViewMethod(asyncImpl = "setTrackResourceAsync") public void setTrackResource(@DrawableRes int resId) { setTrackDrawable(getContext().getDrawable(resId)); } /** @hide **/ public Runnable setTrackResourceAsync(@DrawableRes int resId) { Drawable drawable = resId == 0 ? null : getContext().getDrawable(resId); return () -> setTrackDrawable(drawable); } /** * Get the drawable used for the track that the switch slides within. * * @return Track drawable * * @attr ref android.R.styleable#Switch_track */ @InspectableProperty(name = "track") public Drawable getTrackDrawable() { return mTrackDrawable; } /** * Set the drawable used for the track that the switch slides within to the specified Icon. * * @param icon an Icon holding the desired track, or {@code null} to clear * the track */ @RemotableViewMethod(asyncImpl = "setTrackIconAsync") public void setTrackIcon(@Nullable Icon icon) { setTrackDrawable(icon == null ? null : icon.loadDrawable(getContext())); } /** @hide **/ public Runnable setTrackIconAsync(@Nullable Icon icon) { Drawable track = icon == null ? null : icon.loadDrawable(getContext()); return () -> setTrackDrawable(track); } /** * Applies a tint to the track drawable. Does not modify the current * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. ** Subsequent calls to {@link #setTrackDrawable(Drawable)} will * automatically mutate the drawable and apply the specified tint and tint * mode using {@link Drawable#setTintList(ColorStateList)}. * * @param tint the tint to apply, may be {@code null} to clear tint * * @attr ref android.R.styleable#Switch_trackTint * @see #getTrackTintList() * @see Drawable#setTintList(ColorStateList) */ @RemotableViewMethod public void setTrackTintList(@Nullable ColorStateList tint) { mTrackTintList = tint; mHasTrackTint = true; applyTrackTint(); } /** * @return the tint applied to the track drawable * @attr ref android.R.styleable#Switch_trackTint * @see #setTrackTintList(ColorStateList) */ @InspectableProperty(name = "trackTint") @Nullable public ColorStateList getTrackTintList() { return mTrackTintList; } /** * Specifies the blending mode used to apply the tint specified by * {@link #setTrackTintList(ColorStateList)}} to the track drawable. * The default mode is {@link PorterDuff.Mode#SRC_IN}. * * @param tintMode the blending mode used to apply the tint, may be * {@code null} to clear tint * @attr ref android.R.styleable#Switch_trackTintMode * @see #getTrackTintMode() * @see Drawable#setTintMode(PorterDuff.Mode) */ public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) { setTrackTintBlendMode(tintMode != null ? BlendMode.fromValue(tintMode.nativeInt) : null); } /** * Specifies the blending mode used to apply the tint specified by * {@link #setTrackTintList(ColorStateList)}} to the track drawable. * The default mode is {@link BlendMode#SRC_IN}. * * @param blendMode the blending mode used to apply the tint, may be * {@code null} to clear tint * @attr ref android.R.styleable#Switch_trackTintMode * @see #getTrackTintMode() * @see Drawable#setTintBlendMode(BlendMode) */ @RemotableViewMethod public void setTrackTintBlendMode(@Nullable BlendMode blendMode) { mTrackBlendMode = blendMode; mHasTrackTintMode = true; applyTrackTint(); } /** * @return the blending mode used to apply the tint to the track * drawable * @attr ref android.R.styleable#Switch_trackTintMode * @see #setTrackTintMode(PorterDuff.Mode) */ @InspectableProperty @Nullable public PorterDuff.Mode getTrackTintMode() { BlendMode mode = getTrackTintBlendMode(); return mode != null ? BlendMode.blendModeToPorterDuffMode(mode) : null; } /** * @return the blending mode used to apply the tint to the track * drawable * @attr ref android.R.styleable#Switch_trackTintMode * @see #setTrackTintBlendMode(BlendMode) */ @InspectableProperty(attributeId = com.android.internal.R.styleable.Switch_trackTintMode) @Nullable public BlendMode getTrackTintBlendMode() { return mTrackBlendMode; } private void applyTrackTint() { if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) { mTrackDrawable = mTrackDrawable.mutate(); if (mHasTrackTint) { mTrackDrawable.setTintList(mTrackTintList); } if (mHasTrackTintMode) { mTrackDrawable.setTintBlendMode(mTrackBlendMode); } // The drawable (or one of its children) may not have been // stateful before applying the tint, so let's try again. if (mTrackDrawable.isStateful()) { mTrackDrawable.setState(getDrawableState()); } } } /** * Set the drawable used for the switch "thumb" - the piece that the user * can physically touch and drag along the track. * * @param thumb Thumb drawable * * @attr ref android.R.styleable#Switch_thumb */ public void setThumbDrawable(Drawable thumb) { if (mThumbDrawable != null) { mThumbDrawable.setCallback(null); } mThumbDrawable = thumb; if (thumb != null) { thumb.setCallback(this); } requestLayout(); } /** * Set the drawable used for the switch "thumb" - the piece that the user * can physically touch and drag along the track. * * @param resId Resource ID of a thumb drawable * * @attr ref android.R.styleable#Switch_thumb */ @RemotableViewMethod(asyncImpl = "setThumbResourceAsync") public void setThumbResource(@DrawableRes int resId) { setThumbDrawable(getContext().getDrawable(resId)); } /** @hide **/ public Runnable setThumbResourceAsync(@DrawableRes int resId) { Drawable drawable = resId == 0 ? null : getContext().getDrawable(resId); return () -> setThumbDrawable(drawable); } /** * Get the drawable used for the switch "thumb" - the piece that the user * can physically touch and drag along the track. * * @return Thumb drawable * * @attr ref android.R.styleable#Switch_thumb */ @InspectableProperty(name = "thumb") public Drawable getThumbDrawable() { return mThumbDrawable; } /** * Set the drawable used for the switch "thumb" - the piece that the user * can physically touch and drag along the track - to the specified Icon. * * @param icon an Icon holding the desired thumb, or {@code null} to clear * the thumb */ @RemotableViewMethod(asyncImpl = "setThumbIconAsync") public void setThumbIcon(@Nullable Icon icon) { setThumbDrawable(icon == null ? null : icon.loadDrawable(getContext())); } /** @hide **/ public Runnable setThumbIconAsync(@Nullable Icon icon) { Drawable track = icon == null ? null : icon.loadDrawable(getContext()); return () -> setThumbDrawable(track); } /** * Applies a tint to the thumb drawable. Does not modify the current * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. *
     * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
     * automatically mutate the drawable and apply the specified tint and tint
     * mode using {@link Drawable#setTintList(ColorStateList)}.
     *
     * @param tint the tint to apply, may be {@code null} to clear tint
     *
     * @attr ref android.R.styleable#Switch_thumbTint
     * @see #getThumbTintList()
     * @see Drawable#setTintList(ColorStateList)
     */
    @RemotableViewMethod
    public void setThumbTintList(@Nullable ColorStateList tint) {
        mThumbTintList = tint;
        mHasThumbTint = true;
        applyThumbTint();
    }
    /**
     * @return the tint applied to the thumb drawable
     * @attr ref android.R.styleable#Switch_thumbTint
     * @see #setThumbTintList(ColorStateList)
     */
    @InspectableProperty(name = "thumbTint")
    @Nullable
    public ColorStateList getThumbTintList() {
        return mThumbTintList;
    }
    /**
     * Specifies the blending mode used to apply the tint specified by
     * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
     * The default mode is {@link PorterDuff.Mode#SRC_IN}.
     *
     * @param tintMode the blending mode used to apply the tint, may be
     *                 {@code null} to clear tint
     * @attr ref android.R.styleable#Switch_thumbTintMode
     * @see #getThumbTintMode()
     * @see Drawable#setTintMode(PorterDuff.Mode)
     */
    public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
        setThumbTintBlendMode(tintMode != null ? BlendMode.fromValue(tintMode.nativeInt) : null);
    }
    /**
     * Specifies the blending mode used to apply the tint specified by
     * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
     * The default mode is {@link PorterDuff.Mode#SRC_IN}.
     *
     * @param blendMode the blending mode used to apply the tint, may be
     *                 {@code null} to clear tint
     * @attr ref android.R.styleable#Switch_thumbTintMode
     * @see #getThumbTintMode()
     * @see Drawable#setTintBlendMode(BlendMode)
     */
    @RemotableViewMethod
    public void setThumbTintBlendMode(@Nullable BlendMode blendMode) {
        mThumbBlendMode = blendMode;
        mHasThumbTintMode = true;
        applyThumbTint();
    }
    /**
     * @return the blending mode used to apply the tint to the thumb
     *         drawable
     * @attr ref android.R.styleable#Switch_thumbTintMode
     * @see #setThumbTintMode(PorterDuff.Mode)
     */
    @InspectableProperty
    @Nullable
    public PorterDuff.Mode getThumbTintMode() {
        BlendMode mode = getThumbTintBlendMode();
        return mode != null ? BlendMode.blendModeToPorterDuffMode(mode) : null;
    }
    /**
     * @return the blending mode used to apply the tint to the thumb
     *         drawable
     * @attr ref android.R.styleable#Switch_thumbTintMode
     * @see #setThumbTintBlendMode(BlendMode)
     */
    @InspectableProperty(attributeId = com.android.internal.R.styleable.Switch_thumbTintMode)
    @Nullable
    public BlendMode getThumbTintBlendMode() {
        return mThumbBlendMode;
    }
    private void applyThumbTint() {
        if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
            mThumbDrawable = mThumbDrawable.mutate();
            if (mHasThumbTint) {
                mThumbDrawable.setTintList(mThumbTintList);
            }
            if (mHasThumbTintMode) {
                mThumbDrawable.setTintBlendMode(mThumbBlendMode);
            }
            // The drawable (or one of its children) may not have been
            // stateful before applying the tint, so let's try again.
            if (mThumbDrawable.isStateful()) {
                mThumbDrawable.setState(getDrawableState());
            }
        }
    }
    /**
     * Specifies whether the track should be split by the thumb. When true,
     * the thumb's optical bounds will be clipped out of the track drawable,
     * then the thumb will be drawn into the resulting gap.
     *
     * @param splitTrack Whether the track should be split by the thumb
     *
     * @attr ref android.R.styleable#Switch_splitTrack
     */
    @RemotableViewMethod
    public void setSplitTrack(boolean splitTrack) {
        mSplitTrack = splitTrack;
        invalidate();
    }
    /**
     * Returns whether the track should be split by the thumb.
     *
     * @attr ref android.R.styleable#Switch_splitTrack
     */
    @InspectableProperty
    public boolean getSplitTrack() {
        return mSplitTrack;
    }
    /**
     * Returns the text displayed when the button is in the checked state.
     *
     * @attr ref android.R.styleable#Switch_textOn
     */
    @InspectableProperty
    public CharSequence getTextOn() {
        return mTextOn;
    }
    /**
     * Sets the text displayed when the button is in the checked state.
     *
     * @attr ref android.R.styleable#Switch_textOn
     */
    @RemotableViewMethod
    public void setTextOn(CharSequence textOn) {
        mTextOn = textOn;
        requestLayout();
        // Default state is derived from on/off-text, so state has to be updated when on/off-text
        // are updated.
        setDefaultStateDescription();
    }
    /**
     * Returns the text displayed when the button is not in the checked state.
     *
     * @attr ref android.R.styleable#Switch_textOff
     */
    @InspectableProperty
    public CharSequence getTextOff() {
        return mTextOff;
    }
    /**
     * Sets the text displayed when the button is not in the checked state.
     *
     * @attr ref android.R.styleable#Switch_textOff
     */
    @RemotableViewMethod
    public void setTextOff(CharSequence textOff) {
        mTextOff = textOff;
        requestLayout();
        // Default state is derived from on/off-text, so state has to be updated when on/off-text
        // are updated.
        setDefaultStateDescription();
    }
    /**
     * Sets whether the on/off text should be displayed.
     *
     * @param showText {@code true} to display on/off text
     * @attr ref android.R.styleable#Switch_showText
     */
    @RemotableViewMethod
    public void setShowText(boolean showText) {
        if (mShowText != showText) {
            mShowText = showText;
            requestLayout();
        }
    }
    /**
     * @return whether the on/off text should be displayed
     * @attr ref android.R.styleable#Switch_showText
     */
    @InspectableProperty
    public boolean getShowText() {
        return mShowText;
    }
    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mShowText) {
            if (mOnLayout == null) {
                mOnLayout = makeLayout(mTextOn);
            }
            if (mOffLayout == null) {
                mOffLayout = makeLayout(mTextOff);
            }
        }
        final Rect padding = mTempRect;
        final int thumbWidth;
        final int thumbHeight;
        if (mThumbDrawable != null) {
            // Cached thumb width does not include padding.
            mThumbDrawable.getPadding(padding);
            thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
            thumbHeight = mThumbDrawable.getIntrinsicHeight();
        } else {
            thumbWidth = 0;
            thumbHeight = 0;
        }
        final int maxTextWidth;
        if (mShowText) {
            maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
                    + mThumbTextPadding * 2;
        } else {
            maxTextWidth = 0;
        }
        mThumbWidth = Math.max(maxTextWidth, thumbWidth);
        final int trackHeight;
        if (mTrackDrawable != null) {
            mTrackDrawable.getPadding(padding);
            trackHeight = mTrackDrawable.getIntrinsicHeight();
        } else {
            padding.setEmpty();
            trackHeight = 0;
        }
        // Adjust left and right padding to ensure there's enough room for the
        // thumb's padding (when present).
        int paddingLeft = padding.left;
        int paddingRight = padding.right;
        if (mThumbDrawable != null) {
            final Insets inset = mThumbDrawable.getOpticalInsets();
            paddingLeft = Math.max(paddingLeft, inset.left);
            paddingRight = Math.max(paddingRight, inset.right);
        }
        final int switchWidth = Math.max(mSwitchMinWidth,
                2 * mThumbWidth + paddingLeft + paddingRight);
        final int switchHeight = Math.max(trackHeight, thumbHeight);
        mSwitchWidth = switchWidth;
        mSwitchHeight = switchHeight;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int measuredHeight = getMeasuredHeight();
        if (measuredHeight < switchHeight) {
            setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
        }
    }
    /** @hide */
    @Override
    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
        super.onPopulateAccessibilityEventInternal(event);
        final CharSequence text = isChecked() ? mTextOn : mTextOff;
        if (text != null) {
            event.getText().add(text);
        }
    }
    private Layout makeLayout(CharSequence text) {
        final CharSequence transformed = (mSwitchTransformationMethod != null)
                    ? mSwitchTransformationMethod.getTransformation(text, this)
                    : text;
        int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
                transformed.length(), mTextPaint, getTextDirectionHeuristic()));
        return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                .build();
    }
    /**
     * @return true if (x, y) is within the target area of the switch thumb
     */
    private boolean hitThumb(float x, float y) {
        if (mThumbDrawable == null) {
            return false;
        }
        // Relies on mTempRect, MUST be called first!
        final int thumbOffset = getThumbOffset();
        mThumbDrawable.getPadding(mTempRect);
        final int thumbTop = mSwitchTop - mTouchSlop;
        final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
        final int thumbRight = thumbLeft + mThumbWidth +
                mTempRect.left + mTempRect.right + mTouchSlop;
        final int thumbBottom = mSwitchBottom + mTouchSlop;
        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        final int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                if (isEnabled() && hitThumb(x, y)) {
                    mTouchMode = TOUCH_MODE_DOWN;
                    mTouchX = x;
                    mTouchY = y;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                switch (mTouchMode) {
                    case TOUCH_MODE_IDLE:
                        // Didn't target the thumb, treat normally.
                        break;
                    case TOUCH_MODE_DOWN: {
                        final float x = ev.getX();
                        final float y = ev.getY();
                        if (Math.abs(x - mTouchX) > mTouchSlop ||
                                Math.abs(y - mTouchY) > mTouchSlop) {
                            mTouchMode = TOUCH_MODE_DRAGGING;
                            getParent().requestDisallowInterceptTouchEvent(true);
                            mTouchX = x;
                            mTouchY = y;
                            return true;
                        }
                        break;
                    }
                    case TOUCH_MODE_DRAGGING: {
                        final float x = ev.getX();
                        final int thumbScrollRange = getThumbScrollRange();
                        final float thumbScrollOffset = x - mTouchX;
                        float dPos;
                        if (thumbScrollRange != 0) {
                            dPos = thumbScrollOffset / thumbScrollRange;
                        } else {
                            // If the thumb scroll range is empty, just use the
                            // movement direction to snap on or off.
                            dPos = thumbScrollOffset > 0 ? 1 : -1;
                        }
                        if (isLayoutRtl()) {
                            dPos = -dPos;
                        }
                        final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
                        if (newPos != mThumbPosition) {
                            mTouchX = x;
                            setThumbPosition(newPos);
                        }
                        return true;
                    }
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                if (mTouchMode == TOUCH_MODE_DRAGGING) {
                    stopDrag(ev);
                    // Allow super class to handle pressed state, etc.
                    super.onTouchEvent(ev);
                    return true;
                }
                mTouchMode = TOUCH_MODE_IDLE;
                mVelocityTracker.clear();
                break;
            }
        }
        return super.onTouchEvent(ev);
    }
    private void cancelSuperTouch(MotionEvent ev) {
        MotionEvent cancel = MotionEvent.obtain(ev);
        cancel.setAction(MotionEvent.ACTION_CANCEL);
        super.onTouchEvent(cancel);
        cancel.recycle();
    }
    /**
     * Called from onTouchEvent to end a drag operation.
     *
     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
     */
    private void stopDrag(MotionEvent ev) {
        mTouchMode = TOUCH_MODE_IDLE;
        // Commit the change if the event is up and not canceled and the switch
        // has not been disabled during the drag.
        final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
        final boolean oldState = isChecked();
        final boolean newState;
        if (commitChange) {
            mVelocityTracker.computeCurrentVelocity(1000);
            final float xvel = mVelocityTracker.getXVelocity();
            if (Math.abs(xvel) > mMinFlingVelocity) {
                newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
            } else {
                newState = getTargetCheckedState();
            }
        } else {
            newState = oldState;
        }
        if (newState != oldState) {
            playSoundEffect(SoundEffectConstants.CLICK);
        }
        // Always call setChecked so that the thumb is moved back to the correct edge
        setChecked(newState);
        cancelSuperTouch(ev);
    }
    private void animateThumbToCheckedState(boolean newCheckedState) {
        final float targetPosition = newCheckedState ? 1 : 0;
        mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
        mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
        mPositionAnimator.setAutoCancel(true);
        mPositionAnimator.start();
    }
    @UnsupportedAppUsage
    private void cancelPositionAnimator() {
        if (mPositionAnimator != null) {
            mPositionAnimator.cancel();
        }
    }
    private boolean getTargetCheckedState() {
        return mThumbPosition > 0.5f;
    }
    /**
     * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
     *
     * @param position new position between [0,1]
     */
    @UnsupportedAppUsage
    private void setThumbPosition(float position) {
        mThumbPosition = position;
        invalidate();
    }
    @Override
    public void toggle() {
        setChecked(!isChecked());
    }
    /** @hide **/
    @Override
    @NonNull
    protected CharSequence getButtonStateDescription() {
        if (isChecked()) {
            return mTextOn == null ? getResources().getString(R.string.capital_on) : mTextOn;
        } else {
            return mTextOff == null ? getResources().getString(R.string.capital_off) : mTextOff;
        }
    }
    @Override
    public void setChecked(boolean checked) {
        super.setChecked(checked);
        // Calling the super method may result in setChecked() getting called
        // recursively with a different value, so load the REAL value...
        checked = isChecked();
        if (isAttachedToWindow() && isLaidOut()) {
            animateThumbToCheckedState(checked);
        } else {
            // Immediately move the thumb to the new position.
            cancelPositionAnimator();
            setThumbPosition(checked ? 1 : 0);
        }
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        int opticalInsetLeft = 0;
        int opticalInsetRight = 0;
        if (mThumbDrawable != null) {
            final Rect trackPadding = mTempRect;
            if (mTrackDrawable != null) {
                mTrackDrawable.getPadding(trackPadding);
            } else {
                trackPadding.setEmpty();
            }
            final Insets insets = mThumbDrawable.getOpticalInsets();
            opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
            opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
        }
        final int switchRight;
        final int switchLeft;
        if (isLayoutRtl()) {
            switchLeft = getPaddingLeft() + opticalInsetLeft;
            switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
        } else {
            switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
            switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
        }
        final int switchTop;
        final int switchBottom;
        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
            default:
            case Gravity.TOP:
                switchTop = getPaddingTop();
                switchBottom = switchTop + mSwitchHeight;
                break;
            case Gravity.CENTER_VERTICAL:
                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
                        mSwitchHeight / 2;
                switchBottom = switchTop + mSwitchHeight;
                break;
            case Gravity.BOTTOM:
                switchBottom = getHeight() - getPaddingBottom();
                switchTop = switchBottom - mSwitchHeight;
                break;
        }
        mSwitchLeft = switchLeft;
        mSwitchTop = switchTop;
        mSwitchBottom = switchBottom;
        mSwitchRight = switchRight;
    }
    @Override
    public void draw(Canvas c) {
        final Rect padding = mTempRect;
        final int switchLeft = mSwitchLeft;
        final int switchTop = mSwitchTop;
        final int switchRight = mSwitchRight;
        final int switchBottom = mSwitchBottom;
        int thumbInitialLeft = switchLeft + getThumbOffset();
        final Insets thumbInsets;
        if (mThumbDrawable != null) {
            thumbInsets = mThumbDrawable.getOpticalInsets();
        } else {
            thumbInsets = Insets.NONE;
        }
        // Layout the track.
        if (mTrackDrawable != null) {
            mTrackDrawable.getPadding(padding);
            // Adjust thumb position for track padding.
            thumbInitialLeft += padding.left;
            // If necessary, offset by the optical insets of the thumb asset.
            int trackLeft = switchLeft;
            int trackTop = switchTop;
            int trackRight = switchRight;
            int trackBottom = switchBottom;
            if (thumbInsets != Insets.NONE) {
                if (thumbInsets.left > padding.left) {
                    trackLeft += thumbInsets.left - padding.left;
                }
                if (thumbInsets.top > padding.top) {
                    trackTop += thumbInsets.top - padding.top;
                }
                if (thumbInsets.right > padding.right) {
                    trackRight -= thumbInsets.right - padding.right;
                }
                if (thumbInsets.bottom > padding.bottom) {
                    trackBottom -= thumbInsets.bottom - padding.bottom;
                }
            }
            mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
        }
        // Layout the thumb.
        if (mThumbDrawable != null) {
            mThumbDrawable.getPadding(padding);
            final int thumbLeft = thumbInitialLeft - padding.left;
            final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
            mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
            final Drawable background = getBackground();
            if (background != null) {
                background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
            }
        }
        // Draw the background.
        super.draw(c);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final Rect padding = mTempRect;
        final Drawable trackDrawable = mTrackDrawable;
        if (trackDrawable != null) {
            trackDrawable.getPadding(padding);
        } else {
            padding.setEmpty();
        }
        final int switchTop = mSwitchTop;
        final int switchBottom = mSwitchBottom;
        final int switchInnerTop = switchTop + padding.top;
        final int switchInnerBottom = switchBottom - padding.bottom;
        final Drawable thumbDrawable = mThumbDrawable;
        if (trackDrawable != null) {
            if (mSplitTrack && thumbDrawable != null) {
                final Insets insets = thumbDrawable.getOpticalInsets();
                thumbDrawable.copyBounds(padding);
                padding.left += insets.left;
                padding.right -= insets.right;
                final int saveCount = canvas.save();
                canvas.clipRect(padding, Op.DIFFERENCE);
                trackDrawable.draw(canvas);
                canvas.restoreToCount(saveCount);
            } else {
                trackDrawable.draw(canvas);
            }
        }
        final int saveCount = canvas.save();
        if (thumbDrawable != null) {
            thumbDrawable.draw(canvas);
        }
        final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
        if (switchText != null) {
            final int drawableState[] = getDrawableState();
            if (mTextColors != null) {
                mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
            }
            mTextPaint.drawableState = drawableState;
            final int cX;
            if (thumbDrawable != null) {
                final Rect bounds = thumbDrawable.getBounds();
                cX = bounds.left + bounds.right;
            } else {
                cX = getWidth();
            }
            final int left = cX / 2 - switchText.getWidth() / 2;
            final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
            canvas.translate(left, top);
            switchText.draw(canvas);
        }
        canvas.restoreToCount(saveCount);
    }
    @Override
    public int getCompoundPaddingLeft() {
        if (!isLayoutRtl()) {
            return super.getCompoundPaddingLeft();
        }
        int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
        if (!TextUtils.isEmpty(getText())) {
            padding += mSwitchPadding;
        }
        return padding;
    }
    @Override
    public int getCompoundPaddingRight() {
        if (isLayoutRtl()) {
            return super.getCompoundPaddingRight();
        }
        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
        if (!TextUtils.isEmpty(getText())) {
            padding += mSwitchPadding;
        }
        return padding;
    }
    /**
     * Translates thumb position to offset according to current RTL setting and
     * thumb scroll range. Accounts for both track and thumb padding.
     *
     * @return thumb offset
     */
    private int getThumbOffset() {
        final float thumbPosition;
        if (isLayoutRtl()) {
            thumbPosition = 1 - mThumbPosition;
        } else {
            thumbPosition = mThumbPosition;
        }
        return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
    }
    private int getThumbScrollRange() {
        if (mTrackDrawable != null) {
            final Rect padding = mTempRect;
            mTrackDrawable.getPadding(padding);
            final Insets insets;
            if (mThumbDrawable != null) {
                insets = mThumbDrawable.getOpticalInsets();
            } else {
                insets = Insets.NONE;
            }
            return mSwitchWidth - mThumbWidth - padding.left - padding.right
                    - insets.left - insets.right;
        } else {
            return 0;
        }
    }
    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
        }
        return drawableState;
    }
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        final int[] state = getDrawableState();
        boolean changed = false;
        final Drawable thumbDrawable = mThumbDrawable;
        if (thumbDrawable != null && thumbDrawable.isStateful()) {
            changed |= thumbDrawable.setState(state);
        }
        final Drawable trackDrawable = mTrackDrawable;
        if (trackDrawable != null && trackDrawable.isStateful()) {
            changed |= trackDrawable.setState(state);
        }
        if (changed) {
            invalidate();
        }
    }
    @Override
    public void drawableHotspotChanged(float x, float y) {
        super.drawableHotspotChanged(x, y);
        if (mThumbDrawable != null) {
            mThumbDrawable.setHotspot(x, y);
        }
        if (mTrackDrawable != null) {
            mTrackDrawable.setHotspot(x, y);
        }
    }
    @Override
    protected boolean verifyDrawable(@NonNull Drawable who) {
        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
    }
    @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();
        if (mThumbDrawable != null) {
            mThumbDrawable.jumpToCurrentState();
        }
        if (mTrackDrawable != null) {
            mTrackDrawable.jumpToCurrentState();
        }
        if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
            mPositionAnimator.end();
            mPositionAnimator = null;
        }
    }
    @Override
    public CharSequence getAccessibilityClassName() {
        return Switch.class.getName();
    }
    /** @hide */
    @Override
    protected void onProvideStructure(@NonNull ViewStructure structure,
            @ViewStructureType int viewFor, int flags) {
        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
        if (!TextUtils.isEmpty(switchText)) {
            CharSequence oldText = structure.getText();
            if (TextUtils.isEmpty(oldText)) {
                structure.setText(switchText);
            } else {
                StringBuilder newText = new StringBuilder();
                newText.append(oldText).append(' ').append(switchText);
                structure.setText(newText);
            }
            // The style of the label text is provided via the base TextView class. This is more
            // relevant than the style of the (optional) on/off text on the switch button itself,
            // so ignore the size/color/style stored this.mTextPaint.
        }
    }
    private static final FloatProperty