1151 lines
No EOL
39 KiB
Java
1151 lines
No EOL
39 KiB
Java
/*
|
|
* Copyright (C) 2014 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.support.v7.widget;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.content.res.ColorStateList;
|
|
import android.content.res.Resources;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.graphics.Region;
|
|
import android.graphics.Typeface;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Build;
|
|
import android.support.v4.graphics.drawable.DrawableCompat;
|
|
import android.support.v4.view.MotionEventCompat;
|
|
import android.support.v4.view.ViewCompat;
|
|
import android.support.v7.appcompat.R;
|
|
import android.support.v7.internal.text.AllCapsTransformationMethod;
|
|
import android.support.v7.internal.widget.DrawableUtils;
|
|
import android.support.v7.internal.widget.TintManager;
|
|
import android.support.v7.internal.widget.TintTypedArray;
|
|
import android.support.v7.internal.widget.ViewUtils;
|
|
import android.text.Layout;
|
|
import android.text.StaticLayout;
|
|
import android.text.TextPaint;
|
|
import android.text.TextUtils;
|
|
import android.text.method.TransformationMethod;
|
|
import android.util.AttributeSet;
|
|
import android.view.Gravity;
|
|
import android.view.MotionEvent;
|
|
import android.view.SoundEffectConstants;
|
|
import android.view.VelocityTracker;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.view.animation.Animation;
|
|
import android.view.animation.Transformation;
|
|
import android.widget.CompoundButton;
|
|
|
|
/**
|
|
* SwitchCompat is a version of the Switch widget which on devices back to API v7. It does not
|
|
* make any attempt to use the platform provided widget on those devices which it is available
|
|
* normally.
|
|
* <p>
|
|
* A Switch is a two-state toggle switch widget that can select between two
|
|
* options. The user may drag the "thumb" back and forth to choose the selected option,
|
|
* or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
|
|
* property controls the text displayed in the label for the switch, whereas the
|
|
* {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
|
|
* controls the text on the thumb. Similarly, the
|
|
* {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
|
|
* setTypeface() methods control the typeface and style of label text, whereas the
|
|
* {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
|
|
* the related seSwitchTypeface() methods control that of the thumb.
|
|
*/
|
|
public class SwitchCompat 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;
|
|
|
|
// We force the accessibility events to have a class name of Switch, since screen readers
|
|
// already know how to handle their events
|
|
private static final String ACCESSIBILITY_EVENT_CLASS_NAME = "android.widget.Switch";
|
|
|
|
// Enum for the "typeface" XML parameter.
|
|
private static final int SANS = 1;
|
|
private static final int SERIF = 2;
|
|
private static final int MONOSPACE = 3;
|
|
|
|
private Drawable mThumbDrawable;
|
|
private Drawable mTrackDrawable;
|
|
private int mThumbTextPadding;
|
|
private int mSwitchMinWidth;
|
|
private int mSwitchPadding;
|
|
private boolean mSplitTrack;
|
|
private CharSequence mTextOn;
|
|
private CharSequence mTextOff;
|
|
private boolean mShowText;
|
|
|
|
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.
|
|
*/
|
|
private int mSwitchWidth;
|
|
|
|
/**
|
|
* Height required to draw the switch track and thumb. Includes padding and
|
|
* optical bounds for both the track and thumb.
|
|
*/
|
|
private int mSwitchHeight;
|
|
|
|
/**
|
|
* Width of the thumb's content region. Does not include padding or
|
|
* optical bounds.
|
|
*/
|
|
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;
|
|
private Layout mOnLayout;
|
|
private Layout mOffLayout;
|
|
private TransformationMethod mSwitchTransformationMethod;
|
|
private Animation mPositionAnimator;
|
|
|
|
@SuppressWarnings("hiding")
|
|
private final Rect mTempRect = new Rect();
|
|
|
|
private final TintManager mTintManager;
|
|
|
|
private static final int[] CHECKED_STATE_SET = {
|
|
android.R.attr.state_checked
|
|
};
|
|
|
|
/**
|
|
* Construct a new Switch with default styling.
|
|
*
|
|
* @param context The Context that will determine this widget's theming.
|
|
*/
|
|
public SwitchCompat(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 SwitchCompat(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 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 SwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
|
|
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
|
|
|
final Resources res = getResources();
|
|
mTextPaint.density = res.getDisplayMetrics().density;
|
|
|
|
final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,
|
|
attrs, R.styleable.SwitchCompat, defStyleAttr, 0);
|
|
mThumbDrawable = a.getDrawable(R.styleable.SwitchCompat_android_thumb);
|
|
if (mThumbDrawable != null) {
|
|
mThumbDrawable.setCallback(this);
|
|
}
|
|
mTrackDrawable = a.getDrawable(R.styleable.SwitchCompat_track);
|
|
if (mTrackDrawable != null) {
|
|
mTrackDrawable.setCallback(this);
|
|
}
|
|
mTextOn = a.getText(R.styleable.SwitchCompat_android_textOn);
|
|
mTextOff = a.getText(R.styleable.SwitchCompat_android_textOff);
|
|
mShowText = a.getBoolean(R.styleable.SwitchCompat_showText, true);
|
|
mThumbTextPadding = a.getDimensionPixelSize(
|
|
R.styleable.SwitchCompat_thumbTextPadding, 0);
|
|
mSwitchMinWidth = a.getDimensionPixelSize(
|
|
R.styleable.SwitchCompat_switchMinWidth, 0);
|
|
mSwitchPadding = a.getDimensionPixelSize(
|
|
R.styleable.SwitchCompat_switchPadding, 0);
|
|
mSplitTrack = a.getBoolean(R.styleable.SwitchCompat_splitTrack, false);
|
|
|
|
final int appearance = a.getResourceId(
|
|
R.styleable.SwitchCompat_switchTextAppearance, 0);
|
|
if (appearance != 0) {
|
|
setSwitchTextAppearance(context, appearance);
|
|
}
|
|
|
|
mTintManager = a.getTintManager();
|
|
|
|
a.recycle();
|
|
|
|
final ViewConfiguration config = ViewConfiguration.get(context);
|
|
mTouchSlop = config.getScaledTouchSlop();
|
|
mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
|
|
|
|
// Refresh display with current params
|
|
refreshDrawableState();
|
|
setChecked(isChecked());
|
|
}
|
|
|
|
/**
|
|
* Sets the switch text color, size, style, hint color, and highlight color
|
|
* from the specified TextAppearance resource.
|
|
*/
|
|
public void setSwitchTextAppearance(Context context, int resid) {
|
|
TypedArray appearance = context.obtainStyledAttributes(resid, R.styleable.TextAppearance);
|
|
|
|
ColorStateList colors;
|
|
int ts;
|
|
|
|
colors = appearance.getColorStateList(R.styleable.TextAppearance_android_textColor);
|
|
if (colors != null) {
|
|
mTextColors = colors;
|
|
} else {
|
|
// If no color set in TextAppearance, default to the view's textColor
|
|
mTextColors = getTextColors();
|
|
}
|
|
|
|
ts = appearance.getDimensionPixelSize(R.styleable.TextAppearance_android_textSize, 0);
|
|
if (ts != 0) {
|
|
if (ts != mTextPaint.getTextSize()) {
|
|
mTextPaint.setTextSize(ts);
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
int typefaceIndex, styleIndex;
|
|
typefaceIndex = appearance.getInt(R.styleable.TextAppearance_android_typeface, -1);
|
|
styleIndex = appearance.getInt(R.styleable.TextAppearance_android_textStyle, -1);
|
|
|
|
setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
|
|
|
|
boolean allCaps = appearance.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
|
|
if (allCaps) {
|
|
mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
|
|
} 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.
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
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
|
|
*/
|
|
public int getThumbTextPadding() {
|
|
return mThumbTextPadding;
|
|
}
|
|
|
|
/**
|
|
* Set the drawable used for the track that the switch slides within.
|
|
*
|
|
* @param track Track drawable
|
|
*/
|
|
public void setTrackDrawable(Drawable track) {
|
|
mTrackDrawable = track;
|
|
requestLayout();
|
|
}
|
|
|
|
/**
|
|
* Set the drawable used for the track that the switch slides within.
|
|
*
|
|
* @param resId Resource ID of a track drawable
|
|
*/
|
|
public void setTrackResource(int resId) {
|
|
setTrackDrawable(mTintManager.getDrawable(resId));
|
|
}
|
|
|
|
/**
|
|
* Get the drawable used for the track that the switch slides within.
|
|
*
|
|
* @return Track drawable
|
|
*/
|
|
public Drawable getTrackDrawable() {
|
|
return mTrackDrawable;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public void setThumbDrawable(Drawable thumb) {
|
|
mThumbDrawable = thumb;
|
|
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
|
|
*/
|
|
public void setThumbResource(int resId) {
|
|
setThumbDrawable(mTintManager.getDrawable(resId));
|
|
}
|
|
|
|
/**
|
|
* Get the drawable used for the switch "thumb" - the piece that the user
|
|
* can physically touch and drag along the track.
|
|
*
|
|
* @return Thumb drawable
|
|
*/
|
|
public Drawable getThumbDrawable() {
|
|
return mThumbDrawable;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public void setSplitTrack(boolean splitTrack) {
|
|
mSplitTrack = splitTrack;
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Returns whether the track should be split by the thumb.
|
|
*/
|
|
public boolean getSplitTrack() {
|
|
return mSplitTrack;
|
|
}
|
|
|
|
/**
|
|
* Returns the text displayed when the button is in the checked state.
|
|
*/
|
|
public CharSequence getTextOn() {
|
|
return mTextOn;
|
|
}
|
|
|
|
/**
|
|
* Sets the text displayed when the button is in the checked state.
|
|
*/
|
|
public void setTextOn(CharSequence textOn) {
|
|
mTextOn = textOn;
|
|
requestLayout();
|
|
}
|
|
|
|
/**
|
|
* Returns the text displayed when the button is not in the checked state.
|
|
*/
|
|
public CharSequence getTextOff() {
|
|
return mTextOff;
|
|
}
|
|
|
|
/**
|
|
* Sets the text displayed when the button is not in the checked state.
|
|
*/
|
|
public void setTextOff(CharSequence textOff) {
|
|
mTextOff = textOff;
|
|
requestLayout();
|
|
}
|
|
|
|
/**
|
|
* Sets whether the on/off text should be displayed.
|
|
*
|
|
* @param showText {@code true} to display on/off text
|
|
*/
|
|
public void setShowText(boolean showText) {
|
|
if (mShowText != showText) {
|
|
mShowText = showText;
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return whether the on/off text should be displayed
|
|
*/
|
|
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 Rect inset = DrawableUtils.getOpticalBounds(mThumbDrawable);
|
|
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(ViewCompat.getMeasuredWidthAndState(this), switchHeight);
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
@Override
|
|
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
|
|
super.onPopulateAccessibilityEvent(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;
|
|
|
|
return new StaticLayout(transformed, mTextPaint,
|
|
transformed != null ?
|
|
(int) Math.ceil(Layout.getDesiredWidth(transformed, mTextPaint)) : 0,
|
|
Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true);
|
|
}
|
|
|
|
/**
|
|
* @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 = MotionEventCompat.getActionMasked(ev);
|
|
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 (ViewUtils.isLayoutRtl(this)) {
|
|
dPos = -dPos;
|
|
}
|
|
final float newPos = 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 = ViewUtils.isLayoutRtl(this) ? (xvel < 0) : (xvel > 0);
|
|
} else {
|
|
newState = getTargetCheckedState();
|
|
}
|
|
} else {
|
|
newState = oldState;
|
|
}
|
|
|
|
if (newState != oldState) {
|
|
playSoundEffect(SoundEffectConstants.CLICK);
|
|
setChecked(newState);
|
|
}
|
|
cancelSuperTouch(ev);
|
|
}
|
|
|
|
private void animateThumbToCheckedState(boolean newCheckedState) {
|
|
final float startPosition = mThumbPosition;
|
|
final float targetPosition = newCheckedState ? 1 : 0;
|
|
final float diff = targetPosition - startPosition;
|
|
|
|
mPositionAnimator = new Animation() {
|
|
@Override
|
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
setThumbPosition(startPosition + (diff * interpolatedTime));
|
|
}
|
|
};
|
|
mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
|
|
startAnimation(mPositionAnimator);
|
|
}
|
|
|
|
private void cancelPositionAnimator() {
|
|
if (mPositionAnimator != null) {
|
|
clearAnimation();
|
|
mPositionAnimator = null;
|
|
}
|
|
}
|
|
|
|
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]
|
|
*/
|
|
private void setThumbPosition(float position) {
|
|
mThumbPosition = position;
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
public void toggle() {
|
|
setChecked(!isChecked());
|
|
}
|
|
|
|
@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 (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
|
|
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 Rect insets = DrawableUtils.getOpticalBounds(mThumbDrawable);
|
|
opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
|
|
opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
|
|
}
|
|
|
|
final int switchRight;
|
|
final int switchLeft;
|
|
if (ViewUtils.isLayoutRtl(this)) {
|
|
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 Rect thumbInsets;
|
|
if (mThumbDrawable != null) {
|
|
thumbInsets = DrawableUtils.getOpticalBounds(mThumbDrawable);
|
|
} else {
|
|
thumbInsets = DrawableUtils.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 != null && !thumbInsets.isEmpty()) {
|
|
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) {
|
|
DrawableCompat.setHotspotBounds(background, 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 Rect insets = DrawableUtils.getOpticalBounds(thumbDrawable);
|
|
thumbDrawable.copyBounds(padding);
|
|
padding.left += insets.left;
|
|
padding.right -= insets.right;
|
|
|
|
final int saveCount = canvas.save();
|
|
canvas.clipRect(padding, Region.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 (!ViewUtils.isLayoutRtl(this)) {
|
|
return super.getCompoundPaddingLeft();
|
|
}
|
|
int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
|
|
if (!TextUtils.isEmpty(getText())) {
|
|
padding += mSwitchPadding;
|
|
}
|
|
return padding;
|
|
}
|
|
|
|
@Override
|
|
public int getCompoundPaddingRight() {
|
|
if (ViewUtils.isLayoutRtl(this)) {
|
|
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 (ViewUtils.isLayoutRtl(this)) {
|
|
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 Rect insets;
|
|
if (mThumbDrawable != null) {
|
|
insets = DrawableUtils.getOpticalBounds(mThumbDrawable);
|
|
} else {
|
|
insets = DrawableUtils.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[] myDrawableState = getDrawableState();
|
|
|
|
if (mThumbDrawable != null) {
|
|
mThumbDrawable.setState(myDrawableState);
|
|
}
|
|
|
|
if (mTrackDrawable != null) {
|
|
mTrackDrawable.setState(myDrawableState);
|
|
}
|
|
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
public void drawableHotspotChanged(float x, float y) {
|
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
super.drawableHotspotChanged(x, y);
|
|
}
|
|
|
|
if (mThumbDrawable != null) {
|
|
DrawableCompat.setHotspot(mThumbDrawable, x, y);
|
|
}
|
|
|
|
if (mTrackDrawable != null) {
|
|
DrawableCompat.setHotspot(mTrackDrawable, x, y);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean verifyDrawable(Drawable who) {
|
|
return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
|
|
}
|
|
|
|
@Override
|
|
public void jumpDrawablesToCurrentState() {
|
|
if (Build.VERSION.SDK_INT >= 11) {
|
|
super.jumpDrawablesToCurrentState();
|
|
|
|
if (mThumbDrawable != null) {
|
|
mThumbDrawable.jumpToCurrentState();
|
|
}
|
|
|
|
if (mTrackDrawable != null) {
|
|
mTrackDrawable.jumpToCurrentState();
|
|
}
|
|
|
|
if (mPositionAnimator != null && !mPositionAnimator.hasEnded()) {
|
|
clearAnimation();
|
|
mPositionAnimator = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
@Override
|
|
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
|
super.onInitializeAccessibilityEvent(event);
|
|
event.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
|
|
}
|
|
|
|
@Override
|
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
|
if (Build.VERSION.SDK_INT >= 14) {
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
|
|
CharSequence switchText = isChecked() ? mTextOn : mTextOff;
|
|
if (!TextUtils.isEmpty(switchText)) {
|
|
CharSequence oldText = info.getText();
|
|
if (TextUtils.isEmpty(oldText)) {
|
|
info.setText(switchText);
|
|
} else {
|
|
StringBuilder newText = new StringBuilder();
|
|
newText.append(oldText).append(' ').append(switchText);
|
|
info.setText(newText);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Taken from android.util.MathUtils
|
|
*/
|
|
private static float constrain(float amount, float low, float high) {
|
|
return amount < low ? low : (amount > high ? high : amount);
|
|
}
|
|
} |