美文网首页
自制控件1 开关按钮

自制控件1 开关按钮

作者: 阿敏其人 | 来源:发表于2016-03-07 10:58 被阅读604次

    本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。

    自定义控件——初识自定义控件里面,我们已经大致介绍三种自定义控件,分别是

    • 自制控件
    • 组合控件
    • 拓展控件

    并且,我们已经对自制控件就继承自View和继承自ViewGroup进行了分析和最简单deme展示。

    熟能生巧,接下的几篇文章,我们依然来进行自制控件。
    在本篇里面,我们来进行自制简单的开关按钮。

    有图有真相,先看一下最终的效果图。

    效果.gif

    马上开工。

    一、思路整理

    从上面的图片中,我们看出,这个自定义控件涉及到3张图片,1张是黑色背景,1张是白色背景,另外一张就是一个圆形球的图片。

    思路:弄一个类继承自View,比如叫做DiyToggleView,利用onDraw()方法里面,控制着三张图片在对应的时刻显示对应的图片。

    我们触摸球状图片的时候,这张图片会动起来,所以需要用到onTouchEvent,然后在这里面进行MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP的判断

    在DiyToggleView里面,弄一些方法和回调接口给控件的使用者使用。

    大概思路已经整理好了,现在开工。

    二、准备三张图片,然后在在继承自View的自定义控件里面做出一个最简单的开关的样子

    .
    .

    1、准备3张图片

    球.png 关闭背景.png 打开背景.png

    2、画上简单的开关样子

    这个开关我们继承自View,然后复写三个构造方法。

    其实说到底,我们这个开关的自定义流程就是弄3个Bitmap,然后对点击和滑动进行监听,根据滑动的位置控制Bitmap是否显示。

    既然思路摆在这里,那么我们就先在onDraw画上一个开关的样子。

    DiyToggleView

    public class DiyToggleView extends View {
        private Bitmap toggleBall;
        private Bitmap toggleOnBg;
        private Bitmap toggleOffBg;
        public DiyToggleView(Context context) {
            super(context);
        }
        public DiyToggleView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        /**
         * 设置开关的滑动的球
         * @param resId
         */
        public void setToggleBallBitmap(int resId) {
            toggleBall = BitmapFactory.decodeResource(getResources(), resId);
        }
        /**
         * 设置开关的打开状态的背景
         * @param resId
         */
        public void setToggleOnBgBitmap(int resId){
            toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
        }
        /**
         * 设置开关关闭状态的背景
         * @param resId
         */
        public void setToggleOffBgBitmap(int resId){
            toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
        }
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if(toggleOnBg != null){
                canvas.drawBitmap(toggleOnBg, 0, 0, null);
            }
            if(toggleBall != null){
                canvas.drawBitmap(toggleBall,0,0,null);
            }
            
        }
        // 我们需要精确控制这个开关的大小,所以必须复写onMeasure方法
        // 在onMeasure里面精确控制大小用到的是  setMeasuredDimension 这个方法
        // 假设不复写控制的大小,那么这个自动自定义View即使宽高都为wrap_content也会占据全屏
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int measureWidth = toggleOnBg.getWidth();
            int measureHeight = toggleOnBg.getHeight();
            setMeasuredDimension(measureWidth,measureHeight);  // 控制View的大小的关键方法
        }
        
    }
    

    .
    .
    MainActivity

    public class MainActivity extends Activity {
        private DiyToggleView mDtv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mDtv = (DiyToggleView) findViewById(R.id.mDtv);
            mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
            mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
            mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
        }
    }
    

    .
    .
    activity_main

    <?xml version="1.0" encoding="utf-8"?>
    <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"
        tools:context="com.amqr.diytoggle.MainActivity">
        <com.amqr.diytoggle.DiyToggleView
            android:id="@+id/mDtv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:background="#00ff00"
            />
    </RelativeLayout>
    

    画好了
    (绿色背景是故意设置上去的,便于标示)

    模样初长成.png

    三、利用 onTouchEvent和onDraw让开关球可以被滑动

    代码都不用改动,只需要改动 DiyToggleView

    public class DiyToggleView extends View {
        private Bitmap toggleBall;
        private Bitmap toggleOnBg;
        private Bitmap toggleOffBg;
        private int ballCurrentX;  // 当前开关球所在的X
        public DiyToggleView(Context context) {
            super(context);
        }
        public DiyToggleView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        /**
         * 设置开关的滑动的球
         * @param resId
         */
        public void setToggleBallBitmap(int resId) {
            toggleBall = BitmapFactory.decodeResource(getResources(), resId);
        }
        /**
         * 设置开关的打开状态的背景
         * @param resId
         */
        public void setToggleOnBgBitmap(int resId){
            toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
        }
        /**
         * 设置开关关闭状态的背景
         * @param resId
         */
        public void setToggleOffBgBitmap(int resId){
            toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
        }
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if(toggleOnBg != null){
                canvas.drawBitmap(toggleOnBg, 0, 0, null);
            }
            if(toggleBall != null){
                canvas.drawBitmap(toggleBall,ballCurrentX,0,null);
            }
        }
        // 我们需要精确控制这个开关的大小,所以必须复写onMeasure方法
        // 在onMeasure里面精确控制大小用到的是  setMeasuredDimension 这个方法
        // 假设不复写控制的大小,那么这个自动自定义View即使宽高都为wrap_content也会占据全屏
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int measureWidth = toggleOnBg.getWidth();
            int measureHeight = toggleOnBg.getHeight();
            setMeasuredDimension(measureWidth,measureHeight);  // 控制View的大小的关键方法
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            //return super.onTouchEvent(event);
            // 触摸事件的down,move,up,按下,移动,松开
            // getX()是触摸的点与控件自身的距离
            int action = event.getAction();
            switch (action){
                case MotionEvent.ACTION_DOWN:
                    ballCurrentX = (int)(event.getX()+0.5f); // getX()代表当前,返回值是float,加上0.5是为了确保四舍五进进1
                    break;
                case MotionEvent.ACTION_MOVE:
                    ballCurrentX = (int)(event.getX()+0.5f);
                    break;
                case MotionEvent.ACTION_UP:
                    ballCurrentX = (int)(event.getX()+0.5f);
                    break;
            }
            invalidate(); // 重新绘制,可以理解为调用onDraw
            return true; // 返回 true,代表当前View消费当前touch
        }
    }
    

    简单的滑动最好了,但是现在滑到中途释放的时候没有进行位置归位判断,而且还有越界问题。

    四、释放的位置归和越界问题的解决

    其实也简单,就是添加两个boolean值,判断是否打开和是否触摸

    private boolean isOpen = true;
    private boolean isOpen;  // 作用是区分开 触摸事件的 up
    

    然后onDraw和onTouchEvent相互结合,isOpen和isOpen的值,做出相应的位置处理

    最后我们写了一个回调,让调用者可以获知当前的开关状态和控制开关的状态。

    .
    .
    DiyToggleView

    
    public class DiyToggleView extends View {
        private Bitmap toggleBall;
        private Bitmap toggleOnBg;
        private Bitmap toggleOffBg;
        private int ballCurrentX;  // 当前开关球所在的X
        private boolean isOpen = true;
        private boolean isTouch;  // 作用是区分开 触摸事件的 up
        private ToggleStaleListener toggleStaleListener;
        /**
         * 对开关状态的回调操作
         * @param toggleStaleListener
         */
        public void setToggleState(ToggleStaleListener toggleStaleListener){
            this.toggleStaleListener = toggleStaleListener;
        }
        public void setToggleState(boolean state){
            isOpen = state;
            invalidate();
        }
        public boolean getToggleState(){
            return isOpen;
        }
        public DiyToggleView(Context context) {
            super(context);
        }
        public DiyToggleView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        /**
         * 设置开关的滑动的球
         *
         * @param resId
         */
        public void setToggleBallBitmap(int resId) {
            toggleBall = BitmapFactory.decodeResource(getResources(), resId);
        }
        /**
         * 设置开关的打开状态的背景
         *
         * @param resId
         */
        public void setToggleOnBgBitmap(int resId) {
            toggleOnBg = BitmapFactory.decodeResource(getResources(), resId);
        }
        /**
         * 设置开关关闭状态的背景
         *
         * @param resId
         */
        public void setToggleOffBgBitmap(int resId) {
            toggleOffBg = BitmapFactory.decodeResource(getResources(), resId);
        }
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (toggleOnBg == null ||toggleOffBg==null|| toggleBall == null) {
                return;
            }
            int ballWidth = toggleBall.getWidth();
            int ballToRightX = toggleOnBg.getWidth() - ballWidth;
            // 当处于触摸事件的down和move状态
            if(isTouch){
                if(isOpen){
                    canvas.drawBitmap(toggleOnBg, 0, 0, null);
                    // 开关球 不触及边界的自由活动范围
                    if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
                        canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
                    }
                    // 当开关球与View的距离大于右侧的球和边框的距离,就停在右侧
                    if (ballCurrentX>ballToRightX){
                        canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
                    }
                    // 当开关球与View的距离小于左侧球与边距的边框的距离,就停在左侧
                    if (ballCurrentX < ballWidth) {
                        canvas.drawBitmap(toggleBall, 0, 0, null);
                        isOpen = true;
                    }
                }else{
                    canvas.drawBitmap(toggleOffBg, 0, 0, null);
                    if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
                        canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
                    }
                    if (ballCurrentX > ballToRightX) {
                        canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
                        isOpen = false;
                    }
                    if(ballCurrentX < ballWidth){
                        canvas.drawBitmap(toggleBall, 0, 0, null);
                    }
                }
            // 当触摸时间的up状态被触发
            }else{
                if(isOpen){
                    canvas.drawBitmap(toggleOnBg, 0, 0, null);
                    canvas.drawBitmap(toggleBall, 0, 0, null);
                }else{
                    canvas.drawBitmap(toggleOffBg, 0, 0, null);
                    canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
                }
            }
        }
        // 我们需要精确控制这个开关的大小,所以必须复写onMeasure方法
        // 在onMeasure里面精确控制大小用到的是  setMeasuredDimension 这个方法
        // 假设不复写控制的大小,那么这个自动自定义View即使宽高都为wrap_content也会占据全屏
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int measureWidth = toggleOnBg.getWidth();
            int measureHeight = toggleOnBg.getHeight();
            setMeasuredDimension(measureWidth, measureHeight);  // 控制View的大小的关键方法
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            //return super.onTouchEvent(event);
            // 触摸事件的down,move,up,按下,移动,松开
            // getX()是触摸的点与控件自身的距离
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    isTouch = true;
                    ballCurrentX = (int)(event.getX()+0.5f); // getX()代表当前,返回值是float,加上0.5是为了确保四舍五进进1
                    break;
                case MotionEvent.ACTION_MOVE:
                    isTouch = true;
                    ballCurrentX = (int) (event.getX() + 0.5f);
                    break;
                case MotionEvent.ACTION_UP:
                    isTouch = false;
                    ballCurrentX = (int) (event.getX() + 0.5f);
                    if(ballCurrentX<toggleBall.getWidth()){
                        isOpen = true;
                        if(toggleStaleListener != null){
                            // 回调真正被执行
                            toggleStaleListener.toggleState(this,isOpen);
                        }
                    }
                    if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
                        isOpen = false;
                        if(toggleStaleListener != null){
                            toggleStaleListener.toggleState(this,isOpen);
                        }
                    }
                    break;
            }
            invalidate(); // 重新绘制,可以理解为调用onDraw
            return true; // 返回 true,代表当前View消费当前touch
        }
        // 回调接口
        public interface ToggleStaleListener{
            void toggleState(DiyToggleView view,boolean state);
        }
    }
    

    上面的代码其实主要看onDraw和onTouchEvent就好。
    上面的onTouchEvent需要注意的是记得在后面加上invalidate();每次invalidate()被执行就会去重新调用一下onDraw

    1、利用getX获得球当前相对于控件的距离

    不管是越界处理还是手指释放时的位置归正,都需要对球的位置进行判断,那么怎么获取球当前的位置呢?利用getX

    这里我们有必要先来了解什么View的
    getX()、getY()
    getRawX()、getRawY()
    这么几个方法。

    这几个方法都是关于距离和点的方法。

    先看图

    getX和getRawX的区别.png

    上结论:
    getX()是触摸的点与控件自身的距离
    getRawX()是触摸的点与屏幕的距离

    (getX之后得到的是float类型的值,而不是int,所以我们加上0.5f保证带小数的float转换成int类型的时候都能够进1,方便计算。)

    我们利用getWidth可以得到背景的宽度。
    利用getX可以得到当前我们的球当前按下的点距离背景的距离。

    这两者一结合,再加判断,就可解决越界和位置归正的问题

    越界问题的解决

    左侧不越界

    // 当开关球与View的距离小于左侧球与边距的边框的距离,就停在左侧
    if (ballCurrentX < ballWidth) {
        canvas.drawBitmap(toggleBall, 0, 0, null);
        isOpen = true;
    }
    

    右侧不越界

    // 当开关球与View的距离大于右侧的球和边框的距离,就停在右侧
    if (ballCurrentX>ballToRightX){
        canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
    }
    

    位置归正问题的解决

            // 当触摸时间的up状态被触发
            }else{
                if(isOpen){
                    canvas.drawBitmap(toggleOnBg, 0, 0, null);
                    canvas.drawBitmap(toggleBall, 0, 0, null);
                }else{
                    canvas.drawBitmap(toggleOffBg, 0, 0, null);
                    canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
                }
            }
    

    设置回调接口,给使用者自主设置开关的状态的权力

        // 回调接口
        public interface ToggleStaleListener{
            void toggleState(DiyToggleView view,boolean state);
        }
    

    回调执行的地方

    case MotionEvent.ACTION_UP:
        isTouch = false;
        ballCurrentX = (int) (event.getX() + 0.5f);
        if(ballCurrentX<toggleBall.getWidth()){
            isOpen = true;
            if(toggleStaleListener != null){
                // 回调真正被执行
                toggleStaleListener.toggleState(this,isOpen);
            }
        }
        if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
            isOpen = false;
            if(toggleStaleListener != null){
                toggleStaleListener.toggleState(this,isOpen);
            }
        }
        break;
    

    关于回调可以参考这里 说说安卓回调——CallBack
    .
    .
    MainActivity 使用这个回调

    public class MainActivity extends Activity {
        private DiyToggleView mDtv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mDtv = (DiyToggleView) findViewById(R.id.mDtv);
            mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
            mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
            mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
            mDtv.setToggleState(false);
            mDtv.setToggleState(new DiyToggleView.ToggleStaleListener() {
                @Override
                public void toggleState(DiyToggleView view, boolean state) {
                    Toast.makeText(MainActivity.this,"当前的开关状态是:"+mDtv.getToggleState(),Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    

    至此完成。
    另外一篇博文自制控件2 —— 自制控件 仿qq侧滑菜单一文中,将继续自制控件,欢迎点击查阅。

    效果.gif

    下载链接

    本篇完。

    相关文章

      网友评论

          本文标题:自制控件1 开关按钮

          本文链接:https://www.haomeiwen.com/subject/ydrakttx.html