0.3 view 滑动冲突

Android 触摸事件分发与滑动冲突处理 — 完整文档

一、核心知识点总结

1. 事件分发流程(Event Flow)

  • 事件传递路径:Activity → Window (DecorView) → ViewGroup → View
  • 若所有子 View 均未消费事件,最终会回传到 Activity.onTouchEvent()
  • 一个完整事件序列 = 1 个 ACTION_DOWN + 多个 ACTION_MOVE + 1 个 ACTION_UP

2. 关键规则

  • 通常一个事件序列应由同一个 View 消费到底。
  • 若 View 在 ACTION_DOWN 返回 false,则后续事件不会交给它。
  • ViewGroup 默认不拦截事件(onInterceptTouchEvent 返回 false)。
  • 只有当 View 的 clickablelongClickabletrue 时,onTouchEvent 才会返回 true(即消费事件)。
  • OnTouchListener.onTouch() 优先于 onTouchEvent() 被调用;若其返回 true,则 onTouchEvent() 不再执行。
  • 子 View 可通过 parent.requestDisallowInterceptTouchEvent(true/false) 动态控制父容器是否拦截。

3. 滑动冲突类型与解决思路

  • 类型1:外层水平滑动(如 ViewPager),内层垂直滑动(如 ListView)
    → 根据 |Δx| > |Δy| 判断方向,决定由谁拦截。
  • 类型2:内外均为垂直滑动
    → 依赖业务逻辑(如是否滚动到顶部/底部)。
  • 类型3:嵌套上述两种情况。
  • 解决方法
    • 外部拦截法:在父容器的 onInterceptTouchEvent 中判断是否拦截。
    • 内部拦截法:子 View 主动调用 requestDisallowInterceptTouchEvent() 控制父容器行为。

4. 滑动实现方式

  • 三种滑动方式
    1. scrollTo / scrollBy:改变内容位置(非 View 本身)
    2. 属性动画(如 ObjectAnimator.ofFloat(view, "translationX", ...)
    3. 修改 LayoutParams 并调用 requestLayout()
  • 弹性滑动实现
    • 使用 Scroller + computeScroll()(推荐)
    • 使用 ValueAnimator 动态更新 scroll 值
    • 使用 Handler + postDelayed(较少用)

5. 辅助工具

  • VelocityTracker:计算滑动速度(用于 fling 判断)
  • ViewConfiguration.get(context).scaledTouchSlop:系统认定的最小滑动距离(防误触)

二、代码文件(带完整注释)

1. ViewActivity.kt

package com.shakespace.artofandroid.chapter03

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.shakespace.artofandroid.R
import com.shakespace.artofandroid.chapter03.basic.ViewCoordinateActivity
import com.shakespace.artofandroid.chapter03.conflict.ConflictActivity
import com.shakespace.firstlinecode.global.start
import kotlinx.android.synthetic.main.activity_view.*

/**
 * 主入口 Activity,用于跳转到两个子功能模块:
 * - View 坐标系演示
 * - 滑动冲突演示
 */
class ViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view)

        // 点击跳转到坐标系演示页面
        tv_coordinate.setOnClickListener {
            start(ViewCoordinateActivity::class.java)
        }

        // 点击跳转到滑动冲突演示页面
        tv_conflict.setOnClickListener {
            start(ConflictActivity::class.java)
        }
    }
}

2. basic/DemoView.kt

package com.shakespace.artofandroid.chapter03.basic

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Scroller

/**
 * 自定义 View,演示三种滑动方式及弹性滑动实现
 */
