自定义View之低仿UIswitch按钮

参考这个帖子进行了优化,主要就是修改触摸点View的处理.先看一下最终效果

最终效果.omyladygaga

 

制作过程需要的资源文件

滑动的小球 背景尺寸 图层

 

滑动的背景

 

1.千里之行,始于足下.首先创建一个工程,名称为MyUISwitch,新建自定义View类MyUiSwitch.kt继承自View.在res/values下创建attrs.xml新建我们唯一需要的自定义属性checked



    
        
    

2.在需要使用UISwitch的布局中(avtivity_main.xml)调用自定义View.




    

3.在自定义View中两个参数的构造函数中初始化自定义View需要的资源文件.

 constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.checkboxStyle) {
        val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.MyUiSwitch)
        //获取xml布局文件中app:checked=""属性值
        isChecked = obtainStyledAttributes.getBoolean(R.styleable.MyUiSwitch_checked, false)
        obtainStyledAttributes.recycle()
        init()
    }

不要在意构造函数中调用自己三个参数构造函数传入的值R.attr.checkboxStyle,它只是为了不凸显自定义View.

private fun init() {
        mPaint = Paint()
        mPaint!!.isAntiAlias = true //抗锯齿

        pdf = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)

        bitmapBackGroud = BitmapFactory.decodeResource(resources, R.drawable.switch_bg)
        bitmapBall = BitmapFactory.decodeResource(resources, R.drawable.switch_ball)
        bitmapBottom = BitmapFactory.decodeResource(resources, R.drawable.switch_bottom)
        bitmapBlack = BitmapFactory.decodeResource(resources, R.drawable.switch_black)

        switchWidth = bitmapBackGroud!!.width
        switchHight = bitmapBackGroud!!.height

        if (isChecked) {
            ballMoveState = RIGHT_MOST
            ballCenter = switchWidth!! - bitmapBall!!.width / 2
        } else {
            ballCenter = bitmapBall!!.width / 2
        }
    }

主要说一下if(isChecked)判断,其他的都是普通的初始化操作.isChecked属性是判断按钮是否处于开关状态.

开状态的时候,将ballMoveState小球的移动状态属性设置为RIGHT_MOST最右边,ballCenter小球的中心点相对父布局坐标设置为switchWidth!! - bitmapBall!!.width / 2,switchWidth为父布局的宽度,也就是按钮控件宽度.bitmapBall!!.width是小球的宽度,也是小球的直径,除以2后得到小球的半径,当按钮控件的宽度-小球的半径后,得到的就是小球开状态下,在最右边中心点的坐标.

关状态的时候,因为ballMoveState属性初始化为LEFT_MOST所以不必再设置.ballCenter小球的中心点相对父布局坐标设置为bitmapBall!!.width / 2.当小球处于按钮控件最左边的时候,小球的半径即小球的中心点的坐标.

到这里可能会有人好奇,ballCenter小球中心点坐标有什么用呢?先简单说明一下,下面会根据小球中心点的坐标画滑动背景的位置来达到开关的视觉效果.

4.好了,初始化所有的资源后肯定是要重写onDraw方法啦

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //增加图层
        canvas!!.saveLayer(
            0.toFloat(),
            0.toFloat(),
            switchWidth!!.toFloat(),
            switchHight!!.toFloat(),
            null
        )
        //底部是黑色图层
        canvas.drawBitmap(bitmapBlack, 0.toFloat(), 0.toFloat(), mPaint)
        mPaint!!.setXfermode(pdf)
        Log.i(STX, "ballCenter: $ballCenter")
        if (ballCenter!! >= switchWidth!! - bitmapBall!!.width / 2) {
            ballCenter = switchWidth!! - bitmapBall!!.width / 2
        } else if (ballCenter!! <= bitmapBall!!.width / 2) {
            ballCenter = bitmapBall!!.width / 2
        }
        canvas.drawBitmap(
            bitmapBottom,
            (-(switchWidth!! - bitmapBall!!.width / 2 - ballCenter!!)).toFloat(),
            0.toFloat(),
            mPaint
        )
        // 还原混合模式
        mPaint!!.setXfermode(null)
        canvas.restore()
        ballMoveState(canvas)
    }

