Android左右滑动控件实现开关的切换效果

/**
 * 开关控件,通过左右滑动控件实现开关的切换效果

 * 使用时需要设置开关状态监听{@link OnSwitchStateChangeListener}

 * 切换开关状态有2种方式:

 *


 * 1.{@link #on()}, {@link #off()}, 该方式分开操作, 并且带头动作动画

 *


 * 2.{@link #turn(boolean, boolean), 该方式通过传入布尔值控制开关以及是否显示切换动画

 *
 */
public class UISwitch extends View {

    /**
     * 用来监听开关状态变化的接口
     *
     */
    public interface OnSwitchStateChangeListener {

        public void onChange(boolean state);
    }

    /** 最大滑动时间 **/
    private static final int MAX_SETTLE_DURATION = 5000;

    private static final int MESSAGE_SCROLL = 1;

    private static final int DEFAULE_TRACKER_NEXT = 600;

    /** 开, 关, 按钮 **/
    private Bitmap on, off, handle;

    private Paint mPaint;

    private Paint handlePaint;

    private Scroller mScroll;

    private VelocityTracker mVelocityTracker;

    private OnSwitchStateChangeListener l;

    private int width, height;

    private int mLastX;

    /** 滑动偏移量 **/
    private int mOffset;
    /** 最大最小偏移 **/
    private int mMaxOffset, mMinOffset;

    /** 开关状态 **/
    private boolean mState;
    
    private boolean mOnEnable;

    /** 是否进行了拖拽 **/
    private boolean mIsBeingDragged;

    /** 触摸优化 **/
    private int mTouchSlop;

    private int drawableIds[] = new int[] { R.drawable.switch_btn_on,
            R.drawable.switch_btn_off, R.drawable.switch_btn_handle };

