撸一款”灵动“的滑动按钮

栏目: Android · 发布时间: 5年前

内容简介:前言:为什么起这个标题呢?因为,这个滑动按钮看起来不是那么的僵硬,哈哈。限于篇幅原因,不会把所有的知识点都讲解一遍,只会挑选一些需要注意的点及不太好理解的地方进行讲解。先放张最后实现效果图,大家可以看着这个效果,思考一下怎么实现的。文章将会选择以下内容进行讲解

前言:为什么起这个标题呢?因为,这个滑动按钮看起来不是那么的僵硬,哈哈。限于篇幅原因,不会把所有的知识点都讲解一遍,只会挑选一些需要注意的点及不太好理解的地方进行讲解。

效果图

先放张最后实现效果图,大家可以看着这个效果,思考一下怎么实现的。

撸一款”灵动“的滑动按钮

主要讲解的内容

文章将会选择以下内容进行讲解

  • 怎样让按钮随手指移动
  • 处理越界问题的方法
  • 怎样处理回弹(就是没有滑动到指定位置,返回到原点)
  • 怎样在滑动到指定位置后禁止滑动
  • 让文字跟随按钮移动的方法
  • 自定义View添加阴影的方法

这里会选择按钮初始位置在中间的这种情况来讲解,因为,按钮初始位置在左边的时候就是按钮位置在中间的时候一种状态。

让按钮随手指移动

要想让按钮随着手指的移动而移动,就需要重写View的 onTouchEvent 方法,在这个方法中可以监听手指的几个动作,如手指的“按下”、“滑动”、“抬起”,

捕获到这些动作后,就可以针对每个动作做相应的处理,最终达到让按钮随手指移动的效果。具体的代码如下

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            //手指按下
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			//手指移动
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                //刷新view
                postInvalidate()

            }
			//手指抬起
            MotionEvent.ACTION_UP -> {
                
            }
        }
        return super.onTouchEvent(event)
    }
复制代码

简单解释一下上面的代码,在手指按下的时候,获取手机按下位置的坐标,用 clickX 变量保存按下的X轴的位置 (后面会用到) ,在手指滑动的时候用全局变量 slideX 来保存滑动到的X的坐标,然后刷新view。手指抬起的动作这里不用处理。上面的代码只是获取到了手指移动的X轴的坐标,下面就要通过View的 onDraw 方法来绘制中间按钮,代码如下

private fun drawSnake(canvas: Canvas) {
    //计算圆的半径
        val circleRadius = mSnakeRadius.toFloat() - mShadowRadius / 2
		//计算中间按钮的X坐标,在初始的时候,slideX的值为0
        val circleCenter = if (slideX == 0f) {
            if (mSlideState == SlideState.INIT_LEFT) {
                //按钮位于左边的时候圆心的X坐标
                mSnakeRadius + mShadowRadius / 2
            } else {
                //按钮在中间的时候圆心的X坐标,mResultWidth为View的宽度
                mResultWidth / 2.toFloat()

            }
        } else {
            //手指滑动后
            slideX
        }
		//这里根据手指移动到的位置来确定圆形的X坐标
        canvas.drawCircle(circleCenter, mResultHeight / 2.toFloat(), circleRadius, mSnakePaint)

    }
复制代码

上面的每行代码都进行了注释,这里就不再讲解每句代码的了。

处理越界问题的方法

如果你是按上面的代码来让按钮跟随手指进行移动的话,当移动到边缘的时候你会发现移动的按钮超出背景了!那这是什么问题呢?因为,这里是把手指移动的位置的X坐标作为圆心的X坐标了,当手指移动到边缘的时候,圆心的X坐标也在边缘了,所以,就会出现按钮超出背景的问题了。那么该怎么解决这个问题呢?其实很简单,就是当手指移动的位置的X坐标大于View的总长度减去按钮半径长度的时候,将 slideX 变量重新赋值。代码如下

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            //手指按下
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			//手指移动
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    //防止超出右边界
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    //防止超出左边界
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                //刷新view
                postInvalidate()

            }
			//手指抬起
            MotionEvent.ACTION_UP -> {
                
            }
        }
        return super.onTouchEvent(event)
    }
复制代码

从上面的代码中可以可看到,处理按钮超出边界的方法是, 当圆心的X的坐标将要大于或小于要求的坐标的时候,不直接让手指的X的坐标作为圆心的坐标了,而是重新计算圆心X轴的坐标。

处理回弹

这部分要处理的就是,当按钮没有移动到目标位置时,抬起手指,是让按钮返回到初始的位置,还是让按钮到达目标位置。处理这个问题其实也很简单,就是需要事先设定一个位置,当抬起手指的时候判断圆心的X的坐标大于这个位置还是小于这个位置,如果大于就直接将按钮移到目标位置,如果小于就将按钮移动到初始位置。如下图

撸一款”灵动“的滑动按钮