在onDraw方法中,首先创建一个与控件大小相同的图层,然后绘画一个黑色的背景(给的资源文件大小都是规格正好的),然后使用mPaint!!.setXfermode(pdf),pdf模式为PorterDuff.Mode.SRC_IN得到控件的椭圆型活动空间,然后根据ballCenter小球中心点的坐标绘画可以滑动的背景bitmapBottom的位置,达到开关的视觉效果.

ballCenter小球中心点的坐标分为三种情况处理:

  ①.当小球中心点的坐标(ballCenter)>=小球处于最右边的坐标(switchWidth!! - bitmapBall!!.width / 2)时,小球不可能让他划出右边框外,所以把中心点坐标设置为ballCenter = switchWidth!! - bitmapBall!!.width / 2,即小球处于控件最右边.

  ②.当小球中心点的坐标(ballCenter)<=小球处于最左边的坐标(bitmapBall!!.width / 2)时,小球不可能让他划出左边框外,所以把中心点坐标设置为ballCenter =bitmapBall!!.width / 2.

  ③.所以小球的可活动范围是:小球处于最左边的坐标(bitmapBall!!.width / 2)<=小球中心点的坐标(ballCenter)<=小球处于最右边的坐标(switchWidth!! - bitmapBall!!.width / 2)时,在这个范围内,根据小球中心点的坐标(ballCenter)绘制可滑动的背景bitmapBottom.bitmapBottom的绘制主要是控制x点坐标,bitmapBottom的x点坐标活动范围是-(switchWidth!! - bitmapBall!!.width)到0之间,-(switchWidth!! - bitmapBall!!.width)表示关闭状态,bitmapBottom显示出来的是粉色的画面,0是开启状态,bitmapBottom显示出来的是白色的开启状态.bitmapBottom的滑动受小球中心点的坐标(ballCenter)改变而改变.即(-(switchWidth!! - bitmapBall!!.width / 2 - ballCenter!!)).toFloat()

方法ballMoveState(canvas)根据小球的滑动状态而绘制小球.

    /**
     * 滑动状态绘制
     */
    fun ballMoveState(canvas: Canvas) {
        when (ballMoveState) {
            LEFT_MOST ->
                canvas.drawBitmap(bitmapBall!!, 0.toFloat(), 0.toFloat(), mPaint)
            RIGHT_MOST ->
                canvas.drawBitmap(bitmapBall!!, (switchWidth!! - bitmapBall!!.width).toFloat(), 0.toFloat(), mPaint)
            TOUCH_STATE_MOVE->
                if (touchX!! > bitmapBall!!.width / 2 && touchX!! < switchWidth!! - bitmapBall!!.width / 2) {
                    canvas!!.drawBitmap(
                        bitmapBall!!,
                        (touchX!! - bitmapBall!!.width / 2).toFloat(),
                        0.toFloat(),
                        mPaint
                    )
                } else if (touchX!! <= bitmapBall!!.width / 2) {
                    canvas.drawBitmap(bitmapBall!!, 0.toFloat(), 0.toFloat(), mPaint)
                } else if (touchX!! >= switchWidth!! - bitmapBall!!.width / 2) {
                    canvas.drawBitmap(bitmapBall, (switchWidth!! - bitmapBall!!.width).toFloat(), 0.toFloat(), mPaint)
                }
            TOUCH_STATE_UP ->
                if (touchX!! > bitmapBall!!.width / 2 && touchX!! < switchWidth!! - bitmapBall!!.width / 2) {
                    if (touchX!! > switchWidth!! / 2) {
                        touchX = switchWidth!! - bitmapBall!!.width / 2
                    } else {
                        touchX = bitmapBall!!.width / 2
                    }
                    canvas!!.drawBitmap(bitmapBall!!, touchX!! - bitmapBall!!.width / 2.toFloat(), 0.toFloat(), mPaint)
                } else if (touchX!! <= bitmapBall!!.width / 2) {
                    canvas.drawBitmap(bitmapBall!!, 0.toFloat(), 0.toFloat(), mPaint)
                } else if (touchX!! >= switchWidth!! - bitmapBall!!.width / 2) {
                    canvas.drawBitmap(bitmapBall, (switchWidth!! - bitmapBall!!.width).toFloat(), 0.toFloat(), mPaint)
                }
        }
    }

