HarmonyOS 鸿蒙Next中View的事件体系和工作流程

HarmonyOS 鸿蒙Next中View的事件体系和工作流程

View

1.View的事件体系

1.1 View基础知识

1.1.1 什么是View:View是所有控件的基类

ViewGroup也继承了View,即一组View,每个View又可以是一个ViewGroup

1.1.2 View的位置参数

x,y,translationX,translationY:

x,y是view左上角的坐标:x=left+translationX,y=top+translationY top,left是原生的左上角位置,不会改变,变的是x,y,translationX,translationY

1.1.3 MotionEvent和TouchSlop

1.MotionEvent:ACTION_DOWN,ACTION_MOVE,ACTION_UP

getX/getY:获得当前View的左上角的x,y坐标

getRowX/getRowY:获得手机屏幕左上角的x,y坐标

2.TouchSlop:可被识别为滑动的最小距离

这是一个常量,和设备有关,用ViewConfiguration.get(getContext()).getScaledTouchSlop()

1.1.4 VelocityTracker,GestureDetector,Scoller

1.VelocityTracker:追踪滑动速度(滑动过程中可能触发多次微小滑动事件)
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var velocityTracker: VelocityTracker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding=ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        velocityTracker=VelocityTracker.obtain()
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        velocityTracker.addMovement(event)
        velocityTracker.computeCurrentVelocity(1000)//设置时间间隔,根据这个间隔内划过的像素数计算速度
        val xVelocity=velocityTracker.xVelocity//与父布局坐标轴正方向一致,则速度为正
        val yVelocity=velocityTracker.yVelocity
        Toast.makeText(this,"水平速度是 ${xVelocity} , 竖直速度是 ${yVelocity}",Toast.LENGTH_SHORT).show()
        return super.onTouchEvent(event)
    }
    override fun onDestroy() {
        super.onDestroy()
        // 释放VelocityTracker资源
        velocityTracker.clear()
        velocityTracker.recycle()
    }
}
2.GestureDetector

1.实现GestureDetector.OnGestureListener接口:还要重写onTouchEvent

必须重写接口的所有方法:onDown,onShowPress, onSingleTapUp(单击),onScroll(拖动),onLongPress(长按),onFling(快速滑动)

2.使用GestureDetector.SimpleOnGestureListener()

只需要重写需要的方法

mGestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        Toast.makeText(this@MainActivity, "你按下了", Toast.LENGTH_SHORT).show()
        return true//true表示继续接收后续事件,比如滚动,滑动等
    }

    override fun onSingleTapUp(e: MotionEvent): Boolean {
        Toast.makeText(this@MainActivity, "你松开了", Toast.LENGTH_SHORT).show()
        return true
    }
})

 override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.let {
            // 将事件传递给GestureDetector处理
            if (mGestureDetector.onTouchEvent(it)) {
                return true
            }
        }
        return super.onTouchEvent(event)
    }

3.实现GestureDetector.OnDoubleTapListener接口:onDoubleTap(双击),onSingleTapConfirmed(严格的单击,后面不能跟另外的单击),onDoubleTapEvent(双击,双击过程中的任何MotionEvent都会触发这个回调)

1.2 View的滑动

1.2.1 scrollTo/scrollBy
private var scrollX = 0
private var scrollY = 0

  mGestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
            override fun onDown(e: MotionEvent): Boolean {
                return true
            }

            override fun onScroll(
                e1: MotionEvent?,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                // 使用 scrollBy 实现跟随手指滚动
                binding.root.scrollBy(distanceX.toInt(), distanceY.toInt())
                return true
            }

            override fun onFling(
                e1: MotionEvent?,
                e2: MotionEvent,
                velocityX: Float,
                velocityY: Float
            ): Boolean {
                // 快速滑动动画 - 方向应与手势一致
                // 使用 Scroller 实现更平滑的动画效果
                // 这里简化处理
                scrollX -= (velocityX / 50).toInt()   // 调整系数控制滑动距离
                scrollY -= (velocityY / 50).toInt()
                binding.root.scrollTo(scrollX, scrollY)
                return true
            }
        })
1.2.2 使用动画
 binding.btn.setOnClickListener {
        moveViewWithPropertyAnimator(binding.btn)
    }
}

private fun moveViewWithPropertyAnimator(view: View) {
    view.animate()//获取 View 的 ViewPropertyAnimator 实例
        .translationX(300f)    // 移动到 X=300 位置
        .translationY(200f)    // 移动到 Y=200 位置
        .setDuration(1000)     // 动画持续时间
        .setInterpolator(AccelerateDecelerateInterpolator()) // 插值器,设置动画的变化速率
        .start()
}
1.2.3 修改布局参数:

