请不要问,为什么这一次突然说到switch了,我也不知道,可能是看到前面的文章了吧, android4.0新控件Switch方法解析,然后今天就看下switch的源码。
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 text property controls the text displayed in the label for the switch, whereas the off and on text controls the text on the thumb. Similarly, the textAppearance and the related setTypeface() methods control the typeface and style of label text, whereas the switchTextAppearance and the related seSwitchTypeface() methods control that of the thumb. See the Toggle Buttons guide.
public class Switch extends CompoundButton { 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; private Drawable mThumbDrawable; private Drawable mTrackDrawable; private int mThumbTextPadding; private int mSwitchMinWidth; private int mSwitchPadding; private CharSequence mTextOn; private CharSequence mTextOff; private int mTouchMode; private int mTouchSlop; private float mTouchX; private float mTouchY; private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); private int mMinFlingVelocity; private float mThumbPosition; private int mSwitchWidth; private int mSwitchHeight; private int mThumbWidth; // Does not include padding private int mSwitchLeft; private int mSwitchTop; private int mSwitchRight; private int mSwitchBottom; private TextPaint mTextPaint; private ColorStateList mTextColors; private Layout mOnLayout; private Layout mOffLayout; @SuppressWarnings("hiding") private final Rect mTempRect = new Rect(); private static final int[] CHECKED_STATE_SET = { R.attr.state_checked };switch类继承自CompoundButton,好吧,这个明天看一下。变量具体含义在下面方法里具体说明。
/** * 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 defStyle An attribute ID within the active theme containing a reference to the * default style for this widget. e.g. android.R.attr.switchStyle. */ public Switch(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); Resources res = getResources(); mTextPaint.density = res.getDisplayMetrics().density; mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Switch, defStyle, 0); mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn); mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff); 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); int appearance = a.getResourceId( com.android.internal.R.styleable.Switch_switchTextAppearance, 0); if (appearance != 0) { setSwitchTextAppearance(context, appearance); } a.recycle(); ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); // Refresh display with current params refreshDrawableState(); setChecked(isChecked()); }第一个和第二个构造方法实际上都是调用的第三个构造方法,但是关于AttributeSet的具体使用,这里也不详细叙,不了解的可以看一下android自定义视图属性(atts.xml,TypedArray)学习。
/** * 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, 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); appearance.recycle(); }关于TypeArray的学习,请参考android自定义视图属性(atts.xml,TypedArray)学习。
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); }为什么要这样设计呢,在android自定义视图属性(atts.xml,TypedArray)学习里讲到,attrs.xml里面format格式就那几种,存不了复杂内容格式。
/** * 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); } }设置在switch上面显示字体和风格,如果没有指定字体风格,字体会采用默认的倾斜加粗。
/** * 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(); } }设置在switch上面显示的字体格式,应该注意并不是所有的字体集都含有粗体和倾斜,所以当需要时,可以调用setSwitchTypeface来设置自己的字体风格。
* 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(); }
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (mOnLayout == null) { mOnLayout = makeLayout(mTextOn); } if (mOffLayout == null) { mOffLayout = makeLayout(mTextOff); } mTrackDrawable.getPadding(mTempRect); final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()); final int switchWidth = Math.max(mSwitchMinWidth, maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); final int switchHeight = mTrackDrawable.getIntrinsicHeight(); mThumbWidth = maxTextWidth + mThumbTextPadding * 2; switch (widthMode) { case MeasureSpec.AT_MOST: widthSize = Math.min(widthSize, switchWidth); break; case MeasureSpec.UNSPECIFIED: widthSize = switchWidth; break; case MeasureSpec.EXACTLY: // Just use what we were given break; } switch (heightMode) { case MeasureSpec.AT_MOST: heightSize = Math.min(heightSize, switchHeight); break; case MeasureSpec.UNSPECIFIED: heightSize = switchHeight; break; case MeasureSpec.EXACTLY: // Just use what we were given break; } mSwitchWidth = switchWidth; mSwitchHeight = switchHeight; super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int measuredHeight = getMeasuredHeight(); if (measuredHeight < switchHeight) { setMeasuredDimension(getMeasuredWidthAndState(), switchHeight); } }
private Layout makeLayout(CharSequence text) { return new StaticLayout(text, mTextPaint, (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)), Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); }如果mOnLayout活着mOffLayout为空,就返回一个Layout。
@Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); if (isChecked()) { CharSequence text = mOnLayout.getText(); if (TextUtils.isEmpty(text)) { text = mContext.getString(R.string.switch_on); } event.getText().add(text); } else { CharSequence text = mOffLayout.getText(); if (TextUtils.isEmpty(text)) { text = mContext.getString(R.string.switch_off); } event.getText().add(text); } }
@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 float dx = x - mTouchX; float newPos = Math.max(0, Math.min(mThumbPosition + dx, getThumbScrollRange())); if (newPos != mThumbPosition) { mThumbPosition = newPos; mTouchX = x; invalidate(); } return true; } } break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mTouchMode == TOUCH_MODE_DRAGGING) { stopDrag(ev); return true; } mTouchMode = TOUCH_MODE_IDLE; mVelocityTracker.clear(); break; } } return super.onTouchEvent(ev); }
/** * @return true if (x, y) is within the target area of the switch thumb */ private boolean hitThumb(float x, float y) { mThumbDrawable.getPadding(mTempRect); final int thumbTop = mSwitchTop - mTouchSlop; final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - 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; }如果是,把mTouchMode 设为TOUCH_MODE_DOWN。
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;
private int getThumbScrollRange() { if (mTrackDrawable == null) { return 0; } mTrackDrawable.getPadding(mTempRect); return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right; }
/** * 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; // Up and not canceled, also checks the switch has not been disabled during the drag boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); cancelSuperTouch(ev); if (commitChange) { boolean newState; mVelocityTracker.computeCurrentVelocity(1000); float xvel = mVelocityTracker.getXVelocity(); if (Math.abs(xvel) > mMinFlingVelocity) { newState = xvel > 0; } else { newState = getTargetCheckedState(); } animateThumbToCheckedState(newState); } else { animateThumbToCheckedState(isChecked()); } }
private void cancelSuperTouch(MotionEvent ev) { MotionEvent cancel = MotionEvent.obtain(ev); cancel.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(cancel); cancel.recycle(); } if (commitChange) { boolean newState; mVelocityTracker.computeCurrentVelocity(1000); float xvel = mVelocityTracker.getXVelocity(); if (Math.abs(xvel) > mMinFlingVelocity) { newState = xvel > 0; } else { newState = getTargetCheckedState(); } animateThumbToCheckedState(newState); } else { animateThumbToCheckedState(isChecked()); }
private void animateThumbToCheckedState(boolean newCheckedState) { // TODO animate! //float targetPos = newCheckedState ? 0 : getThumbScrollRange(); //mThumbPosition = targetPos; setChecked(newCheckedState); } private boolean getTargetCheckedState() { return mThumbPosition >= getThumbScrollRange() / 2; } @Override public void setChecked(boolean checked) { super.setChecked(checked); mThumbPosition = checked ? getThumbScrollRange() : 0; invalidate(); }
Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mThumbPosition = isChecked() ? getThumbScrollRange() : 0; int switchRight = getWidth() - getPaddingRight(); int switchLeft = switchRight - mSwitchWidth; int switchTop = 0; int switchBottom = 0; 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 protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the switch int switchLeft = mSwitchLeft; int switchTop = mSwitchTop; int switchRight = mSwitchRight; int switchBottom = mSwitchBottom; mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); mTrackDrawable.draw(canvas); canvas.save(); mTrackDrawable.getPadding(mTempRect); int switchInnerLeft = switchLeft + mTempRect.left; int switchInnerTop = switchTop + mTempRect.top; int switchInnerRight = switchRight - mTempRect.right; int switchInnerBottom = switchBottom - mTempRect.bottom; canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); mThumbDrawable.getPadding(mTempRect); final int thumbPos = (int) (mThumbPosition + 0.5f); int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); mThumbDrawable.draw(canvas); // mTextColors should not be null, but just in case if (mTextColors != null) { mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(), mTextColors.getDefaultColor())); } mTextPaint.drawableState = getDrawableState(); Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2, (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2); switchText.draw(canvas); canvas.restore(); }获取控件距离右边距距离:
@Override public int getCompoundPaddingRight() { int padding = super.getCompoundPaddingRight() + mSwitchWidth; if (!TextUtils.isEmpty(getText())) { padding += mSwitchPadding; } return padding; }下面四个方法是
@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(); int[] myDrawableState = getDrawableState(); // Set the state of the Drawable // Drawable may be null when checked state is set from XML, from super constructor if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState); if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState); invalidate(); } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; } @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); mThumbDrawable.jumpToCurrentState(); mTrackDrawable.jumpToCurrentState(); } }
初稿完成!终于可以吃饭了。。。
查看更多源码内容:Android源码解析!