/* * The MIT License (MIT) * * Copyright (c) 2014-2015 Umeng, Inc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.umeng.comm.ui.widgets; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableContainer; import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.CompoundButton; import android.widget.Scroller; import com.umeng.comm.core.utils.ResFinder; /** * 开关按钮 */ public class SwitchButton 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; private int buttonLeft; // 按钮在画布上的X坐标 private int buttonTop; // 按钮在画布上的Y坐标 private int tempSlideX = 0; // X轴当前坐标,用于动态绘制图片显示坐标,实现滑动效果 private int tempMinSlideX = 0; // X轴最小坐标,用于防止往左边滑动时超出范围 private int tempMaxSlideX = 0; // X轴最大坐标,用于防止往右边滑动时超出范围 private int tempTotalSlideDistance; // 滑动距离,用于记录每次滑动的距离,在滑动结束后根据距离判断是否切换状态或者回滚 private int duration = 200; // 动画持续时间 private int touchMode; // 触摸模式,用来在处理滑动事件的时候区分操作 private int touchSlop; private int withTextInterval = 16; // 文字和按钮之间的间距 private float touchX; // 记录上次触摸坐标,用于计算滑动距离 private float minChangeDistanceScale = 0.2f; // 有效距离比例,例如按钮宽度为100,比例为0.3,那么只有当滑动距离大于等于(100*0.3)才会切换状态,否则就回滚 private Paint paint; // 画笔,用来绘制遮罩效果 private RectF buttonRectF; // 按钮的位置 private Drawable frameDrawable; // 框架层图片 private Drawable stateDrawable; // 状态图片 private Drawable stateMaskDrawable; // 状态遮罩图片 private Drawable sliderDrawable; // 滑块图片 private SwitchScroller switchScroller; // 切换滚动器,用于实现平滑滚动效果 private PorterDuffXfermode porterDuffXfermode;// 遮罩类型 public SwitchButton(Context context) { this(context, null); } public SwitchButton(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public SwitchButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } /** * 初始化 * * @param attrs 属性 */ private void init(AttributeSet attrs) { setGravity(Gravity.CENTER_VERTICAL); paint = new Paint(); paint.setColor(Color.RED); porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); switchScroller = new SwitchScroller(getContext(), new AccelerateDecelerateInterpolator()); buttonRectF = new RectF(); setDrawables(); ViewConfiguration config = ViewConfiguration.get(getContext()); touchSlop = config.getScaledTouchSlop(); setChecked(isChecked()); setClickable(true); // 设置允许点击,当用户点击在按钮其它区域的时候就会切换状态 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 计算宽度 int measureWidth; switch (MeasureSpec.getMode(widthMeasureSpec)) { case MeasureSpec.AT_MOST:// 如果widthSize是当前视图可使用的最大宽度 measureWidth = getCompoundPaddingLeft() + getCompoundPaddingRight(); break; case MeasureSpec.EXACTLY:// 如果widthSize是当前视图可使用的绝对宽度 measureWidth = MeasureSpec.getSize(widthMeasureSpec); break; case MeasureSpec.UNSPECIFIED:// 如果widthSize对当前视图宽度的计算没有任何参考意义 measureWidth = getCompoundPaddingLeft() + getCompoundPaddingRight(); break; default: measureWidth = getCompoundPaddingLeft() + getCompoundPaddingRight(); break; } // 计算高度 int measureHeight; switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST:// 如果heightSize是当前视图可使用的最大宽度 measureHeight = (frameDrawable != null ? frameDrawable.getIntrinsicHeight() : 0) + getCompoundPaddingTop() + getCompoundPaddingBottom(); break; case MeasureSpec.EXACTLY:// 如果heightSize是当前视图可使用的绝对宽度 measureHeight = MeasureSpec.getSize(heightMeasureSpec); break; case MeasureSpec.UNSPECIFIED:// 如果heightSize对当前视图宽度的计算没有任何参考意义 measureHeight = (frameDrawable != null ? frameDrawable.getIntrinsicHeight() : 0) + getCompoundPaddingTop() + getCompoundPaddingBottom(); break; default: measureHeight = (frameDrawable != null ? frameDrawable.getIntrinsicHeight() : 0) + getCompoundPaddingTop() + getCompoundPaddingBottom(); break; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (measureWidth < getMeasuredWidth()) { measureWidth = getMeasuredWidth(); } if (measureHeight < getMeasuredHeight()) { measureHeight = getMeasuredHeight(); } setMeasuredDimension(measureWidth, measureHeight); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); Drawable[] drawables = getCompoundDrawables(); int drawableRightWidth = 0; int drawableTopHeight = 0; int drawableBottomHeight = 0; if (drawables != null) { if (drawables.length > 1 && drawables[1] != null) { drawableTopHeight = drawables[1].getIntrinsicHeight() + getCompoundDrawablePadding(); } if (drawables.length > 2 && drawables[2] != null) { drawableRightWidth = drawables[2].getIntrinsicWidth() + getCompoundDrawablePadding(); } if (drawables.length > 3 && drawables[3] != null) { drawableBottomHeight = drawables[3].getIntrinsicHeight() + getCompoundDrawablePadding(); } } buttonLeft = (getWidth() - (frameDrawable != null ? frameDrawable.getIntrinsicWidth() : 0) - getPaddingRight() - drawableRightWidth); buttonTop = (getHeight() - (frameDrawable != null ? frameDrawable.getIntrinsicHeight() : 0) + drawableTopHeight - drawableBottomHeight) / 2; buttonRectF.set(buttonLeft, buttonTop, buttonLeft + (frameDrawable != null ? frameDrawable.getIntrinsicWidth() : 0), buttonTop + (frameDrawable != null ? frameDrawable.getIntrinsicHeight() : 0)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 保存图层并全体偏移,让paddingTop和paddingLeft生效 canvas.save(); canvas.translate(buttonLeft, buttonTop); // 绘制状态层 if (stateDrawable != null && stateMaskDrawable != null) { Bitmap stateBitmap = getBitmapFromDrawable(stateDrawable); if (stateMaskDrawable != null && stateBitmap != null && !stateBitmap.isRecycled()) { // 保存并创建一个新的透明层,如果不这样做的话,画出来的背景会是黑的 int src = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); // 绘制遮罩层 stateMaskDrawable.draw(canvas); // 绘制状态图片按并应用遮罩效果 paint.setXfermode(porterDuffXfermode); canvas.drawBitmap(stateBitmap, tempSlideX, 0, paint); paint.setXfermode(null); // 融合图层 canvas.restoreToCount(src); } } // 绘制框架层 if (frameDrawable != null) { frameDrawable.draw(canvas); } // 绘制滑块层 if (sliderDrawable != null) { Bitmap sliderBitmap = getBitmapFromDrawable(sliderDrawable); if (sliderBitmap != null && !sliderBitmap.isRecycled()) { canvas.drawBitmap(sliderBitmap, tempSlideX, 0, paint); } } // 融合图层 canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { // 如果按钮当前可用并且按下位置正好在按钮之内 if (isEnabled() && buttonRectF.contains(event.getX(), event.getY())) { touchMode = TOUCH_MODE_DOWN; tempTotalSlideDistance = 0; // 清空总滑动距离 touchX = event.getX(); // 记录X轴坐标 setClickable(false); // 当用户触摸在按钮位置的时候禁用点击效果,这样做的目的是为了不让背景有按下效果 } break; } case MotionEvent.ACTION_MOVE: { switch (touchMode) { case TOUCH_MODE_IDLE: { break; } case TOUCH_MODE_DOWN: { final float x = event.getX(); if (Math.abs(x - touchX) > touchSlop) { touchMode = TOUCH_MODE_DRAGGING; // 禁值父View拦截触摸事件 // 如果不加这段代码的话,当被ScrollView包括的时候,你会发现,当你在此按钮上按下, // 紧接着滑动的时候ScrollView会跟着滑动,然后按钮的事件就丢失了,这会造成很难完成滑动操作 // 这样一来用户会抓狂的,加上这句话呢ScrollView就不会滚动了 if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } touchX = x; return true; } break; } case TOUCH_MODE_DRAGGING: { float newTouchX = event.getX(); tempTotalSlideDistance += setSlideX(tempSlideX + ((int) (newTouchX - touchX))); // 更新X轴坐标并记录总滑动距离 touchX = newTouchX; invalidate(); return true; } } break; } case MotionEvent.ACTION_UP: { setClickable(true); // 结尾滑动操作 if (touchMode == TOUCH_MODE_DRAGGING) {// 这是滑动操作 touchMode = TOUCH_MODE_IDLE; // 如果滑动距离大于等于最小切换距离就切换状态,否则回滚 if (Math.abs(tempTotalSlideDistance) >= Math.abs(frameDrawable .getIntrinsicWidth() * minChangeDistanceScale)) { toggle(); // 切换状态 } else { switchScroller.startScroll(isChecked()); } } else if (touchMode == TOUCH_MODE_DOWN) { // 这是按在按钮上的单击操作 touchMode = TOUCH_MODE_IDLE; toggle(); } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: { setClickable(true); if (touchMode == TOUCH_MODE_DRAGGING) { touchMode = TOUCH_MODE_IDLE; switchScroller.startScroll(isChecked()); // 回滚 } else { touchMode = TOUCH_MODE_IDLE; } break; } } super.onTouchEvent(event); return isEnabled(); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); int[] drawableState = getDrawableState(); if (frameDrawable != null) frameDrawable.setState(drawableState); // 更新框架图片的状态 if (stateDrawable != null) stateDrawable.setState(drawableState); // 更新状态图片的状态 if (stateMaskDrawable != null) stateMaskDrawable.setState(drawableState); // 更新状态遮罩图片的状态 if (sliderDrawable != null) sliderDrawable.setState(drawableState); // 更新滑块图片的状态 invalidate(); } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == frameDrawable || who == stateDrawable || who == stateMaskDrawable || who == sliderDrawable; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void jumpDrawablesToCurrentState() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { super.jumpDrawablesToCurrentState(); if (frameDrawable != null) frameDrawable.jumpToCurrentState(); if (stateDrawable != null) stateDrawable.jumpToCurrentState(); if (stateMaskDrawable != null) stateMaskDrawable.jumpToCurrentState(); if (sliderDrawable != null) sliderDrawable.jumpToCurrentState(); } } @Override public void setChecked(boolean checked) { boolean changed = checked != isChecked(); super.setChecked(checked); if (changed) { if (getWidth() > 0 && switchScroller != null) { // 如果已经绘制完成 switchScroller.startScroll(checked); } else { setSlideX(isChecked() ? tempMinSlideX : tempMaxSlideX); // 直接修改X轴坐标,因为尚未绘制完成的时候,动画执行效果不理想,所以直接修改坐标,而不执行动画 } } } @Override public int getCompoundPaddingRight() { // 重写此方法实现让文本提前换行,避免当文本过长时被按钮给盖住 int padding = super.getCompoundPaddingRight() + (frameDrawable != null ? frameDrawable.getIntrinsicWidth() : 0); if (!TextUtils.isEmpty(getText())) { padding += withTextInterval; } return padding; } /** * 设置图片 * * @param frameBitmap 框架图片 * @param stateDrawable 状态图片 * @param stateMaskDrawable 状态遮罩图片 * @param sliderDrawable 滑块图片 */ public void setDrawables() { this.frameDrawable = ResFinder.getDrawable("umeng_comm_switch_frame"); this.stateDrawable = ResFinder.getDrawable("umeng_comm_selector_switch_state"); this.stateMaskDrawable = ResFinder.getDrawable("umeng_comm_switch_state_mask"); this.sliderDrawable = ResFinder.getDrawable("umeng_comm_selector_switch_slider"); this.frameDrawable.setBounds(0, 0, this.frameDrawable.getIntrinsicWidth(), this.frameDrawable.getIntrinsicHeight()); this.frameDrawable.setCallback(this); this.stateDrawable.setBounds(0, 0, this.stateDrawable.getIntrinsicWidth(), this.stateDrawable.getIntrinsicHeight()); this.stateDrawable.setCallback(this); this.stateMaskDrawable.setBounds(0, 0, this.stateMaskDrawable.getIntrinsicWidth(), this.stateMaskDrawable.getIntrinsicHeight()); this.stateMaskDrawable.setCallback(this); this.sliderDrawable.setBounds(0, 0, this.sliderDrawable.getIntrinsicWidth(), this.sliderDrawable.getIntrinsicHeight()); this.sliderDrawable.setCallback(this); this.tempMinSlideX = (-1 * (stateDrawable.getIntrinsicWidth() - frameDrawable .getIntrinsicWidth())); // 初始化X轴最小值 setSlideX(isChecked() ? tempMinSlideX : tempMaxSlideX); // 根据选中状态初始化默认坐标 requestLayout(); } /** * 设置动画持续时间 * * @param duration 动画持续时间 */ public void setDuration(int duration) { this.duration = duration; } /** * 设置有效距离比例 * * @param minChangeDistanceScale * 有效距离比例,例如按钮宽度为100,比例为0.3,那么只有当滑动距离大于等于(100*0.3)才会切换状态,否则就回滚 */ public void setMinChangeDistanceScale(float minChangeDistanceScale) { this.minChangeDistanceScale = minChangeDistanceScale; } /** * 设置按钮和文本之间的间距 * * @param withTextInterval 按钮和文本之间的间距,当有文本的时候此参数才能派上用场 */ public void setWithTextInterval(int withTextInterval) { this.withTextInterval = withTextInterval; requestLayout(); } /** * 设置X轴坐标 * * @param newSlideX 新的X轴坐标 * @return Xz轴坐标增加的值,例如newSlideX等于100,旧的X轴坐标为49,那么返回值就是51 */ private int setSlideX(int newSlideX) { // 防止滑动超出范围 if (newSlideX < tempMinSlideX) newSlideX = tempMinSlideX; if (newSlideX > tempMaxSlideX) newSlideX = tempMaxSlideX; // 计算本次距离增量 int addDistance = newSlideX - tempSlideX; this.tempSlideX = newSlideX; return addDistance; } private static Bitmap getBitmapFromDrawable(Drawable drawable) { if (drawable == null) { return null; } if (drawable instanceof DrawableContainer) { return getBitmapFromDrawable(drawable.getCurrent()); } else if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } else { return null; } } /** * 切换滚动器,用于实现滚动动画 */ private class SwitchScroller implements Runnable { private Scroller scroller; public SwitchScroller(Context context, android.view.animation.Interpolator interpolator) { this.scroller = new Scroller(context, interpolator); } /** * 开始滚动 * * @param checked 是否选中 */ public void startScroll(boolean checked) { scroller.startScroll(tempSlideX, 0, (checked ? tempMinSlideX : tempMaxSlideX) - tempSlideX, 0, duration); post(this); } @Override public void run() { if (scroller.computeScrollOffset()) { setSlideX(scroller.getCurrX()); invalidate(); post(this); } } } }