如果想让一个button向右平移100px,那我可以在button放一个空的view,只需要设置空的view宽度为100px,button自动会被挤到右边

1.3 View的事件分发机制

MotionEvent事件的分发过程:当MotionEvent产生后,系统要把这个事件传递给一个具体的View,这个传递的过程就是分发过程。

分发过程需要三个重要方法共同参与:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent

只要事件可以传递给当前View,那么dispatchTouchEvent一定会被调用

fun dispatchTouchEvent(ev:MotionEvent):Boolean{
   val consume:Boolean=false
    if(onInterceptTouchEvent(ev)){
        consume=onTouchEvent(ev)
    }else{
        consume=child.dispatchTouchEvent(ev)
    }
}

onTouchListener>onTouchEvent>onClickListener

同一个事件序列:从down到up的整个过程,中间包含了数量不定的move

点击事件的传递顺序:Activity->Windows->View

1.如果View拦截了某个事件,那么在同一个事件序列当中,onIntercepTouchEvent将不会被再次调用

2.如果在onTouchEvent中,View不消耗当前点击事件,那么在同一个事件序列中,当前view将无法再次收到事件

3.如果一个View拦截了点击事件,但是onTouchEvent返回了false(没有消耗ACTION_DOWN事件),那么就会调用父容器的onTouchEvent,如果还是返回false,那么就一层层往上传递,最后传递给Activity处理

4.一般来说,一个事件序列只能被一个View被处理,除非这个View在onTouchEvent中强行把自己的事件传递给其他View

5.如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前的View可以持续收到后续的事件,最终消失的点击事件会传递给Activity处理 一个完整的触摸事件序列:ACTION_DOWN → ACTION_MOVE* → ACTION_UP/ACTION_CANCEL 一旦某个View的onTouchEvent返回true消费了ACTION_DOWN,系统会记住这个View后续的事件会直接传递给这个View,不会重新走分发流程

View 不消耗 ACTION_DOWN 以外的其他事件的情况:

  1. ACTION_DOWN:

    • View.onTouchEvent() 返回 true (消费了)
    • 系统记住这个View作为事件处理者
  2. ACTION_MOVE:

    • 直接传递给该View
    • View.onTouchEvent() 返回 false (不消费)
    • 事件不会传递给父View的onTouchEvent()
    • 但View仍会继续收到后续事件
  3. 最终:

    • 未处理的事件传递给Activity

6.ViewGroup默认不会拦截任何事件,即onInterceptTouchEvent默认返回false

7.View没有onInterceptTouchEvent,因为View是ui层级的最底层,没有子View需要保护,只要有事件传递给他,onTouchEvent就会被调用

8.View的onTouchEvent默认会消耗事件,除非是不可点击的(clickable和longClickable都为false)

9.事件传递是由外向内的,父元素分发给子View,使用requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素事件的分发过程,但是除了ACTION_DOWN外

2.View的工作原理

2.1 ViewRoot和DecorView

ViewRoot的实现类是ViewRootImpl类,他是连接WindowManager和DecorView的纽带

当Acitivity对象被创建完毕后,会把DecorView添加到Window中,同时会创建ViewRootImpl对象,并且把ViewRootImpl和DecorView建立联系

1.流程从ViewRoot的performTraversals开始,依次调用:performMeasure,performLayout和performDraw。

这三个方法分别完成了顶级View的measure,layout,draw的流程

2.performMeasure- >measure->onMeasure 在onMeasure中又会对所有的子元素完成measure流程,传递下去就完成了对整个View树的遍历

3.performLayout和performDraw也类似,不过performDraw->draw->dispatchDraw ,本质上没有区别

只有draw方法完成后,View的内容才能显示在屏幕上,

4.DecorView是顶级的View,本质就是一个FrameLayout,内部包含一个竖直的LinearLayout(标题栏+内容栏)

2.2 MeasureSpec

MeasureSpec 是 Android 中用于封装 View 测量规格的工具类,包含测量模式和尺寸大小两部分信息,用来在测量过程中指导 View 如何确定自己的尺寸。

MeasureSpec为了避免过多的对象内存分配以及方便操作,将SpecMode(高2位)和SpecSize(低30位)打包成一个int值(32位),SpecMode和SpecSize也是int

SpecMode分类

LayoutParams 是 Android 中用于描述 View 在父容器中布局参数的类,它定义了 View 的尺寸、位置以及其他布局相关属性。每个 View 都必须有一个与之关联的 LayoutParams。