class DemoView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // Scroller 用于实现平滑滚动(弹性滑动)
    val scroller = Scroller(context)

    /**
     * 使用 Scroller 实现平滑滚动到目标位置
     * 注意:Scroller 本身不执行滚动,需配合 computeScroll()
     */
    fun smoothScrollTo(destX: Int, destY: Int) {
        val deltaX = destX - scrollX
        // 保存滚动参数(起始值、偏移量、持续时间)
        scroller.startScroll(scrollX, scrollY, deltaX, destY - scrollY, 1000)
        // 触发重绘,从而调用 computeScroll()
        invalidate()
    }

    /**
     * 关键方法:在每次绘制前被调用,用于更新滚动位置
     * 若滚动未完成,则继续 scrollTo 并请求重绘
     */
    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {
            // 更新当前滚动位置
            scrollTo(scroller.currX, scroller.currY)
            // 继续触发下一次绘制
            postInvalidate()
        }
    }

    /**
     * 方式1:使用 scrollTo/scrollBy
     * - scrollX = View 左边缘 - 内容左边缘
     * - 向右滑动 → scrollX < 0
     */

    /**
     * 方式2:使用属性动画
     */
    fun moveByAnimation() {
        // 改变 View 的实际位置(视觉位移,不影响布局)
        ObjectAnimator.ofFloat(this, "translationX", 0f, 100f)
            .setDuration(100)
            .start()

        // 使用 ValueAnimator + scrollTo 实现弹性滑动
        val startX = scrollX
        val deltaX = 100
        ValueAnimator.ofInt(0, 100).apply {
            duration = 1000
            addUpdateListener { animator ->
                val fraction = animator.animatedFraction // [0,1]
                scrollTo(startX + (deltaX * fraction).toInt(), 0)
            }
            start()
        }
    }

    /**
     * 方式3:修改 LayoutParams
     */
    fun moveByChangeLayoutParam() {
        // 直接修改宽高(仅作演示,实际中需考虑父容器约束)
        layoutParams.width += 100
        layoutParams.height += 100
        this.layoutParams = layoutParams // 触发 requestLayout()
    }
}

3. basic/EventDemoView.kt

package com.shakespace.artofandroid.chapter03.basic

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup

/**
 * 自定义 ViewGroup,用于演示事件分发机制
 * 注:此处重写了 dispatchTouchEvent 但最终仍调用 super,仅用于理解流程
 */
class EventDemoView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // 空实现,实际使用需布局子 View
    }

    /**
     * 事件分发入口
     * 正常流程:
     *   - 先调用 onInterceptTouchEvent 判断是否拦截
     *   - 若拦截,则调用自己的 onTouchEvent
     *   - 否则,将事件分发给子 View
     */
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        // 示例逻辑(注释掉,实际走默认流程)
        // var consume = false
        // if (onInterceptTouchEvent(ev)) {
        //     consume = onTouchEvent(ev)
        // } else {
        //     consume = child.dispatchTouchEvent(ev)
        // }
        // return consume

        // 实际使用默认分发机制
        return super.dispatchTouchEvent(ev)
    }

    /**
     * 是否拦截事件
     * 默认返回 false(不拦截)
     */
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }

    /**
     * 处理触摸事件
     * 若设置了 OnTouchListener,会先调用其 onTouch 方法
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return super.onTouchEvent(event)
    }

    private val touchListener = object : View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            // TODO: 实现具体逻辑
            return false
        }
    }
}

4. basic/ViewCoordinateActivity.kt

package com.shakespace.artofandroid.chapter03.basic

import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
import androidx.appcompat.app.AppCompatActivity
import com.shakespace.artofandroid.R
import com.shakespace.firstlinecode.global.TAG
import kotlinx.android.synthetic.main.activity_view_coordinate.*

/**
 * 演示 View 坐标系、touchSlop、VelocityTracker 等基础概念
 */
class ViewCoordinateActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_coordinate)

        // 显示静态坐标说明
        val memo = """
            left = getLeft()
            right = getRight()
            top = getTop()
            bottom = getBottom()
            width = right - left
            height = bottom - top
        """.trimIndent()
        tv_coordinate.text = memo

        // 显示移动后的坐标关系
        val memo2 = """
            当发生位移时(left/top/right/bottom 不变):
            x = left + translationX
            y = top + translationY
        """.trimIndent()
        tv_coordinate_move.text = memo2

        // 获取系统最小滑动距离(防误触阈值)
        val scaledTouchSlop = ViewConfiguration.get(this).scaledTouchSlop
        val other = "scaledTouchSlop = $scaledTouchSlop"
        tv_memo.text = other

        // 测试 disabled View 的触摸行为
        tv_disable.setOnTouchListener { _, _ ->
            Log.e(TAG, "onCreate: tv_disable onTouch")
            false
        }
        tv_disable.setOnClickListener {
            Log.e(TAG, "onCreate: tv_disable onClick")
        }
    }

    /**
     * 演示 VelocityTracker 的基本用法
     * 注意:此处每次 MOVE 都新建 tracker,实际应复用
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val velocityTracker = VelocityTracker.obtain()
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> {
                velocityTracker.addMovement(event)
                velocityTracker.computeCurrentVelocity(100) // 100ms 单位
                Log.e(TAG, "onTouchEvent: ${velocityTracker.xVelocity} -- ${velocityTracker.yVelocity}")
            }
        }
        velocityTracker.clear()
        velocityTracker.recycle()
        return super.onTouchEvent(event)
    }
}

5. conflict/ConflictActivity.kt

package com.shakespace.artofandroid.chapter03.conflict

import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.shakespace.artofandroid.R
import com.shakespace.firstlinecode.global.screenWidth
import kotlinx.android.synthetic.main.activity_confilct.*

/**
 * 滑动冲突演示主界面
 * 创建 3 个页面,每个页面包含一个 ListView
 */
class ConflictActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_confilct)

        // 添加 3 个页面到水平滑动容器
        for (i in 0..2) {
            val view = layoutInflater.inflate(R.layout.confilct_test_layout, horizontal_view, false)
            view.layoutParams.width = screenWidth

            val tvTitle: TextView = view.findViewById(R.id.title)
            tvTitle.text = "Page $i"

            // 设置背景色(避免全白)
            view.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i * i + 1), 255 / (i * i * i * i + 1)))

            createList(view)
            horizontal_view.addView(view)
        }
    }

    /**
     * 为每个页面创建 ListView 数据
     */
    private fun createList(view: View) {
        val listView = view.findViewById<ListView>(R.id.list_view)
        val dataList = ArrayList<String>().apply {
            for (i in 0..50) {
                add("item $i")
            }
        }
        val adapter = ArrayAdapter<String>(
            this,
            android.R.layout.simple_list_item_1,
            android.R.id.text1,
            dataList
        )
        listView.adapter = adapter
    }
}

6. conflict/ConflictDemoView.kt

package com.shakespace.artofandroid.chapter03.conflict

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup

/**
 * 演示两种滑动冲突处理方法:
 * - 方法1:外部拦截(重写 onInterceptTouchEvent)
 * - 方法2:内部拦截(子 View 调用 requestDisallowInterceptTouchEvent)
 */
class ConflictDemoView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        // 空实现
    }

    // ==================== 方法2:内部拦截法 ====================
    /**
     * 在子 View 的 dispatchTouchEvent 中控制父容器是否拦截
     * 注意:此方法需配合父容器的 onInterceptTouchEvent 使用
     */
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 允许自己先处理
                requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                // 根据业务逻辑决定是否允许父容器拦截
                // 此处简化为始终允许(实际应判断滑动方向)
                requestDisallowInterceptTouchEvent(false)
            }
            MotionEvent.ACTION_UP -> {
                // 无特殊处理
            }
        }
        return super.dispatchTouchEvent(ev)
    }

    // ==================== 方法1:外部拦截法 ====================
    var flag = true // 控制是否拦截(演示用)
    var lastX: Float = 0f
    var lastY: Float = 0f

    /**
     * 父容器判断是否拦截事件
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        var intercepted = false
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = false // 必须放行 DOWN,否则子 View 收不到
            }
            MotionEvent.ACTION_MOVE -> {
                intercepted = flag // 根据业务逻辑决定
            }
            MotionEvent.ACTION_UP -> {
                intercepted = false // 通常不拦截 UP
            }
        }
        lastX = ev.x
        lastY = ev.y
        return intercepted
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return super.onTouchEvent(event)
    }

    private val touchListener = object : View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            return false
        }
    }
}

7. conflict/HorizontalScrollViewEx.kt

package com.shakespace.artofandroid.chapter03.conflict

import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewGroup
import android.widget.Scroller
import com.shakespace.firstlinecode.global.TAG

/**
 * 自定义水平滑动容器,使用【外部拦截法】处理滑动冲突
 * 适用于 ViewPager + ListView 场景
 */
