• 自定义控件之滑动开关按钮


    对于Android的自定义控件是自己一直想研究总结的,所以未来会从基础开始,一点点来学习一些自定义控件的效果,这些知识并非完全自己来研究的,但是是自己学习成长的点滴记录,重在搞懂原理,言归正传~

    这次要实现的滑动开关按钮的效果如下:

    【说明】:之后所有的自定义效果的学习文章都是先上效果图之后,然后再一步步从无到有的去实现。

    从效果图中可以发现,这是一个"很简单"的开关按钮控件,也就是平常使用的CheckBox的效果,但是又要比CheckBox控件要多出一个效果,就是该控件支持滑动,而且这个效果是从无到有一点点自定义出来的,也就是自己动手实现一个类似于CheckBox的效果,所以其实也不是很简单,麻雀虽小五脏俱全,通过这个例子来熟知自定义控件的整个过程,下面则一点点来实现它。

    首先控件需要这两张图片素材:

                 

    自定义过CheckBox的都清楚,图片的高度是一样大小的:

    新建一个工程,然后将这两个资源文件放入到工程中:

    然后新建一个自定义View,如下:

    其中需要重写构造方法:

    其中只需要重写两个构造方法既可:

    /**
     * 自定义滑动开关view
     */
    public class MyToggleButton extends View {
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
    }

    而从上面的代码注释中可以发现这两个构造方法是有区分的,有必要知道一下,第一个是类似于这种使用场景:

    而第二个构造是在布局文件中定义该View,由系统去调用的,而不是我们去调的,也就是这样:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.example.togglebutton.MainActivity" >
    
        <com.example.togglebutton.MyToggleButton
            android:id="@+id/toggle_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
    </RelativeLayout>

    此时,如果我们将第二个构造方法注释掉,就会报错:

    /**
     * 自定义滑动开关view
     */
    public class MyToggleButton extends View {
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
    //    public MyToggleButton(Context context, AttributeSet attrs) {
    //        super(context, attrs);
    //    }
    
    }

    运行看效果:

    所以对于这两个构造的含义就清楚了,将注释的代码还原。

    接下来绘制View,在正式绘制之前,需要把View对象要能显示在屏幕上的几个步骤说明一下,相当于中心思想,抓住了中心思想代码写起来就能有的放矢,如下:

    ①、调用构造方法,创建对象。

    ②、测量View的大小,在绘制之前是需要先确定View的大小的,对应的方法是onMeasure(int , int)。

    ③、确定View的位置,View自身有一些建议权,决定权在父View手中,onLayout(),这个方法由于是由ViewGoup决定的,所以对于View一般没用,不会重写它。

    ④、绘制View的内容。对应的方法是onDraw(Canvas)。

    下面则遵照上面的步骤一一去实现,第一步已经定义了,接着来实现第二步,重写onMeasure方法:

    public class MyToggleButton extends View {
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
    }

    而它的大小应该是来背景图片来决定的,所以需要把图片加载进来:

    public class MyToggleButton extends View {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
    }

    接着来实现onMeasure方法,先来查看一下它的父类的实现:

    所以我们也依葫芦画瓢:

    public class MyToggleButton extends View {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
    }

    接下来到第三部:确认View的位置,由于View本身决定不了,而是由它的父控件决定的,所以不用管这个方法,这里重写只用来观察方法:

    public class MyToggleButton extends View {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
    }

    要显示在屏幕上还差最后一步了,重写onDraw()方法:

    所以super可以直接删掉,下面来开始将图片绘制在画布上:

    其中第四个参数中需要一个paint对象,所以先初始化一个:

    public class MyToggleButton extends View {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
        }
    }

    下面来绘制图片:

    【说明】:关于View的绘制这里不多讲,之后会不断去研究它的,这里先直接用。

    这时先来运行看下初步的效果:

    接下来,实现点击可以进行开关按钮的切换效果,先来思考一下怎么来实现:

    所以这时的这个参数需要声明成一个变量动态去改变:

    public class MyToggleButton extends View {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    }

    接着给当前View增加点击事件,然后处理点击逻辑:

    public class MyToggleButton extends View implements OnClickListener {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
        /** 当前开关的状态,true为开,false为关 **/
        private boolean currentToggleSate;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
    
            setOnClickListener(this);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    
        @Override
        public void onClick(View v) {
            currentToggleSate = !currentToggleSate;
            flushState();
        }
    
        /**
         * 刷新当前状态
         */
        private void flushState() {
            if (currentToggleSate) {
                slideButtonLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
            } else {
                slideButtonLeft = 0;
            }
            invalidate();
        }
    }

    编译运行看效果:

    接下来实现最后一个功能,也是相对而言最复杂的,也就是支持滑动切换,怎么做呢?当然是要监听它的touch事件喽:

    这个滑动切换的第一步,就是这个SlideButton能够随着手指滑动,所以先来实现它:

    public class MyToggleButton extends View implements OnClickListener {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
        /** 当前开关的状态,true为开,false为关 **/
        private boolean currentToggleSate;
        /** down 事件时的x值 **/
        private int firstX;
        /** touch 事件时上一个x值 **/
        private int lastX;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
    
            setOnClickListener(this);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    
        @Override
        public void onClick(View v) {
            currentToggleSate = !currentToggleSate;
            flushState();
        }
    
        /**
         * 刷新当前状态
         */
        private void flushState() {
            if (currentToggleSate) {
                slideButtonLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
            } else {
                slideButtonLeft = 0;
            }
            invalidate();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = lastX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int currentX = (int) event.getX();
                // 算出移动的距离
                int moveDistance = currentX - lastX;
                // 并把当前的x值缓存起来,但计算下次移动的距离
                lastX = currentX;
                // 然后根据移动位置来动态改变slideButtonLeft
                slideButtonLeft = slideButtonLeft + moveDistance;
                break;
            case MotionEvent.ACTION_UP:
                break;
            }
            invalidate();
            return true;
        }
    }

    运行看下效果:

    随着手指移动倒没啥问题了,但是发现移动时没有做位置限制,应该不允许滑出背景,所以接下来需要做一下判断,也就是在触摸刷新前需要判断一下:

    public class MyToggleButton extends View implements OnClickListener {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
        /** 当前开关的状态,true为开,false为关 **/
        private boolean currentToggleSate;
        /** down 事件时的x值 **/
        private int firstX;
        /** touch 事件时上一个x值 **/
        private int lastX;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
    
            setOnClickListener(this);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    
        @Override
        public void onClick(View v) {
            currentToggleSate = !currentToggleSate;
            flushState();
        }
    
        /**
         * 刷新当前状态
         */
        private void flushState() {
            if (currentToggleSate) {
                slideButtonLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
            } else {
                slideButtonLeft = 0;
            }
            flushView();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = lastX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int currentX = (int) event.getX();
                // 算出移动的距离
                int moveDistance = currentX - lastX;
                // 并把当前的x值缓存起来,但计算下次移动的距离
                lastX = currentX;
                // 然后根据移动位置来动态改变slideButtonLeft
                slideButtonLeft = slideButtonLeft + moveDistance;
                break;
            case MotionEvent.ACTION_UP:
                break;
            }
            flushView();
            return true;
        }
    
        private void flushView() {
            // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
            int maxLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
            // 确保slideButtonLeft>=0
            slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
            // 确保slideButtonLeft<=maxleft
            slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
            invalidate();
        }
    }

    其区域判断的核心就是:

    再次运行:

    从结果来看已经加入了区域限制,这时滑不出背景区域了,接下来还有一个问题需要处理一下,就是关于滑动事件与onClick冲突的问题,下面来看下:

    首先给onClick()方法上加上一条log用来观察呆会的实验:

    下面运行,对于onClick()事件的触发,就是称按下鼠标,最后松开鼠标这样就构成了一个点击事件,那如果按下鼠标不松手,然后进行滑动,最后再松开鼠标也会触发onClick()事件么,用实验来证明下:

    可见系统的这种onClick()触发行为不是我们想要的,我们想要的是如果发生了滑动,则就不触发onClick()了,所以下面需要进行一个逻辑判断来避免这个问题:

    public class MyToggleButton extends View implements OnClickListener {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
        /** 当前开关的状态,true为开,false为关 **/
        private boolean currentToggleSate;
        /** down 事件时的x值 **/
        private int firstX;
        /** touch 事件时上一个x值 **/
        private int lastX;
        /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/
        private boolean isDrag;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
    
            setOnClickListener(this);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    
        @Override
        public void onClick(View v) {
            if (isDrag) {
                // 如果发生了拖动,则不响应点击事件了
                return;
            }
            Log.d("cexo", "onClick()");
            currentToggleSate = !currentToggleSate;
            flushState();
        }
    
        /**
         * 刷新当前状态
         */
        private void flushState() {
            if (currentToggleSate) {
                slideButtonLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
            } else {
                slideButtonLeft = 0;
            }
            flushView();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = lastX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int currentX = (int) event.getX();
                // 算出移动的距离
                int moveDistance = currentX - lastX;
                // 并把当前的x值缓存起来,但计算下次移动的距离
                lastX = currentX;
                // 然后根据移动位置来动态改变slideButtonLeft
                slideButtonLeft = slideButtonLeft + moveDistance;
                break;
            case MotionEvent.ACTION_UP:
                break;
            }
            flushView();
            return true;
        }
    
        private void flushView() {
            // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
            int maxLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
            // 确保slideButtonLeft>=0
            slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
            // 确保slideButtonLeft<=maxleft
            slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
            invalidate();
        }
    }

    那问题的关键来了,判断是否是拖动的界限是?其实可以这样来认为:如果从ACTION_DOWN到ACTION_MOVE这两点的位置超过了5px,则认为是滑动,所以判断代码如下:

    下面再来运行看下是否对滑动和点击做了明显的区分:

    从结果来看,当拖动时再松手,则就没有走onClick的逻辑了,而是停到了我们滑动的位置不动了,也就达到了我们的目的。

    接下来就要处理滑动切换的效果了,那切换的界限在哪呢?

    所以,根据上图的描述,开关的判断也很简单了,具体代码如下:

    public class MyToggleButton extends View implements OnClickListener {
    
        /** 做为背景的图片 **/
        private Bitmap backgroundBitmap;
        /** 可以滑动的图片 **/
        private Bitmap slideButtonBitmap;
        private Paint paint;
        /** 滑动按钮的左边距 **/
        private float slideButtonLeft;
        /** 当前开关的状态,true为开,false为关 **/
        private boolean currentToggleSate;
        /** down 事件时的x值 **/
        private int firstX;
        /** touch 事件时上一个x值 **/
        private int lastX;
        /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/
        private boolean isDrag;
    
        /**
         * 在代码里面创建对象的时候,使用此构造方法
         */
        public MyToggleButton(Context context) {
            super(context);
        }
    
        /**
         * 在布局文件中声名的view,创建时由系统自动调用
         */
        public MyToggleButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView();
        }
    
        private void initView() {
            backgroundBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.switch_background);
            slideButtonBitmap = BitmapFactory.decodeResource(getResources(),
                    R.drawable.slide_button);
    
            paint = new Paint();
            paint.setAntiAlias(true);
    
            setOnClickListener(this);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 设置当前View的大小,以背景图为大小,单位都是像素
            setMeasuredDimension(backgroundBitmap.getWidth(),
                    backgroundBitmap.getHeight());
        }
    
        /**
         * 确定位置的时候调用此方法,自定义View的时候作用不大
         */
        @Override
        protected void onLayout(boolean changed, int left, int top, int right,
                int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            // 先绘制背景图
            canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
            // 再绘制滑动按钮
            canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint);
        }
    
        @Override
        public void onClick(View v) {
            if (isDrag) {
                // 如果发生了拖动,则不响应点击事件了
                return;
            }
            Log.d("cexo", "onClick()");
            currentToggleSate = !currentToggleSate;
            flushState();
        }
    
        /**
         * 刷新当前状态
         */
        private void flushState() {
            if (currentToggleSate) {
                slideButtonLeft = backgroundBitmap.getWidth()
                        - slideButtonBitmap.getWidth();
            } else {
                slideButtonLeft = 0;
            }
            flushView();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            super.onTouchEvent(event);
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isDrag = false;
                firstX = lastX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int currentX = (int) event.getX();
                // 判断是否发生拖动
                if (Math.abs(currentX - firstX) > 5) {
                    isDrag = true;
                }
                // 算出移动的距离
                int moveDistance = currentX - lastX;
                // 并把当前的x值缓存起来,但计算下次移动的距离
                lastX = currentX;
                // 然后根据移动位置来动态改变slideButtonLeft
                slideButtonLeft = slideButtonLeft + moveDistance;
                break;
            case MotionEvent.ACTION_UP:
                if (isDrag) {
                    int maxLeft = backgroundBitmap.getWidth()
                            - slideButtonBitmap.getWidth();
                    // 根据slideButtonLeft来判断当前应该是什么状态(开,关)
                    if (slideButtonLeft > maxLeft / 2) {
                        // 开状态
                        currentToggleSate = true;
                    } else {
                        currentToggleSate = false;
                    }
    
                    flushState();
                }
                break;
            }
            flushView();
            return true;
        }
    
        private void flushView() {
            // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft
            int maxLeft = backgroundBitmap.getWidth()
                    - slideButtonBitmap.getWidth();
            // 确保slideButtonLeft>=0
            slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0;
            // 确保slideButtonLeft<=maxleft
            slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft;
            invalidate();
        }
    }

    运行看下效果:

    虽说这个例子很简单,但是实际上涉及了自定义一个View的一个大致过程,之后会不断对其进行研究。

  • 相关阅读:
    git stash 一个场景 mark
    sendBeacon 使用
    【踩坑笔记】layui之单选和复选框不显示
    wordpress后台管理超时没反应:load-scripts.php载入缓慢出错
    MySQL_Sql_打怪升级_进阶篇_测试: SQL随机生成测试数据
    MySQL_Sql_打怪升级_进阶篇_测试: 游标应用
    MySQL8.0新特性_01_JSON数据格式的支持
    MySQL_Sql_打怪升级_进阶篇_进阶19: 函数
    MySQL_Sql_打怪升级_进阶篇_进阶18: 存储过程
    MySQL_Sql_打怪升级_进阶篇_进阶17: 变量
  • 原文地址:https://www.cnblogs.com/webor2006/p/4625461.html
Copyright © 2020-2023  润新知