对于普通的View:根据父容器施加的规则将当前View的LayoutParams转换成对应的MeasureSpec,然后根据measureSpec测量出View的宽/高(测量)(也就是说,View的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams共同决定的,MeasureSpec一旦确定,onMeasure就可以确定View的测量宽/高)

对于DecorView:根据他的LayoutParams的规则:LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 固定大小

3.View的工作流程

View的三大绘制流程:测量流程(measure),布局流程(layout),绘制流程(draw)

measure:确定View的测量宽/高 layout:确定View的最终宽/高和四个顶点的位置 draw:负责把View绘制在屏幕上

3.1 measure

3.1.1 View的measure

measure- >onMeasure->getDefaultSize-> 返回measureSpec的specSize,specSize就是View测量后的大小

3.1.2 ViewGroup的measure

没有重写onMeasure,而是提供了measureChild,取出子元素的LayoutParams创建子元素的MeasureSpec,再把MeasureSpec直接传给View的measure去测量

measure过程和Activity的生命周期不是同步的

3.2 layout

layout->确定四个顶点,再调用onLayout->onLayout遍历所有子元素,再调用子元素中的layout

一般来说,测量宽/高和最终宽/高都是相等的

3.3 draw

1.绘制背景background.draw(canvas)

2.绘制自己(onDraw)

3.绘制子元素(dispatchDraw)//绘制过程的传递是通过dispatchDraw实现,

diapatchDraw-> 遍历所有子元素,调用子元素的draw方法

4.绘制装饰(onDrawScollBars)


更多关于HarmonyOS 鸿蒙Next中View的事件体系和工作流程的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

HarmonyOS Next中View的事件体系基于事件分发机制,通过事件源、事件对象和事件监听器协同工作。事件从Window开始,依次经过根View、父View到目标View的传递流程,支持事件拦截和处理。工作流程包括事件采集、分发、消费和回调,最终由系统统一管理事件生命周期。

更多关于HarmonyOS 鸿蒙Next中View的事件体系和工作流程的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,View的事件体系和工作流程是其UI框架的核心,与Android有相似之处但也有其独特的设计。以下是对您所提内容的专业解读:

1. View的事件体系

1.1 事件分发机制

HarmonyOS Next的事件分发同样遵循从外到内的传递逻辑,核心方法包括:

  • dispatchTouchEvent:负责事件分发。
  • onInterceptTouchEvent(仅ViewGroup):决定是否拦截事件。
  • onTouchEvent:处理事件。

事件传递顺序为:AbilityWindowView。与Android类似,ViewGroup默认不拦截事件,而View没有拦截方法。

1.2 触摸事件处理

  • MotionEvent:包含ACTION_DOWNACTION_MOVEACTION_UP等事件类型,通过getX/getY获取相对坐标。
  • VelocityTracker:用于计算滑动速度,需在事件结束后释放资源。
  • GestureDetector:支持手势识别(如单击、长按、滑动),可通过SimpleOnGestureListener简化实现。

1.3 View的滑动

  • scrollTo/scrollBy:通过改变View的滚动位置实现滑动,但注意滚动的是内容而非View本身。
  • 属性动画:使用animate()方法实现平滑移动,支持插值器控制动画效果。
  • 布局参数:通过修改LayoutConfig(类似Android的LayoutParams)调整View位置。

2. View的工作原理

2.1 测量与布局

  • MeasureSpec:封装测量模式和尺寸,由父容器和View自身的布局参数共同决定。模式包括:
    • EXACTLY:精确尺寸。
    • AT_MOST:最大尺寸。
    • UNSPECIFIED:无限制。
  • 测量流程:从根View开始递归调用measure()onMeasure(),确定View的测量宽高。
  • 布局流程:通过layout()onLayout()确定View的最终位置和大小。

2.2 绘制流程

绘制顺序遵循:

  1. 绘制背景。
  2. 绘制自身内容(onDraw)。
  3. 绘制子View(dispatchDraw)。
  4. 绘制装饰(如滚动条)。

3. 与Android的差异点

  • 组件命名:HarmonyOS Next中使用Component作为基类,而非View,但事件体系类似。
  • 布局参数:使用LayoutConfig替代LayoutParams
  • API调用:部分方法名和参数可能不同,需参考HarmonyOS官方文档。

总结

HarmonyOS Next的事件分发和工作流程继承了经典的UI框架设计,开发者可借助Android经验快速上手。重点在于理解事件传递的递归性质、测量布局的协作机制以及绘制流程的层次顺序。在实际开发中,合理使用手势识别和动画API能提升交互体验。

回到顶部