when判断ballMoveState的状态:

  ①.当ballMoveState的状态为LEFT_MOST或者RIGHT_MOST时,只有控件在初始化的时候绘制,表示控件的开关状态,小球的初始位置为最左边或者最右边.

  ②.当ballMoveState的状态为TOUCH_STATE_MOVE时,代表(手指)小球的状态在移动.小球移动时也分为三种状态:

    a.手指触摸点在小球最左边和最右边之间移动时,即touchX!! > bitmapBall!!.width / 2 && touchX!! < switchWidth!! - bitmapBall!!.width / 2.小球的绘制x点为touchX!! - bitmapBall!!.width / 2.

    b.手指触摸点划过最左边时,即touchX!! <= bitmapBall!!.width / 2.小球的绘制x点为0.

    c.手指触摸点划过最右边时,即touchX!! >= switchWidth!! - bitmapBall!!.width / 2.小球的绘制x点为switchWidth!! - bitmapBall!!.width.

  ③.当ballMoveState的状态为TOUCH_STATE_UP 时,代表(手指)抬起状态.手指抬起点也为三种状态,与TOUCH_STATE_MOVE状态处理类似,只不过如果手指抬起时,触摸点TouchX坐标位于小球最左边和最右边之间时,对TouchX做相应处理:

    a.当手指抬起点在控件右边时,即touchX!! > switchWidth!! / 2,将小球绘制在最右边,touchX = switchWidth!! - bitmapBall!!.width / 2

    b.当手指抬起点在控件左边时,将小球绘制在最左边,touchX = bitmapBall!!.width / 2

5.上面的参数,小球中心点的坐标(ballCenter)和手指触摸点TouchX都是怎么得到的呢?控件既然要与用户进行操作,当然是要重写onTouchEvent方法啦!

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event!!.action) {
            MotionEvent.ACTION_MOVE ->
                touchStateChange(event.x.toInt(), TOUCH_STATE_MOVE)
            MotionEvent.ACTION_UP ->
                touchStateChange(event.x.toInt(), TOUCH_STATE_UP)
        }
        return true
    }

我们只处理了ACTION_MOVE和ACTION_UP,函数fun touchStateChange(mTouchX: Int, touchState: Int)接收值分别是触摸点的getX()坐标和触摸的事件.

    /**
     * 触摸状态改变
     *
     * @param mTouchX 触摸的X坐标
     * @param touchState 触摸状态 ACTION_DOWN ACTION_MOVE ACTION_UP
     */
    private fun touchStateChange(mTouchX: Int, touchState: Int) {
        touchX = mTouchX
        ballCenter = mTouchX
        ballMoveState = touchState
        if (ballMoveState == TOUCH_STATE_UP) {
            if (touchX!! >= switchWidth!! / 2) {
                ballCenter = switchWidth!! - bitmapBall!!.width / 2
                isChecked = true
            } else {
                ballCenter = bitmapBall!!.width / 2
                isChecked = false
            }
            mListener!!.onSwitchChanged(isChecked)
        }
        invalidate()
    }

在这里,我们把触摸事件getX()的坐标赋值给小球中心点的坐标(ballCenter)和手指触摸点TouchX.触摸状态给ballMoveState

6.如果手指抬起,表示一次事件结束,我们把小球中心点的坐标(ballCenter)确定和给调用者提供开启或者关闭的监听.

    fun setOnSwitchListener(listener: onSwitchListener) {
        mListener = listener
    }

    interface onSwitchListener {
        fun onSwitchChanged(ischeck: Boolean)
    }

7.onMeasure方法确定控件的大小

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(switchWidth!!, switchHight!!)
    }

To be continued

你可能感兴趣的:(自定义view)