这里规定的位置如上图,这个位置距离黑色背景边界的长度刚好等于按钮的半径。当往左边滑动时,手指抬起的时候,如果圆心的X坐标大于2倍的按钮的半径即直径的话,就回到初始位置,否则滑动到左边,往右边滑动时的判断同理。具体的代码如下

override fun onTouchEvent(event: MotionEvent): Boolean {
      
        when (event.action) {
            //手指按下
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			//手指移动
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    //防止超出右边界
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    //防止超出左边界
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                //刷新view
                postInvalidate()

            }
			//手指抬起
            MotionEvent.ACTION_UP -> {
                //判断左滑还是右滑
                if (slideX > center) {
                    //按钮的边界超出右边规定的位置
                        if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
                            //直接滑到目标点
                            slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
                            mSlideState = SlideState.RIGHT_FINISH
                            if (slideListener != null) {
                                slideListener!!.onSlideRightFinish()
                            }
                        } else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }

                    } else if (slideX < center) {
                        if (slideX < 2 * mSnakeRadius + mShadowRadius) {
                            //直接滑到目标点
                            slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
                            mSlideState = SlideState.LEFT_FINISH
                            if (slideListener != null) {
                                slideListener!!.onSlideLiftFinish()
                            }

                        } else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }
                    } else {
                        //松手后回到原点
                        slideAnimate(slideX.toInt(), center)
                    }
            }
        }
        return super.onTouchEvent(event)
    }
复制代码

上面的代码中 slideAnimate 这个方法是添加位移动画,代码如下

private fun slideAnimate(start: Int, end: Int) {
        val valueAnimator = ValueAnimator.ofInt(start, end)
        valueAnimator.addUpdateListener { animation ->
            val animatedValue = animation.animatedValue as Int
            slideX = animatedValue.toFloat()
            postInvalidate()
        }
        valueAnimator.start()
    }
复制代码

怎样在滑动到指定位置后禁止滑动

什么叫滑动到指定位置呢?这里拿按钮在中间时候的View举例,比如当按钮滑动到两头的时候,就是到指定位置了,这时抬起手指,在下次再滑动按钮的时候就不能滑动了。这个问题也很好解决,解决的方法也有很多,本文采用的方法就是初始化一个成员变量,每次进入 onTouchEvent 方法时,首先判断这个变量,条件成立则直接返回 false 。不成立就继续响应手指的动作,当按钮滑动到指定的位置的就改变这个变量的值,使下次再进入这个方法时条件成立。代码如下

override fun onTouchEvent(event: MotionEvent): Boolean {
       //滑动完成后禁止滑动
        if (mSlideState == SlideState.LEFT_FINISH || mSlideState == SlideState.RIGHT_FINISH) {
            return false
        }
        when (event.action) {
            //手指按下
            MotionEvent.ACTION_DOWN -> {
                val clickX = event.x
            }

			//手指移动
            MotionEvent.ACTION_MOVE -> {
                slideX = event.x
                
                if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
                    //防止超出右边界
                    slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
                } else if (slideX < mSnakeRadius + mShadowRadius / 2) {
                    //防止超出左边界
                    slideX = mSnakeRadius+mShadowRadius / 2
                }
                //刷新view
                postInvalidate()

            }
			//手指抬起
            MotionEvent.ACTION_UP -> {
                //判断左滑还是右滑
                if (slideX > center) {
                    //按钮的边界超出右边规定的位置
                        if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
                            //直接滑到目标点
                            slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
                            //改变变量的状态
                            mSlideState = SlideState.RIGHT_FINISH
                            if (slideListener != null) {
                                slideListener!!.onSlideRightFinish()
                            }
                        } else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }

                    } else if (slideX < center) {
                        if (slideX < 2 * mSnakeRadius + mShadowRadius) {
                            //直接滑到目标点
                            slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
                            //改变变量的状态
                            mSlideState = SlideState.LEFT_FINISH
                            if (slideListener != null) {
                                slideListener!!.onSlideLiftFinish()
                            }

                        } else {
                            setInitText()
                            slideAnimate(slideX.toInt(), center)
                        }
                    } else {
                        //松手后回到原点
                        slideAnimate(slideX.toInt(), center)
                    }
            }
        }
        return super.onTouchEvent(event)
    }
复制代码

让文字跟随按钮移动

这部分可能算是自定义这个View最复杂的部分了,其实只要想明白了,也不复杂。这里就拿按钮从中间往右滑来讲解,按钮从中间往左边滑计算方法和往右滑的差不多,先看图

撸一款”灵动“的滑动按钮

好了,现在根据上面的图,就可以算出初始和最终的文字的X轴的位置了,

初始状态X = (mResultWidth / 2- mSnakeRadius)/2

最终状态X=mResultWidth / 2

mResultWidth为View的宽度,mSnakeRadius为按钮的半径

知道了初始和最终的X轴的位置,下面就是根据按钮移动的距离来改变文字的X作坐标了

