内容简介:前言:为什么起这个标题呢?因为,这个滑动按钮看起来不是那么的僵硬,哈哈。限于篇幅原因,不会把所有的知识点都讲解一遍,只会挑选一些需要注意的点及不太好理解的地方进行讲解。先放张最后实现效果图,大家可以看着这个效果,思考一下怎么实现的。文章将会选择以下内容进行讲解
前言:为什么起这个标题呢?因为,这个滑动按钮看起来不是那么的僵硬,哈哈。限于篇幅原因,不会把所有的知识点都讲解一遍,只会挑选一些需要注意的点及不太好理解的地方进行讲解。
效果图
先放张最后实现效果图,大家可以看着这个效果,思考一下怎么实现的。
主要讲解的内容
文章将会选择以下内容进行讲解
- 怎样让按钮随手指移动
- 处理越界问题的方法
- 怎样处理回弹(就是没有滑动到指定位置,返回到原点)
- 怎样在滑动到指定位置后禁止滑动
- 让文字跟随按钮移动的方法
- 自定义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”首发
以上所述就是小编给大家介绍的《撸一款”灵动“的滑动按钮》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 优麒麟 20.04 LTS 版本发布,UKUI3.0 灵动转身
- 洞见安全 不止于前丨瑞数信息发布全球第一款双引擎动态WAF――灵动River Safeplus
- 滑动验证码的原理并利用 Vue 实现滑动验证码
- Flink 滑动窗口优化
- Flink 滑动窗口优化
- Flink 滑动窗口优化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Out of their Minds
Dennis Shasha、Cathy Lazere / Springer / 1998-07-02 / USD 16.00
This best-selling book is now available in an inexpensive softcover format. Imagine living during the Renaissance and being able to interview that eras greatest scientists about their inspirations, di......一起来看看 《Out of their Minds》 这本书的介绍吧!