class HorizontalScrollViewEx @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    private var childrenSize = 0
    private var childWidth = 0
    private var childIndex = 0
    private var lastX = 0
    private var lastY = 0
    private var lastXInIntercept = 0
    private var lastYInIntercept = 0
    private val scroller = Scroller(context)
    private val velocityTracker = VelocityTracker.obtain()

    /**
     * 判断是否拦截事件(外部拦截法核心)
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        var intercepted = false
        val eventX = ev.x.toInt()
        val eventY = ev.y.toInt()

        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 如果正在滚动中,中断并接管事件
                if (!scroller.isFinished) {
                    scroller.abortAnimation()
                    intercepted = true
                } else {
                    intercepted = false
                }
                lastXInIntercept = eventX
                lastYInIntercept = eventY
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = eventX - lastXInIntercept
                val deltaY = eventY - lastYInIntercept
                // 水平滑动为主 → 父容器拦截
                intercepted = Math.abs(deltaX) > Math.abs(deltaY)
            }
            MotionEvent.ACTION_UP -> {
                intercepted = false
            }
        }

        Log.e(TAG, "onInterceptTouchEvent: intercepted=$intercepted")
        lastX = eventX
        lastY = eventY
        return intercepted
    }

    /**
     * 处理触摸事件,实现水平滑动和自动吸附
     */
    override fun onTouchEvent(ev: MotionEvent): Boolean {
        velocityTracker.addMovement(ev)
        val eventX = ev.x.toInt()
        val eventY = ev.y.toInt()

        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                if (!scroller.isFinished) {
                    scroller.abortAnimation()
                }
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = eventX - lastX
                scrollBy(-deltaX, 0) // 水平滚动
            }
            MotionEvent.ACTION_UP -> {
                velocityTracker.computeCurrentVelocity(1000)
                val xVelocity = velocityTracker.xVelocity

                // 根据速度或位置自动吸附到最近页面
                if (Math.abs(xVelocity) >= 50) {
                    childIndex += if (xVelocity < 0) 1 else -1
                } else {
                    childIndex = (scrollX + childWidth / 2) / childWidth
                }

                childIndex = Math.max(0, Math.min(childIndex, childrenSize - 1))
                val dx = childIndex * childWidth - scrollX
                smoothScrollBy(dx, 0)

                velocityTracker.clear()
            }
        }

        lastX = eventX
        lastY = eventY
        return true
    }

    // --- 测量与布局 ---
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        measureChildren(widthMeasureSpec, heightMeasureSpec)

        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        if (childCount == 0) {
            setMeasuredDimension(0, 0)
            return
        }

        val firstChild = getChildAt(0)
        val childWidth = firstChild.measuredWidth
        val childHeight = firstChild.measuredHeight

        when {
            widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST ->
                setMeasuredDimension(childWidth * childCount, childHeight)
            heightMode == MeasureSpec.AT_MOST ->
                setMeasuredDimension(widthSize, childHeight)
            widthMode == MeasureSpec.AT_MOST ->
                setMeasuredDimension(childWidth * childCount, heightSize)
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var childLeft = 0
        childrenSize = childCount
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {
                childWidth = child.measuredWidth
                child.layout(childLeft, 0, childLeft + childWidth, child.measuredHeight)
                childLeft += childWidth
            }
        }
    }

    private fun smoothScrollBy(dx: Int, dy: Int) {
        scroller.startScroll(scrollX, scrollY, dx, dy, 500)
        invalidate()
    }

    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.currX, scroller.currY)
            postInvalidate()
        }
    }

    override fun onDetachedFromWindow() {
        velocityTracker.recycle()
        super.onDetachedFromWindow()
    }
}

8. conflict/ListViewEx.kt

package com.shakespace.artofandroid.chapter03.conflict

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ListView

/**
 * 自定义 ListView,使用【内部拦截法】处理滑动冲突
 * 在 dispatchTouchEvent 中主动控制父容器是否拦截
 */
class ListViewEx @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ListView(context, attrs, defStyleAttr) {

