0. 参考资料
来自鸿洋的博客的例子,非常感谢:
http://blog.csdn.net/lmj623565791/article/details/43649913
整体代码:
https://github.com/zhaoyubetter/KotlinAndroidDemo/blob/master/widget/src/main/java/test/com/widget/nested/StickyNavVerticalLayout2.kt
1. 嵌套滑动场合
如上面的例子,当内层的view,如:list,滑动到顶部时,即 firstChild.getTop = 0 的,我们需要将list的事件,转而给外层(怎么给呢?),让外层,去消耗,这点稍有些麻烦(鸿洋的博客中有解决);在Android的事件分发中,如果找到了target了,就一直会把后续的事件源源不断的给target;
而此时,一般我们需要抬起手指,然后重新下拉,这个时候,外层就收到了事件了;
那可以实现嵌套滑动吗?答案是可以的,我们可以用 nestedScrolling 的api来做,这块,我还暂未了解。所有这里,采用原始的事件分发方案
来解决这个问题;
2. 准备开始吧
这里我简化了一下代码,仅供参考;
2.1 简化了布局文件如下
3. 过程实现
一步一步来实现;
3.1 先实现界面布局
创建StickyNavVerticalLayout
继承自LinearyLayout,并添加相应的一些代码,注释在代码里:
class StickyNavVerticalLayout2(context: Context, attrs: AttributeSet?, defAttrStyle: Int) : LinearLayout(context, attrs, defAttrStyle) {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
// --- 内部 View 相关成员变量
private var top: View? = null
private var nav: View? = null
private var scrollView: View? = null
private var topHeight: Int? = 0 // top的高度
init {
}
override fun onFinishInflate() {
super.onFinishInflate()
top = findViewById(R.id.id_stickynavlayout_topview)
nav = findViewById(R.id.id_stickynavlayout_indicator)
scrollView = findViewById(R.id.id_stickynavlayout_scrollview)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
scrollView?.layoutParams?.height = measuredHeight.minus(nav?.measuredHeight ?: 0)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
topHeight = top?.measuredHeight?: 0
}
}
现在的界面效果:
底部的scrollView可以滑动,但是不能滑动到底部;
3.2 添加手势处理逻辑
实现 onInterceptTouchEvent
, onTouchEvent
, 注意,在这里,我们这2个方法,都返回true,表示事件由自己处理,不传给子view(scrollView 收不到事件)
// --- 事件操作相关的成员变量
private var lastY: Int = 0
private var isDrag = false // 是否拖拽
private var scroller: Scroller = Scroller(getContext())
private var velocityTracker: VelocityTracker? = null
private var touchSlop: Int = 0
private var maxFlingVelocity: Int = 0
private var minFlingVelocity: Int = 0
init {
val config = ViewConfiguration.get(context)
touchSlop = config.scaledTouchSlop
maxFlingVelocity = config.scaledMaximumFlingVelocity
minFlingVelocity = config.scaledMinimumFlingVelocity
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
val y = it.y
initVelocityTracker()
velocityTracker?.let { it.addMovement(event) } // 添加运动轨迹
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastY = y.toInt()
}
MotionEvent.ACTION_MOVE -> {
val dy = y - lastY
if (!isDrag && Math.abs(dy) > touchSlop) {
isDrag = true
}
if (isDrag) {
scrollBy(0, -dy.toInt()) // 反向取反
}
lastY = y.toInt()
}
MotionEvent.ACTION_UP -> { // 抬起,运动轨迹判断,是否fling
isDrag = false
velocityTracker?.let {
it.computeCurrentVelocity(1000, maxFlingVelocity?.toFloat())
if (Math.abs(it.yVelocity) > minFlingVelocity) {
fling(-it.yVelocity.toInt())
}
}
releaseVelocity()
}
MotionEvent.ACTION_CANCEL -> {
isDrag = false
releaseVelocity()
}
}
}
return true
//return super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true
//return super.onInterceptTouchEvent(ev)
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(0, scroller.currY)
invalidate()
}
}
/**
* 边界处理
*/
override fun scrollTo(x: Int, y: Int) {
var tmpY = y
if (y < 0) tmpY = 0
if (y > topHeight) tmpY = topHeight
super.scrollTo(x, tmpY)
}
private inline fun fling(velocityY: Int) {
scroller.let {
it.fling(0, scrollY, 0, velocityY, 0, 0, 0, topHeight) // 滑翔
invalidate()
}
}
private inline fun initVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
}
private inline fun releaseVelocity() {
velocityTracker?.let {
it.recycle()
velocityTracker = null
}
}
大部分说明 在鸿洋的 博客里写的很详细了,这里就不再叙述了;
主要目的是,实现 StickyNavVerticalLayout2 的整体的滑动,滑翔等;
可以看到 scrollview
不能单独滑动
实现效果为:
3.3 拦截事件的处理
去掉 onTouchEvent
的 return true;
重写onInterceptTouchEvent
方法,让其在特定的情况下拦截事件,需要拦截分为2种情况:
- topView 可见时拦截;
- topview不可见,并且 内部的
scrollView
在顶部,并且还在下拉的状态下,进行拦截;
我们这里使用ViewCompat
来进行View是否还可以继续滚动的判断;我们来看代码:
// 先添加成员变量,记录 top 的可见状态
private var topHide = false
override fun scrollTo(x: Int, y: Int) {
var tmpY = y
if (y < 0) tmpY = 0
if (y > topHeight) tmpY = topHeight
super.scrollTo(x, tmpY)
topHide = scrollY == topHeight // 更新 topHide
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
.....省略代码.....
// return true
return super.onTouchEvent(event)
}
/**
* 拦截判断
*/
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val y = ev.y
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = y.toInt()
MotionEvent.ACTION_MOVE -> { // 重点
val dy = y - lastY
if (Math.abs(dy) > touchSlop) {
// topView 可见 || (topView不可见 && scrollView不能再下拉 && 继续下拉)
if (!topHide || (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0)) {
lastY = y.toInt()
isDrag = true
initVelocityTracker()
velocityTracker?.let {
it.addMovement(ev)
}
return true
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isDrag = false
releaseVelocity()
}
}
return super.onInterceptTouchEvent(ev)
}
到现在,已经完成了基本的需求了,但是嵌套滑动还是不行,不连贯,没有那种一口气 硬扯到底 的 感觉,需要放开,再拉 ;
效果图,如下:
这个时候,就回到了,开头留下的问题了,如何实现一拉到底中间整个过程没有间断;我们需要使用 dispatchTouchEvent 这个方法了;
3.4 重新 dispatchTouchEvent 嵌套的滑动实现
上面的问题,在于,当事件被 子 view 接收后,后续的事件,都会跑到子view;但事件的传递,都是从父到子的过程,事件的传递会经过父的dispatch,通过这个方法,将事件在父与子View中根据条件来传递,从而实现嵌套滑动;
private var isInControl = false // 是否已dispatch
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
MotionEvent.ACTION_MOVE -> { // 进行判断,是否重发事件
val dy = ev.y - lastY
// 头不可见,继续下拉,重发事件
if (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0 && !isInControl) {
isInControl = true
ev.action = MotionEvent.ACTION_CANCEL
val ev2 = MotionEvent.obtain(ev)
dispatchTouchEvent(ev)
ev2.action = MotionEvent.ACTION_DOWN
return dispatchTouchEvent(ev2)
}
}
}
return super.dispatchTouchEvent(ev)
}
我们需要在 onTouchEvent方法加入以下代码片段,即:在边界时,将MOVE事件转换成 DOWN事件,重新进行分发;
=== > onTouchEvent 方法中修改
MotionEvent.ACTION_MOVE -> {
val dy = y - lastY
if (!isDrag && Math.abs(dy) > touchSlop) {
isDrag = true
}
if (isDrag) {
scrollBy(0, -dy.toInt()) // 反向取反
}
// 如果滑到顶了,将事件转换成点击事情,发送
if (scrollY == topHeight) {
event.action = MotionEvent.ACTION_DOWN
dispatchTouchEvent(event)
isInControl = false
}
lastY = y.toInt()
}
scroller的优化,在onInterceptTouchEvent() 与 onTouchEvent中分别 加入:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val y = ev.y
when (ev.action) {
MotionEvent.ACTION_DOWN -> lastY = y.toInt()
MotionEvent.ACTION_MOVE -> { // 重点
// 惯性未结束,拦截事件
if(!scroller.isFinished) {
return true
}
.......
.......
// onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
// .... 省略代码
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastY = y.toInt()
if(!scroller.isFinished) { // 未结束时,结束scroller
scroller.abortAnimation()
return true
}
}
// ACTION_CANCEL时时,取消scroller
MotionEvent.ACTION_CANCEL -> {
isDrag = false
if(!scroller.isFinished) {
scroller.abortAnimation()
}
releaseVelocity()
}
最终效果如下:
水平滑动(加深印象)
效果如下: