事件分发机制与NestedScrolling机制
一、事件分发机制
1.理论分析
事件分发涉及的是View
和ViewGroup
,相关事件:dispatchTouchEvent
、onInterceptTouchEvent
、OnTouchEvent
,其中onInterceptTouchEvent
只有ViewGroup才有这个方法。
当一个Touch事件到来时,它会从Activity向下依次分发,分发的过程是通过调用ViewGroup或View的dispatchTouchEvent方法实现的。详细的说就是根ViewGroup遍历包含的子view,如果子view处于事件触发范围内,调用每个子View的dispatchTouchEvent,当这个View为ViewGroup的时候,又会进行遍历其内部子view继续调用子view的dispatchTouchEvent。该方法有boolean类型的返回值,当返回true时,事件分发会中断,交给返回true的view的onTouchEvent处理事件,返回false才会执行下一个view的dispatchTouchEvent,如果子view的dispatchTouchEvent都返回false(其实就是view内部的onTouchEvent返回false),那么就会执行到父view的onTouchEvent。
下面是该过程分析的伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//如果是DOWN 则会遍历子view, 找出消耗事件的子view
//mFirstTouchTarget指向找到的子view, 并且子view处理事件
if(mFirstTouchTarget == null){
onTouchEvent() //执行自身OnTouchEvent
}else{
if(intercepted){
//给子view分发cancel事件,并把mFirstTouchTarget置为null
//下次move事件才会真正给自己处理
} else{
mFirstTouchTarget.child.dispatchTouchEvent() //子view处理事件
}
}
下面是简单的事件分发流程图:(针对down事件分析)
总结:其实就是dipatchTouchEvent层层向下分发,分发的过程中涉及onInterceptTouchEvent和OnTouchEvent的调用。对于ViewGroup,当其下面的所有子view都不处理事件的时候,才会调用到自己的onTouchEvent,对于View,dispatchTouchEvent里面就直接调用了onTouchEvent,然后根据返回值决定是否处理该事件。
如果某个父view的onInterceptTouchEvent返回false,之后每次事件都会询问是否拦截 如果onInterceptTouchEvent返回true,那么后续的move up事件将不再询问是否拦截,直接交给自己onTouchEvent处理
如果某个父view的onInterceptTouchEvent返回true,事件不会向下继续分发,会回调自己的onTouchEvent 如果自己的onTouchEvent返回false,则回调给父ViewGroup的onTouchEvent,如果自己的onTouchEvent返回true,那么后续的move up事件都给他
如果ACTION_DOWN遍历了所有view都没有找到处理事件的view,那么MOVE,UP事件将不会分发,即事件存在没有被消费的情况
2.事件冲突解决方法
1.外部拦截法
即父view根据需要对事件进行拦截,重写onInterceptTouchEvent
伪代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if(满足父容器拦截要求){
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
return intercept;
}
有几点需要注意:
1.ACTION_DOWN一定要返回false,否则如果返回true,那么后续的move up事件就会都交给父view处理,事件没有机会到达子view
2.ACTION_MOVE中在父view的需要的时候返回true,然后父view进行事件处理
3.原则上ACTION_UP也需要返回false,因为如果在move中没达到父view的拦截条件,up的时候返回true,那子view就收不到up事件,onClick等事件就无法触发。如果在move中达到了父view的拦截条件,那么up返回什么都无所谓了,因为都会直接交给父view处理。
2.内部拦截法
即子view根据实际情况允许或不允许父view拦截。
伪代码为:
//父view的实现
//父view除了ACTION_DOWN,其他都默认拦截 ACTION_DOWN不拦截主要是为了让子view能收到事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子child
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//不允许父view进行拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
//在不需要自己处理的时候,允许父容器进行拦截
if(不需要自己处理){
getParent().requestDisallowInterceptTouchEvent(false);
}else{
//在这里做自己的事情
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
二、NestedScrolling机制
NestedScrolling机制是为了解决现在事件分发同一个事件中只能有一个view响应事件的问题,在这个机制中,父view和子view可以配合滚动,子view滚动之前会先询问父view是否要进行嵌套滚动,并且如果嵌套滚动的话,消耗多少,剩余的交给子view继续滚动。
现在android中很多组件已经实现了嵌套滚动的机制,比如RecyclerView和NestedScrollView,CoordinatorLayout等,当然我们也可以自定义,如果自定义的话我们首先需要了解下面的几个类:
- NestedScrollingChild 实现嵌套滚动的子view需要实现的接口
- NestedScrollingChildHelper 对实现嵌套滚动的子view提供的嵌套滚动工具类
- NestedScrollingParent 实现嵌套滚动的父view需要实现的接口
- NestedScrollingParentHelper 对实现嵌套滚动的父view提供的嵌套滚动工具类
-
在 ACTION_DOWN 或者ACTION_MOVE 刚开始的时候,Scrolling child 会调用 startNestedScroll 方法
通过 ChildHelper 回调 Scrolling Parent 的 onStartNestedScroll 方法, 如果onStartNestedScroll返回true,则还会调用 parent的 onNestedScrollAccepted -
在 ACTION_MOVE 的时候,Scrolling Child 每 MOVE一段距离,可以调用 dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,
会调用 Parent 的 onNestedPreScroll 方法,然后父容器先根据自身情况滑动一段距离,剩余距离子view继续滑动 -
当 ScrollingChild 滑动完成的时候,可以调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动
会调用 Parent 的 onNestedScroll 方法,父view再根据自身情况处理是否要继续滑动未消费的距离 -
如果需要处理 Fling 动作,我们可以通过 VelocityTracker 获得相应的速度,并在 ACTION_UP 的时候,调用 dispatchNestedPreFling 方法,
通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作, 会调用Parent 的 onNestedPreFling 方法, 剩余距离交给子view fling -
在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,
我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法, 父view根据自身情况处理是否继续fling未消费的距离 -
在 ACTION_UP或者ACTION_CANCEL 的时候,会调用 Scrolling Child 的stopNestedScroll ,
通过 ChildHelper 询问 Scrolling parent 的 onStopNestedScroll 方法
class OuterNestedScrollViewGroup @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyle: Int = 0
) : ConstraintLayout(context, attributeSet, defStyle), NestedScrollingParent3 {
private var mLastConsumedY = 0
private var mNestedScrolling = false
private var mListener: SlideListener? = null
private val mScroller: Scroller = Scroller(context)
private var mCurrentStage = -1
private var mTargetStage = -1
private lateinit var mHeightRatios: FloatArray
fun setPanelSlideListener(listener: SlideListener?) {
mListener = listener
}
fun setMultiHeightRatioFromBottomToTop(args: FloatArray) {
mHeightRatios = args
}
fun go2StageBySmooth(targetStage: Int) {
if (targetStage >= mHeightRatios.size) {
throw RuntimeException("stage must not beyond the max stage")
}
this.scrollY = -getRealHeight()
log("getRealHeight = ${getRealHeight()}")
smoothScrollToStage(targetStage, DEFAULT_SCROLL_DURATION)
this.visibility = View.VISIBLE
}
private fun getRealHeight(): Int {
var totalHeight = 0
for (index in 0 until childCount) {
totalHeight += getChildAt(index).height
}
if (totalHeight > measuredHeight) {
totalHeight = measuredHeight
}
return totalHeight
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
if (!mScroller.isFinished) {
log("abortAnimation")
mScroller.abortAnimation()
}
}
return super.onInterceptTouchEvent(event)
}
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
log("onStartNestedScroll target = $target")
return type == ViewCompat.TYPE_TOUCH
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
log("onNestedScrollAccepted")
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
//上滑dy > 0 下滑dy < 0
//滑出上屏幕scrollY > 0 在屏幕下面scrollY < 0
log("onNestedPreScroll dy = $dy scrollY = $scrollY target = $target")
val scrollToTop = dy > 0 && scrollY < getMaxScrollY() //上滑
val scrollToBottom = //下滑
dy < 0 && scrollY > getStageScrollY(0) && !target.canScrollVertically(-1)
if (scrollToTop || scrollToBottom) {
mNestedScrolling = true
scrollBy(0, dy)
consumed[1] = dy
mLastConsumedY = dy
log("consumed[1] = $dy")
} else {
log("not consume")
}
}
private fun getMaxScrollY(): Int {
return getStageScrollY(mHeightRatios.size - 1)
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
}
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
log("onNestedPreFling velocityY = $velocityY")
return onFingerReleased(velocityY)
}
private fun onFingerReleased(velocityY: Float): Boolean {
if (!mNestedScrolling || scrollY >= getMaxScrollY()) {
return false
}
log("onFingerReleased velocityY = $velocityY")
mNestedScrolling = false
var targetStage = mCurrentStage
when {
(velocityY > 0) -> { //上滑
mHeightRatios.forEachIndexed { stage, _ ->
if (scrollY > getStageScrollY(stage)) {
targetStage = if (stage < mHeightRatios.size - 1) {
stage + 1
} else {
stage
}
}
}
log("onFingerReleased 上滑 targetStage = $targetStage")
}
(velocityY < 0) -> { //下滑
mHeightRatios.forEachIndexed { stage, _ ->
if (scrollY > getStageScrollY(stage)) {
targetStage = stage
}
}
log("onFingerReleased 下滑 targetStage = $targetStage")
}
}
mListener?.onSlideReleased(0f, velocityY, targetStage)
smoothScrollToStage(targetStage, DEFAULT_SCROLL_DURATION)
mLastConsumedY = 0
return true
}
private fun smoothScrollToStage(targetStage: Int, duration: Int) {
if (getRealHeight() <= 0) {
return
}
if (targetStage == mCurrentStage) {
return
}
mTargetStage = targetStage
val y = getStageScrollY(targetStage)
mScroller.startScroll(0, scrollY, 0, y - scrollY, duration)
invalidate()
}
private fun getStageScrollY(stage: Int): Int {
return -(mHeightRatios[stage] * getRealHeight()).toInt()
}
override fun onStopNestedScroll(target: View, type: Int) {
log("onStopNestedScroll target = $target")
onFingerReleased(mLastConsumedY.toFloat())
}
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.currX, mScroller.currY)
invalidate()
} else {
//startScroll 最后一次会返回false, 这里要回调状态
if (mNestedScrolling) {
return
}
setPanelStage(mTargetStage)
}
}
private fun setPanelStage(stage: Int) {
if (mCurrentStage != stage) {
mListener?.onStateChanged(mCurrentStage, stage)
mCurrentStage = stage
}
}
override fun scrollTo(@Px x: Int, @Px y: Int) {
// 控制滚动的边界值:
var yy = y
if (y > getMaxScrollY()) {
yy = getMaxScrollY()
}
if (y < getStageScrollY(0)) {
yy = getStageScrollY(0)
}
val realHeight = getRealHeight()
if (realHeight != 0) {
val offset = -yy / realHeight.toFloat()
log("scrollOffset = $offset")
mListener?.onScrollOffset(offset)
}
super.scrollTo(x, yy)
}
companion object {
const val DEFAULT_SCROLL_DURATION = 600
val mTag = OuterNestedScrollViewGroup::class.simpleName
}
interface SlideListener {
fun onStateChanged(oldStage: Int, newStage: Int)
fun onScrollOffset(offset: Float)
fun onSlideReleased(dx: Float, dy: Float, stage: Int)
}
private fun log(logText: String) {
Log.d(mTag, logText)
}
}
class InnerNestedScrollViewGroup @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyle: Int = 0
) : ConstraintLayout(context, attributeSet, defStyle), NestedScrollingChild3 {
private var mTouchSlop: Int = 0
private var mMinimumFlingVelocity: Int = 0
private var mMaximumFlingVelocity: Int = 0
private var mVelocityTracker: VelocityTracker = VelocityTracker.obtain()
private var mNestedScrollingChildHelper: NestedScrollingChildHelper =
NestedScrollingChildHelper(this)
private var mCurrentY = 0
private var mCurrentX = 0
private var mStartedNestedScroll = false
init {
val viewConfiguration = ViewConfiguration.get(context)
mTouchSlop = viewConfiguration.scaledTouchSlop
log("mTouchSlop = $mTouchSlop")
mMinimumFlingVelocity = viewConfiguration.scaledMinimumFlingVelocity
mMaximumFlingVelocity = viewConfiguration.scaledMaximumFlingVelocity
log("mMinimumFlingVelocity = $mMinimumFlingVelocity")
log("mMaximumFlingVelocity = $mMaximumFlingVelocity")
mNestedScrollingChildHelper.isNestedScrollingEnabled = true
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
log("onInterceptTouchEvent ACTION_DOWN")
mCurrentY = (event.y + 0.5f).toInt()
mCurrentX = (event.x + 0.5f).toInt()
}
MotionEvent.ACTION_MOVE -> {
log("onInterceptTouchEvent ACTION_MOVE")
val dx = (event.x + 0.5f).toInt() - mCurrentX
val dy = (event.y + 0.5f).toInt() - mCurrentY
if (abs(dy) >= mTouchSlop && abs(dy) > abs(dx)) {
//针对于子view有处理事件的,这里要进行拦截,子view如果不想被拦截可以调用parent.requestDisallowInterceptTouchEvent(false)
return true
}
}
}
return super.onInterceptTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val pointerId = event.getPointerId(event.actionIndex)
addMovement(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//所有子view都不处理down touch事件,则MOVE事件不会再向下传递
//所以要在本次DOWN事件这里返回true, 然后后面的move都直接交给该view onTouchEvent处理
log("onTouchEvent ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
log("onTouchEvent ACTION_MOVE")
var dy = mCurrentY - (event.y + 0.5f).toInt() //上滑dy > 0 下滑dy < 0
dy = if (dy > 0) {
max(0, dy - mTouchSlop)
} else {
min(0, dy + mTouchSlop)
}
if (abs(dy) > 0) {
if (!mStartedNestedScroll) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
}
dispatchNestedPreScroll(
0,
dy,
null,
null,
ViewCompat.TYPE_TOUCH
)
}
}
MotionEvent.ACTION_UP -> {
log("onTouchEvent ACTION_UP")
if (mStartedNestedScroll) {
mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity.toFloat())
val yVelocity = mVelocityTracker.getYVelocity(pointerId)
if (abs(yVelocity) > mMinimumFlingVelocity) {
dispatchNestedPreFling(0f, -yVelocity)
}
stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
MotionEvent.ACTION_CANCEL -> {
log("onTouchEvent ACTION_CANCEL")
if (mStartedNestedScroll) {
stopNestedScroll(ViewCompat.TYPE_TOUCH)
}
}
}
return true
}
private fun addMovement(event: MotionEvent) {
val evert = MotionEvent.obtain(event)
evert.setLocation(event.rawX, event.rawY)
mVelocityTracker.addMovement(evert)
evert.recycle()
}
private fun clearVelocityTracker() {
mVelocityTracker.clear()
}
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
log("dispatchNestedPreFling velocityY = $velocityY")
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY)
}
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int,
consumed: IntArray
) {
mNestedScrollingChildHelper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type
)
}
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int
): Boolean {
log("dispatchNestedPreScroll dyConsumed = $dyConsumed dyUnconsumed = $dyUnconsumed")
return mNestedScrollingChildHelper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type
)
}
override fun dispatchNestedPreScroll(
dx: Int,
dy: Int,
consumed: IntArray?,
offsetInWindow: IntArray?,
type: Int
): Boolean {
log("dispatchNestedPreScroll dy = $dy")
return mNestedScrollingChildHelper.dispatchNestedPreScroll(
dx,
dy,
consumed,
offsetInWindow,
type
)
}
override fun stopNestedScroll(type: Int) {
log("stopNestedScroll")
mStartedNestedScroll = false
clearVelocityTracker()
mNestedScrollingChildHelper.stopNestedScroll(type)
}
override fun setNestedScrollingEnabled(enabled: Boolean) {
log("setNestedScrollingEnabled enabled = $enabled")
mNestedScrollingChildHelper.isNestedScrollingEnabled = enabled
}
override fun isNestedScrollingEnabled(): Boolean {
log("isNestedScrollingEnabled")
return mNestedScrollingChildHelper.isNestedScrollingEnabled
}
override fun hasNestedScrollingParent(type: Int): Boolean {
log("hasNestedScrollingParent")
return mNestedScrollingChildHelper.hasNestedScrollingParent(type)
}
override fun startNestedScroll(axes: Int, type: Int): Boolean {
log("startNestedScroll")
mStartedNestedScroll = true
return mNestedScrollingChildHelper.startNestedScroll(axes, type)
}
companion object {
val mTag = InnerNestedScrollViewGroup::class.simpleName
}
private fun log(logText: String) {
Log.d(mTag, logText)
}
}