    private var lastX = 0
    private var lastY = 0

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val eventX = ev.x.toInt()
        val eventY = ev.y.toInt()

        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 先允许自己处理事件
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = eventX - lastX
                val deltaY = eventY - lastY
                // 如果是垂直滑动,禁止父容器拦截;否则允许
                val disallowIntercept = Math.abs(deltaX) <= Math.abs(deltaY)
                parent.requestDisallowInterceptTouchEvent(disallowIntercept)
            }
            MotionEvent.ACTION_UP -> {
                // 无特殊处理
            }
        }

        lastX = eventX
        lastY = eventY
        return super.dispatchTouchEvent(ev)
    }
}

注:HorizontalScrollViewEx2.ktHorizontalScrollViewEx.kt 逻辑高度重复,且存在逻辑缺陷(onInterceptTouchEvent 在非 DOWN 事件直接返回 true),故未单独列出。建议以 HorizontalScrollViewEx.kt 为准。

三、文档笔记

z_memo01.txt

1. Event Flow
   Activity --> Window --> View
   if all views not handle the event, will back to Activity

   /**
    * Called when a touch screen event was not handled by any of the views
    * under it.  This is most useful to process touch events that happen
    * outside of your window bounds, where there is no view to receive it.
    *
    * @param event The touch screen event being processed.
    *
    * @return Return true if you have consumed the event, false if you haven't.
    * The default implementation always returns false.
    */
   public boolean onTouchEvent(MotionEvent event) {
       if (mWindow.shouldCloseOnTouch(this, event)) {
           finish();
           return true;
       }
       return false;
   }

   @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
   public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
       final boolean isOutside =
               event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
               || event.getAction() == MotionEvent.ACTION_OUTSIDE;
       if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
           return true;
       }
       return false;
   }

2. An Event Sequence = a Down + many Move + a Up  
   (事件序列,由一个按下,多个移动,一个抬起组成)

3. Normally, an event should be consumed only by one View  
   (通常一个序列应该由一个 view 处理)

4. In an event sequence, if a view calls onInterceptTouchEvent, then the rest of the event sequence will all be handled by this view and onInterceptTouchEvent will not be called again.

5. If a view returns false in Down Event, then the rest events will not be handled by this view  
   (如果一个 view 没有处理 DOWN,那么也不会处理后续)

6. ViewGroup will not intercept any event by default → onInterceptTouchEvent returns false  
   (ViewGroup 默认不会拦截)

7. If a view clickable = false and longClickable = false, then onTouchEvent will return false.

8. Can call requestDisallowInterceptTouchEvent in child view.

z_memo02 source code.txt

1. Activity
   public boolean dispatchTouchEvent(MotionEvent ev) {
       if (ev.getAction() == MotionEvent.ACTION_DOWN) {
           onUserInteraction(); // empty method
       }
       if (getWindow().superDispatchTouchEvent(ev)) {
           return true;
       }
       return onTouchEvent(ev);
   }

2. PhoneWindow
   @Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
       return mDecor.superDispatchTouchEvent(event);
   }
   // This is the top-level view of the window, containing the window decor.
   private DecorView mDecor;

3. com.android.internal.policy.DecorView  // hide
   /** @hide */
   public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
       private static final String TAG = "DecorView";
       ....
   }

4. ViewGroup: dispatchTouchEvent
   final boolean intercepted;
   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;
       }
   }

   - mFirstTouchTarget: if there is a view that has handled the previous event, mFirstTouchTarget would not be null.
   - disallowIntercept: set by requestDisallowInterceptTouchEvent.
     (Except for DOWN event, ViewGroup will call onInterceptTouchEvent in each DOWN event because it resets FLAG_DISALLOW_INTERCEPT.)

   // 请求父类不要拦截:如果 disallowIntercept is true, parent does not intercept → intercepted = false
   // Note: onInterceptTouchEvent may not be called in an entire event sequence.