val distance = end - start
val resultLeftX = start + distance * proportion
end和start就是最终状态X和初始状态X,resultLeftX就是要画的文字的X坐标,proportion就是百分比,根据公式可知这个proportion值的变化范围值从0到1的。

现在,最重要的一点就是怎么根据按钮的移动来改变上面公示中的 proportion ,应该怎么办呢?继续看图

撸一款”灵动“的滑动按钮

这个 proportion 就是上图中的x比y的值,上图中中间的一条线就是 slideX ,它是根据按钮的滑动不断变化的,这样就能满足 proportion 的值从0到1的条件了。计算的公式如下

proportion = (slideX - halfResultWidth) / (mResultWidth - mSnakeRadius - halfResultWidth - mShadowRadius / 2)

这部分具体的代码如下

private fun drawInnerText(canvas: Canvas) {
		//这部分是裁剪画布,因为当viwe加阴影的时候,文字会越界,想看效果的话,可以把裁剪画布的代码注释掉
        canvas.save()
    //裁剪的大小是黑色背景的大小,形状是矩形
        canvas.clipRect(
            mShadowRadius,
            mShadowRadius,
            mResultWidth.toFloat() - mShadowRadius,
            mResultHeight.toFloat() - mShadowRadius
        )
        val fontMetrics = mInnerTextPaint.fontMetrics

        //画圆环内的文字
        val baseline = mResultHeight / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
        //文字的起始位置
        val halfResultWidth = mResultWidth / 2
        val start = (halfResultWidth - mSnakeRadius) / 2
        //文字的最终位置
//        val end = (mResultWidth - 2 * mSnakeRadius) / 2
        val end = mResultWidth / 2
        //距离
        val distance = end - start
    //根据状态不同设置按钮的初始位置
        var proportion = if (mSlideState == SlideState.INIT_LEFT) {
            -1f
        } else {
            0f

        }

        if (slideX != 0f) {
            //计算比例来移动文字的距离
            proportion = (slideX - halfResultWidth) / (mResultWidth - mSnakeRadius - halfResultWidth - mShadowRadius / 2)
        }

        val resultLeftX = start + distance * proportion
//画按钮左边的文字
        canvas.drawText(
            leftContent,
            resultLeftX,
            baseline,
            mInnerTextPaint
        )
        val resultRightX =
            halfResultWidth + (mSnakeRadius / 2) + (halfResultWidth / 2) + (distance * proportion)
    //画按钮右边的文字
        canvas.drawText(
            rightContent,
            resultRightX,
            baseline,
            mInnerTextPaint
        )
        canvas.restore()
    }
复制代码

这里重点解释一下下面的一段代码

private fun drawInnerText(canvas: Canvas) {

        canvas.save()
        canvas.clipRect(
            mShadowRadius,
            mShadowRadius,
            mResultWidth.toFloat() - mShadowRadius,
            mResultHeight.toFloat() - mShadowRadius
        )
       //...省略部分代码
        canvas.restore()
    }
复制代码

**这段代码是为了解决给View画阴影的时候,文字移动超出背景仍然显示的问题。**当view添加阴影时,可以把这段代码注释掉,看下有什么不同。还有一点就是在指定位置写文字的问题,大家可以参考我的这篇文章 Android自定义View之定点写文字

自定义View添加阴影的方法

添加阴影的方法就是为画笔设置 setShadowLayer ,我们来看下这个方法

setShadowLayer(float radius, float dx, float dy, int shadowColor)
复制代码

方法中有四个参数,四个参数的作用如下

  • radius:设置阴影的模糊半径,其实就是设置阴影的大小,当为0时没有阴影
  • dx:阴影在X轴上的偏移量
  • dy:阴影在Y轴上的偏移量
  • shadowColor:阴影的颜色

注:只为画笔设置这个是不起作用的,还需要关闭硬件加速。

结束语

文章到这里,已经把需要解决的问题都解决了。其实除了文中说的这些,还有其他的一些需要注意的细节,如只有点击在按钮范围内才能滑动、状态改变时的回调等。这些细节都在代码中进行了处理, 可以点击这里获取源码 ,觉得不错的话,就顺手点个star吧!

本文已由公众号“AndroidShared”首发

撸一款”灵动“的滑动按钮
扫码关注公众号,回复“获取资料”有惊喜

以上所述就是小编给大家介绍的《撸一款”灵动“的滑动按钮》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

运营制胜

运营制胜

张恒 / 电子工业出版社 / 2016-10-1 / 65

《运营制胜——从零系统学运营构建用户增长引擎》主要从内容运营、用户运营、推广运营三个方向来介绍产品运营方面的知识。 其中内容运营主要介绍了内容生成的机制、内容方向设定、内容输出、内容生产引擎、内容推荐机制、数据如何驱动内容运营、内容运营的KPI 设定、建立内容库、内容的赢利模式。用户运营主要介绍了产品的冷启动、获得种子用户及早期用户、建立用户增长引擎、利用心理学引爆产品用户增长、增加用户活跃......一起来看看 《运营制胜》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换