Android 自定义滑动控件
简介
通过自定义滑动控件进一步熟悉触摸事件的分发,以及常见的滑动的实现,目的的达到能够完成简单的自定义滑动控件,以及能够读懂第三方开源的自定义滑动组件。通过工程实现一步一步的实现滑动组件,工程源码 https://github.com/CodeKitBox/Control.git
滑动
在触摸事件分发的过程中,UI控件通过响应 MotionEvent
来接收到触摸事件。在不考虑多指触摸的前提下,触摸事件的典型事件有 MotionEvent.ACTION_DOWN
,MotionEvent.ACTION_MOVE
,MotionEvent.ACTION_UP
,MotionEvent.ACTION_CANCEL
正常情况下,手指触摸屏幕会发生一些列的事件,事件为 MotionEvent.ACTION_DOWN
->N* MotionEvent.ACTION_MOVE
->MotionEvent.ACTION_UP
。当N为0 ,或者 MotionEvent.ACTION_MOVE
的情况下滑动的距离小于滑动的阈值,事件为点击事件。在N 大于0,且MotionEvent.ACTION_MOVE
的情况下滑动的距离大于滑动的阈值,事件为滑动事件。
自定义控件实现滑动
class MyScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
LinearLayout(context, attrs, defStyleAttr){
/**
LinearLayout 布局对于超过容器尺寸的,不调用子控件的布局接口,因此子类重写,调用所有子空间的布局接口
**/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int){
...
}
/**
分发触摸事件
1. 当函数 onInterceptTouchEvent 返回值为true的时候,由控件本身的 onTouchEvent 来处理触摸事件,其返回值影响了dispatchTouchEvent的返回值。
2. 当函数 onInterceptTouchEvent 返回值为false的时候,先分发给子控件的 dispatchTouchEvent来处理事件,如果子控件的dispatchTouchEvent返回值为false, 然后再由控件本身的 onTouchEvent 来处理触摸事件,其返回值影响了dispatchTouchEvent的返回值。
@return true: 控件消费触摸事件 false: 控件不消费触摸事件
**/
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
val superConsume = super.dispatchTouchEvent(ev)
// 控件需要处理触摸事件,滑动事件由自身来处理,点击事件由子类来实现,因此直接返回true
return true
}
/**
1. 当返回值为 true 是,由控件本身的 onTouch 函数来处理事件,否则通过分发流程来处理。
2. 是否返回 true 的规则是滑动距离是否满足大于阈值。
**/
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean{
...
}
/**
1. 在事件类型为 MotionEvent.ACTION_MOVE 需要进行滑动处理。
2. 存在由系统分发到控件本身的onTouchEvent函数,此时 onInterceptTouchEvent 返回值为 false,因此
onInterceptTouchEvent 函数中的一些前置操作也要在 onTouchEvent中执行。
**/
override fun onTouchEvent(event: MotionEvent){
val vtev = MotionEvent.obtain(event)
when(vtev.actionMasked){
MotionEvent.ACTION_DOWN->{}
MotionEvent.ACTION_MOVE->{
val dy = differY(event.y.toInt())
// 通过系统分发到这里,判断是否可以滑动
if(abs(dy) > touchSlop && !mIsBeingDragged){
mIsBeingDragged = true
parent?.requestDisallowInterceptTouchEvent(true)
}
if (mIsBeingDragged){
// 调用View的接口判断滑动
// 参数 deltaX ,deltaY 指的是滑动的偏移量
// 参数 scrollX scrollY 指的是已经滑动的距离
// 参数 scrollRangeX scrollRangeY 指的是滑动的范围
// 参见 maxOverScrollX maxOverScrollY 指的是越界滑动的距离
// isTouchEvent 系统中此参数没有使用
// 返回值 true 标识达到了最大越界,在惯性滑动中使用
// 调用onOverScrolled 实现真正的滑动
//println("dy = $dy ;scrollY = $scrollY ")
overScrollBy(0,dy,0,scrollY,0,scrollRange,0,0,true)
// 记录触摸点坐标
saveLocation(event.x.toInt(),event.y.toInt())
}
}
MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL->{}
}
}
override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
// 在ScrollView 中对 clampedX == true clampedY == true 进行了处理
// 滑动到指定坐标
super.scrollTo(scrollX, scrollY)
}
通过以上自定义的滑动控件可以实现简单的滑动组件。滑动组件的本质时通过 View#scrollBy
,View#scrollTo
实现控件内容的滑动。与其他的类无关。
惯性滑动
在生活中,我们开车行驶一段事件,刹车停止,一般都有一个制动距离,将这一场景模拟到滑动过程中,因此手指离开屏幕也存在一个惯性滑动。模拟这一惯性滑动可以收到更好的用户体验,因此我们使用系统的滑动控件的时候,手指离开屏幕也控件也会继续滑动,就是这个原因。
class MyScrollFlingView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
LinearLayout(context, attrs, defStyleAttr){
// 普通的滑动不需要处理这两个类
//<editor-fold desc="惯性或者平滑滑动相关属性">
// 滑动辅助类,在惯性滑动,平滑滑动时使用
private val mScroller = Scroller(context)
// 速度模拟相关类
private var mVelocityTracker: VelocityTracker?= null
//</editor-fold>
// 惯性滑动是在 MotionEvent.ACTION_UP 中事件处理
override fun onTouchEvent(event: MotionEvent): Boolean{
// 初始化速度控制器
initVelocityTrackerIfNotExists()
when(vtev.actionMasked){
//...
MotionEvent.ACTION_UP,MotionEvent.ACTION_CANCEL->{
// 开始惯性滑动
if(mIsBeingDragged){
mVelocityTracker?.let {
it.computeCurrentVelocity(1000)
// 判断是否支持惯性滑动,参考系统源码
println("惯性滑动 ${it.xVelocity};${it.xVelocity}; minFling =$minFling")
// 惯性滑动的速度大于系统最小的惯性滑动速度,执行惯性滑动
if(abs(it.yVelocity) > minFling){
val velocityY = -(it.yVelocity.toInt())
// 判断是否可以滑动
val canFling = (scrollY > 0 || velocityY > 0) &&
(scrollY < getScrollRange() || velocityY < 0)
println("canFling == $canFling")
/**
* 参数 startX, startY 起始的滑动距离
* 参数 velocityX velocityY 滑动的速度
* 参数 minX minY 最小的滑动距离
* 参数 maxX maxY 最大的滑动就离
*/
if(canFling){
mScroller.fling(0,scrollY,
0,velocityY, 0,0, minFling, getScrollRange())
// 通知界面刷新 在onDraw 的时候调用 computeScroll 方法
invalidate()
}
}
}
}
// 设置为非拖动状态
mIsBeingDragged = false
// 记录触摸点坐标
saveLocation(event.x.toInt(),event.y.toInt())
recycleVelocityTracker()
}
}
}
override fun computeScroll() {}
}
惯性滑动的原理是:当手机离开屏幕产生了 MotionEvent.ACTION_UP
,按照以下流程实现惯性滑动:
- 通过之前滑动的速度,计算出惯性滑动的速度
- 根据惯性滑动的速度,是否大于系统惯性滑动的阈值决定是否需要执行惯性滑动。
- 执行惯性滑动,首先通过
Scroller#fling
接口,记录惯性滑动需要的参数,调用invalidate
通知界面开始重绘。 - 在界面开始重绘
onDraw
中调用computeScroll
,computeScroll
函数中进行View的滑动距离,同时再次通知View进行重绘。
平滑
对控件进行滑动的方式有
- 通过手指触摸屏幕来实现,以上的源码已实现该流程
- 通过代码来实现,常见的操作有对
RecyclerView
滑动到顶部,滑动到底部,滑动到指定的item项,如果使用scrollTo
接口,屏幕会立即滑动到指定位置,会出现闪一下的情况,因此需要通过代码实现在一定的事件内滑动到指定位置,因此出现了平滑的概念。
实现平滑的代码
class MyScrollSmoothView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
LinearLayout(context, attrs, defStyleAttr){
fun smoothScrollBy(dx: Int, dy: Int) {
var dy = dy
if (childCount == 0) {
// Nothing to do.
return
}
val duration: Long = AnimationUtils.currentAnimationTimeMillis() - mLastScroll
if (duration >ANIMATED_SCROLL_GAP) {
val maxY = getScrollRange()
val scrollY: Int = scrollY
dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY
mScroller.startScroll(scrollY, scrollY, 0, dy,ANIMATED_SCROLL_GAP)
postInvalidateOnAnimation()
} else {
if (!mScroller.isFinished) {
mScroller.abortAnimation()
}
scrollBy(dx, dy)
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis()
}
}
实现平滑的原理
- 首先通过
Scroller#startScroll
接口,记录惯性滑动需要的参数,调用invalidate
通知界面开始重绘 - 在界面开始重绘
onDraw
中调用computeScroll
,computeScroll
函数中进行View的滑动距离,同时再次通知View进行重绘
总结
通过以上的源码实现了一个自定义的滑动控件,熟悉了事件的分发流程。
本文地址:https://blog.csdn.net/cxmfzu/article/details/114207345
上一篇: python实现ROA算子边缘检测算法
下一篇: Python批量获取基金数据的方法步骤