    /** 滑动事件 **/
    private Handler scrollHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            if (mScroll.computeScrollOffset()) {

                final int currX = mScroll.getCurrX();
                final int finalX = mScroll.getFinalX();

                if (finalX >= 0) {
                    mOffset -= currX;
                } else {
                    mOffset += -currX;
                }

                // 校验偏移量
                mOffset = Math.min(Math.max(mMinOffset, mOffset), mMaxOffset);

                sendEmptyMessage(msg.what);

                invalidate();

            }
        }
    };

    public UISwitch(Context context) {
        this(context, null);
    }

    public UISwitch(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        
    }

    public UISwitch(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.UISwitch, defStyle, 0);

        if (a != null) {
            drawableIds[0] = a.getResourceId(
                    R.styleable.UISwitch_ui_switch_on_src,
                    R.drawable.switch_btn_on);
            drawableIds[1] = a.getResourceId(
                    R.styleable.UISwitch_ui_switch_off_src,
                    R.drawable.switch_btn_off);
            drawableIds[2] = a.getResourceId(
                    R.styleable.UISwitch_ui_switch_handle_src,
                    R.drawable.switch_btn_handle);

            mState = a.getBoolean(R.styleable.UISwitch_ui_switch_state, false);

            a.recycle();
        }
        init(context);

    }

    private void init(Context context) {

        on = getBitmapFor(drawableIds[0]);
        off = getBitmapFor(drawableIds[1]);
        handle = getBitmapFor(drawableIds[2]);

        mScroll = new Scroller(context, new AccelerateInterpolator());

        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat
                .getScaledPagingTouchSlop(configuration);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setFilterBitmap(true);

        handlePaint = new Paint();
        handlePaint.setAntiAlias(true);
        handlePaint.setDither(true);
        handlePaint.setFilterBitmap(true);

        setClickable(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (on == null || off == null || handle == null) {
            throw new RuntimeException("invalid image resources");
        }

        width = Math.max(Math.max(on.getWidth(), off.getWidth()),
                handle.getWidth());
        height = Math.max(Math.max(on.getHeight(), off.getHeight()),
                handle.getHeight());

        setMeasuredDimension(width, height);

        mMinOffset = 0;
        mMaxOffset = width - handle.getWidth();

        setState(mState);

        // final int newW = MeasureSpec.makeMeasureSpec(width,
        // MeasureSpec.EXACTLY);
        // final int newH = MeasureSpec.makeMeasureSpec(height,
        // MeasureSpec.EXACTLY);
        //
        // super.onMeasure(newW, newH);

    }

    @Override
    protected void onDraw(Canvas canvas) {

        final Bitmap bmp = Bitmap.createBitmap(width, height, Config.ARGB_8888);

        final Canvas c = new Canvas(bmp);

        // 画开图像
        c.drawBitmap(on, getSwitchLeft() - on.getWidth() + handle.getWidth()
                + mOffset, getSwitchTop(), mPaint);

        // 画关图像
        c.drawBitmap(off, getSwitchLeft() + mOffset, getSwitchTop(), mPaint);

        // 画把手
        c.drawBitmap(handle, getSwitchLeft() + mOffset, getSwitchTop(),
                handlePaint);

        // 圆角处理
        // canvas.drawBitmap(bmp, m, mPaint);
        canvas.drawBitmap(toRoundCorner(bmp, bmp.getHeight() / 2),
                getSwitchLeft(), getSwitchTop(), mPaint);
    }

    /**
     * 设置开关资源图片
     *
(the bitmap array sequence must be 'on', 'off', 'handle')
     *
     * @param bmp
     */
    public void setResourceBitmaps(Bitmap bmp[]) {
        if (bmp == null) {
            throw new NullPointerException("the resource bitmap do not null");
        }
        
        if (bmp.length != 3) {
            throw new ArrayIndexOutOfBoundsException("the resource bitmap array length must be 3");
        }

        on = bmp[0];
        off = bmp[1];
        handle = bmp[2];

        width = Math.max(Math.max(on.getWidth(), off.getWidth()),
                handle.getWidth());
        height = Math.max(Math.max(on.getHeight(), off.getHeight()),
                handle.getHeight());
        
        onMeasure(0, 0);
        
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        // 如果速度监听对象为空
        if (mVelocityTracker == null) {
            // 获得该对象的实例
            mVelocityTracker = VelocityTracker.obtain();
        }

        mVelocityTracker.addMovement(event);

        final int action = event.getAction();
        final int currX = (int) MotionEventCompat.getX(event, 0);
        switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            
            //不让父控件获取手势
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }

            lightness(handlePaint, 0.9f);

            if (mScroll.computeScrollOffset()) {
                mScroll.abortAnimation();
            }

            mLastX = currX;

            break;

        case MotionEvent.ACTION_MOVE:

            final int dx = currX - mLastX;

            if (!mIsBeingDragged) {

                if (Math.abs(dx) > mTouchSlop / 4) {
                    mIsBeingDragged = true;
                }
            } else {
                mOffset += currX - mLastX;

                // 校验偏移量
                mOffset = Math.min(Math.max(mMinOffset, mOffset), mMaxOffset);

                mLastX = currX;
            }

            break;

        case MotionEvent.ACTION_UP :
            
            //不让父控件获取手势
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }

            final VelocityTracker velocityTracker = mVelocityTracker;
            velocityTracker.computeCurrentVelocity(1000,
                    ViewConfiguration.get(getContext())
                            .getScaledMaximumFlingVelocity());
            int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                    velocityTracker, 0);

            if (Math.abs(initialVelocity) > DEFAULE_TRACKER_NEXT
                    || !mIsBeingDragged) {

                if (!mIsBeingDragged) {
                    initialVelocity = 0;
                }

                turn(!mState, initialVelocity);

            } else {

                final int halfOffset = mMaxOffset / 2;

                turn(mOffset > halfOffset, 0);
            }

            mIsBeingDragged = false;

            if (mVelocityTracker != null) {
                // 回收资源
                mVelocityTracker.recycle();

                mVelocityTracker = null;

            }

            lightness(handlePaint, 1f);

            break;
        }

        invalidate();

        return super.onTouchEvent(event);
    }

    /**
     * 通过提供一个起点和直线距离开始滑动
     *
     * @param startx
     *            起点x坐标
     * @param starty
     *            起点y坐标
     * @param dx
     *            x距离
     * @param dy
     *            y距离
     * @param duration
     *            持续滑动时间
     */
    private void startScroll(int startx, int starty, int dx, int dy,
            int duration) {
        mScroll.startScroll(startx, starty, dx, dy, duration);

        scrollHandler.removeMessages(MESSAGE_SCROLL);
        scrollHandler.sendEmptyMessage(MESSAGE_SCROLL);

    }

    /**
     * 获取滑动持续时间
     *
     * @param dx
     *            滑动距离
     * @param velocity
     *            滑动速度
     * @return
     */
    private int getScrollDuration(int dx, int velocity) {

        final int width = getWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth
                * distanceInfluenceForSnapDuration(distanceRatio);

        int duration = 0;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            duration = Math.abs(dx) * 4;
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);

        return duration;
    }

    // We want the duration of the page snap animation to be influenced by the
    // distance that
    // the screen has to travel, however, we don't want this duration to be
    // effected in a
    // purely linear fashion. Instead, we use this method to moderate the effect
    // that the distance
    // of travel has on the overall snap duration.
    private float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f; // center the values about 0.
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    /**
     * 将图片设置为圆角
     */
    private Bitmap toRoundCorner(Bitmap bitmap, int pixels) {
        final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
                bitmap.getHeight(), Config.ARGB_8888);
        final Canvas canvas = new Canvas(output);
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        final RectF rectF = new RectF(rect);
        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawRoundRect(rectF, pixels, pixels, paint);
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(0);
        paint.setStrokeCap(Cap.ROUND);
        paint.setStrokeJoin(Join.ROUND);
        canvas.drawRoundRect(rectF, pixels, pixels, paint);

        return output;
    }

    /**
     * 获取资源位图
     *
     * @param resId
     * @return
     */
    private Bitmap getBitmapFor(int resId) {
        Options op = new Options();
        op.inScaled = true;
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), resId, op);
        return bmp;
    }
    
    @Override
    public void setEnabled(boolean enabled) {
        setClickable(false);
        setFocusable(false);
        super.setEnabled(enabled);
    }

    /**
     * 开关开启,含有切换效果
     */
    public void on() {
        turn(true, 0);
    }

    /**
     * 开关关闭,含有切换效果
     */
    public void off() {
        turn(false, 0);
    }

    /**
     * 开关操作
     *
     * @param on
     * @param smoothScroll
     *            True to smoothly scroll to the new item, false to transition
     *            immediately
     */
    public void turn(boolean on, boolean smoothScroll) {
        if (smoothScroll) {
            turn(on, 0);
        } else {
            setState(on);
        }
    }

    /**
     * 开关操作,含有切换效果
     *
     * @param on
     *            true/false, 开/关, on/off
     * @param vx
     *            切换速度, 0为系统默认
     */
    private void turn(boolean on, int vx) {
        int distance;

        if (on) {
            distance = mOffset - mMaxOffset;
        } else {
            distance = mOffset;
        }

        if (on != mState) {
            if (l != null) {
                l.onChange(on);
            }
        }

        mState = on;

        if (mScroll.computeScrollOffset()) {
            mScroll.abortAnimation();
        }

        startScroll(0, 0, distance, 0, getScrollDuration(distance, vx));
    }

    /**
     * 设置开关状态,无状态切换动画
     */
    private void setState(boolean state) {

        if (state != mState) {
            if (l != null) {
                l.onChange(state);
            }
        }

        mState = state;

        if (state) {
            mOffset = mMaxOffset;
        } else {
            mOffset = 0;
        }

        invalidate();
    }

    /***
     * 设置开关状态变化事件
     *
     * @param l
     */
    public void setOnStateChangeListener(OnSwitchStateChangeListener l) {
        this.l = l;
    }

    private int getSwitchLeft() {
        return 0;
    }

    private int getSwitchRight() {
        return width;
    }

    private int getSwitchTop() {
        return 0;
    }

    private int getSwitchBottom() {
        return height;
    }

    /**
     * 调整画笔亮度
     *
     * @param paint
     *            画笔
     * @param light
     *            亮度单位 , 正常为1f, 范围0f - 2f
     */
    private void lightness(Paint paint, float light) {
        final ColorMatrix cm = new ColorMatrix();

        cm.setScale(light, light, light, 1);

        paint.setColorFilter(new ColorMatrixColorFilter(cm));
    }

}

你可能感兴趣的:(Android左右滑动控件实现开关的切换效果)