5. If not intercepted:
   for (int i = childrenCount - 1; i >= 0; i--) {
       final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
       final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

       if (childWithAccessibilityFocus != null) {
           if (childWithAccessibilityFocus != child) {
               continue;
           }
           childWithAccessibilityFocus = null;
           i = childrenCount - 1;
       }

       if (!child.canReceivePointerEvents()
               || !isTransformedTouchPointInView(x, y, child, null)) {
           ev.setTargetAccessibilityFocus(false);
           continue;
       }

       newTouchTarget = getTouchTarget(child);
       if (newTouchTarget != null) {
           newTouchTarget.pointerIdBits |= idBitsToAssign;
           break;
       }

       resetCancelNextUpFlag(child);
       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
           mLastTouchDownTime = ev.getDownTime();
           if (preorderedList != null) {
               for (int j = 0; j < childrenCount; j++) {
                   if (children[childIndex] == mChildren[j]) {
                       mLastTouchDownIndex = j;
                       break;
                   }
               }
           } else {
               mLastTouchDownIndex = childIndex;
           }
           mLastTouchDownX = ev.getX();
           mLastTouchDownY = ev.getY();
           newTouchTarget = addTouchTarget(child, idBitsToAssign);  // set touchTarget
           alreadyDispatchedToNewTouchTarget = true;
           break;
       }
       ev.setTargetAccessibilityFocus(false);
   }

   // 可见性 和 动画,结合点击点是否在区域内
   // 当 view 可见时,返回 true;当 view 不可见,若设置了动画,也返回 true(因为动画可能使其隐藏??)
   protected boolean canReceivePointerEvents() {
       return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
   }

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                View child, int desiredPointerIdBits) {
       final boolean handled;
       final int oldAction = event.getAction();
       if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
           event.setAction(MotionEvent.ACTION_CANCEL);
           if (child == null) {
               handled = super.dispatchTouchEvent(event); // → View.dispatchTouchEvent(ev)
           } else {
               handled = child.dispatchTouchEvent(event);
           }
           event.setAction(oldAction);
           return handled;
       }
   }

6. In View
   if (onFilterTouchEventForSecurity(event)) {
       if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
           result = true;
       }
       ListenerInfo li = mListenerInfo;
       if (li != null && li.mOnTouchListener != null
               && (mViewFlags & ENABLED_MASK) == ENABLED
               && li.mOnTouchListener.onTouch(this, event)) {
           result = true;
       }
       if (!result && onTouchEvent(event)) {
           result = true;
       }
   }

   - Will call mOnTouchListener.onTouch first.
   - onTouchEvent(ev)

   final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
           || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
           || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

   if ((viewFlags & ENABLED_MASK) == DISABLED) {
       if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
           setPressed(false);
       }
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
       // A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
       return clickable;
   }

   if (mTouchDelegate != null) {
       if (mTouchDelegate.onTouchEvent(event)) {
           return true;
       }
   }

   // 可点击 / 长按 / 响应鼠标(或触控笔)就可以消费点击事件
   if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
       ....
   }
   // will return true.

   So: if view is disabled, onTouchListener won't work, but onTouchEvent still gets called.
   Returns true if clickable / longClickable / contextClickable / or (viewFlags & TOOLTIP) == TOOLTIP.

7. In addition:
   public void setOnClickListener(@Nullable OnClickListener l) {
       if (!isClickable()) {
           setClickable(true);
       }
       getListenerInfo().mOnClickListener = l;
   }

   public void setOnLongClickListener(@Nullable OnLongClickListener l) {
       if (!isLongClickable()) {
           setLongClickable(true);
       }
       getListenerInfo().mOnLongClickListener = l;
   }

z_memo03 scroll conflict.txt

处理滑动冲突:

Case 1. Outer horizontal, Inner vertical  
        (e.g., ViewPager + Fragment with ListView)
   - When MOVE is horizontal → OUTER should intercept the event.
   - When MOVE is vertical → INNER should intercept the event.
   - Check using |Δx| > |Δy|.

Case 2. Outer vertical, Inner vertical  
        Depends on business logic (e.g., whether inner is at top/bottom).

Case 3. Nested Case 1 and Case 2.

z_memo04滑动和弹性滑动.txt

// TODO 补充滑动和弹性滑动

1. 滑动的三种方式:
   - scrollTo / scrollBy
   - 属性动画(ObjectAnimator / ValueAnimator)
   - 修改 LayoutParams 并调用 requestLayout()

2. 弹性滑动的三种方式:
   - Scroller + computeScroll()
   - ValueAnimator + update listener
   - Handler + postDelayed(不推荐)
posted @ 2026-01-20 23:00  y丶innocence  阅读(0)  评论(0)    收藏  举报