Android开发主要是在智能手机上,而智能手机基本都是触屏手机,用户的交互绝大部分都是通过触碰屏幕完成的,所以Android的View体系中touch事件的传递处理机制就是重中之重。列表跟着手指移动方向滑动、按钮响应点击,这些都是因为touch事件正确传递给了这些View,它们才能做出响应。
我们知道所有界面都是基于View和ViewGroup的,结构上是一层一层组织的:
而且整个View tree的顶层就是ViewGroup,所以touch事件传递的重点就在于ViewGroup分发到子View的过程,事件流动的方向。
以下源码分析基于API26源码
下面就通过分析,替touch事件回答"是什么"、“从哪来”、"到哪去"这三大哲学问题。
在framewor中,代表触碰屏幕的动作在被抽象成了MotionEvent类。使用手机时手机在屏幕上滑动,系统会把手指的动作映射成多个MotionEvent分发下去。
除了操作屏幕,操作手机还可以通过按键,不管是实体键还是更常见的虚拟按键。这类事件是由KeyEvent表示的。而MotionEvent和KeyEvent都是InputEvent的子类。这很容易理解,从操作系统的角度来讲,屏幕和按键都是输入设备。
手指操作屏幕可以分成多种形式,包括落下、抬起、拖动,快速滑动等等。所以每个MotionEvent都有action表示代表的是什么操作。可以通过getAction()获取。每个MotionEvent中还包括了x和y值,表示这个事件对应的位置,分别可以通过getX()和getY()获取。
最常用的事件类型有:
比如最常见的查看一个列表的时候,手指在屏幕上滑动拖动列表滑动的操作,正常情况下,系统会先发出一个ACTION_DOWN事件,然后是一系列的ACTION_MOVE事件,然后以一个ACTION_UP事件结尾。
事件自然是系统分发出来的,在事件传递到Activity的时候,把此时的栈打印出来,就可以看到从Framework层面来说事件分发的起点是哪里:
at TouchEventActivity.dispatchTouchEvent(TouchEventActivity.java:48)at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2486)at android.view.View.dispatchPointerEvent(View.java:8868)at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4777)at android.view.Process(ViewRootImpl.java:4609)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)at android.view.DeliverToNext(ViewRootImpl.java:4138)at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4104)at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4241)at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4112)at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4298)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)at android.view.DeliverToNext(ViewRootImpl.java:4138)at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4104)at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4112)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6593)at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6567)at android.queueInputEvent(ViewRootImpl.java:6520)at android.view.InputEvent(ViewRootImpl.java:6773)at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)at android.os.MessageQueue.nativePollOnce(Native Method)at android.(MessageQueue.java:148)at android.os.Looper.loop(Looper.java:151)at android.app.ActivityThread.main(ActivityThread.java:5898)at flect.Method.invoke(Native Method)at flect.Method.invoke(Method.java:372)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1019)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:814)
在ActivityThread主线程的loop里循环里,走到了InputEventReceiver,再走到ViewRootImpl,再走到DecorView,再走到Activity的dispatchTouchEvent,然后就是继续分发给这个Activity上的各个View。
从主线程的loop走到InputEventReceiver#dispatchInputEvent在SDK里是看不到调用的,是从native调用过来的,dispatchInputEvent方法上的注释说明了这点。
// Called from native code.
private void dispatchInputEvent(int seq, InputEvent event) {mSeqMap.SequenceNumber(), seq);onInputEvent(event);
}
至于native具体是怎么获取屏幕上的事件再传过来的,因为Android基于Linux,更底层的输入驱动和机制类似linux,可以理解成每个输入设备都有一个对应的文件符挂载在系统上以供读取,硬件不断往文件写入,系统不断读取文件。更具体的作为Android应用开发者也不是很了解,就不具体讨论了。可以通过android的adb工具大致看下输入设备的情况。连上手机,输入adb shell getevent -l,滑动下屏幕,可以看到一些输出,每个手机不一样,下面是我的魅族手机的输出:
~ » adb shell getevent -l
add device 1: /dev/input/event7name: "sm8150-meizu-snd-card USBC Jack"
add device 2: /dev/input/event6name: "main_touch"
add device 3: /dev/input/event1name: "qti-haptics"
add device 4: /dev/input/event0name: "qpnp_pon"
add device 5: /dev/input/event2name: "uinput-goodix"
add device 6: /dev/input/event3name: "ndt"
add device 7: /dev/input/event5name: "gpio-keys"
add device 8: /dev/input/event4name: "qbt1000_key_input"
/dev/input/event6: EV_KEY BTN_TOUCH DOWN
/dev/input/event6: EV_ABS ABS_MT_TRACKING_ID 0000a31f
/dev/input/event6: EV_ABS ABS_MT_POSITION_X 000001c1
/dev/input/event6: EV_ABS ABS_MT_POSITION_Y 00000664
/dev/input/event6: EV_ABS ABS_MT_TOUCH_MAJOR 0000002f
/dev/input/event6: EV_ABS ABS_MT_PRESSURE 0000002f
大致可以猜出来,main_touch设备就是我们的手机屏幕。
事件产生了,开始传递到当前Activity了,下面是我们更关心的步骤了:在当前展示的view hierarchy上,touch事件是怎么传递的,怎么让列表动起来,让按钮响应点击事件的。
整个touch事件传递的过程采用的是类似设计模式中责任链模式(Chain of Responsibility Pattern)的设计。
举一个责任链模式的例子:假设员工请假的时候,大于3天需要直属leader审批,大于7天需要部门经理审批,大于15天需要CEO审批。假如有一个员工提交了一个请假16天的工单:
程序员:世界那么大,我要请假16天去看看
Leader:16天有点久,让经理处理
经理:16天有点久,让老板处理
CEO:准了
这就是具体的责任链模式。
touch事件的传递过程类似上面的过程,不同的是传递的方向是会折返的。
先说结论:ViewTree是层级组织的,上一层ViewGroup在收到touch事件的时候,会尝试传递给自己的child,并且child会告诉parent自己有没有消耗掉事件;如果它的child也是ViewGroup,又会重复这个操作;所以touch事件首先会从ViewTree最顶部传递到ViewTree的某个叶子节点的View,如果这个View没有消耗掉事件,处理权会从叶子节点传递回根节点。完整轨迹类似一个U形状。这样能保证所有节点都有机会处理touch事件。
传递过程中之所以需要通过返回值向parent回报事件有没有被消耗,是因为默认当一组touch事件确定传递个某个view之后,这次touch事件序列中后续所有事件都会传递给它而不是别的view。所以需要当消耗了事件后需要回报给parent,这样parent才能记住后续传递事件的目标
再举一个类似的例子:
老板:结合外部市场,我觉得今年我们应该加上这么一个功能。我是老板(onInterceptTouchEvent)这个事情就给小A你来跟进吧 (dispatchTouchEvent)
经理A:最近没空啊(onInterceptTouchEvent)。小C啊,有个功能你来做下(dispatchTouchEvent)
程序员C:我擦这个不想做啊(onInterceptTouchEvent),小D啊,有个功能你来做下(dispatchTouchEvent)
实习生D:学长,这个我不会做呀(onTouchEvent)
程序员C:经理,这个我不会做呀(onTouchEvent)
经理A:擦,那只能我自己做了(onTouchEvent)。老板,搞定了。
事件传递到的每一个节点,都会尝试把事件给自己的子节点处理,子节点没处理的才会尝试自己处理。
下面的一个简单布局中,所有View都没有处理touch,所以touch经历了最长的传递过程。通过在关键节点打的log,可以看到当点击最里面的CView的时候事件传递的具体情况。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=""android:orientation="vertical" android:layout_width="match_parent"android:gravity="center"android:layout_height="match_parent"android:background="#ffffffff"><commondemo.view.LogFrameViewandroid:id="@+id/a"android:layout_width="300dp"android:layout_height="300dp"android:background="#ffff0000"><commondemo.view.LogFrameViewandroid:id="@+id/b"android:layout_width="200dp"android:layout_height="200dp"android:layout_gravity="center"android:background="#ff00ff00"><commondemo.view.LogViwandroid:id="@+id/c"android:layout_width="100dp"android:layout_height="100dp"android:layout_gravity="center"android:background="#ff0000ff"/></commondemo.view.LogFrameView></commondemo.view.LogFrameView>
</LinearLayout>
activity: dispatchTouchEvent: ACTION_DOWN begin
AView: dispatchTouchEvent: ACTION_DOWN begin
AView: onInterceptTouchEvent: ACTION_DOWN begin
AView: onInterceptTouchEvent: ACTION_DOWN return false
BView: dispatchTouchEvent: ACTION_DOWN begin
BView: onInterceptTouchEvent: ACTION_DOWN begin
BView: onInterceptTouchEvent: ACTION_DOWN return false
CView: dispatchTouchEvent: ACTION_DOWN begin
CView: onTouchEvent: ACTION_DOWN begin
CView: onTouchEvent: ACTION_DOWN return false
CView: dispatchTouchEvent: ACTION_DOWN return false
BView: onTouchEvent: ACTION_DOWN begin
BView: onTouchEvent: ACTION_DOWN return false
BView: dispatchTouchEvent: ACTION_DOWN return false
AView: onTouchEvent: ACTION_DOWN begin
AView: onTouchEvent: ACTION_DOWN return false
AView: dispatchTouchEvent: ACTION_DOWN return false
activity: onTouchEvent: ACTION_DOWN begin
activity: onTouchEvent: ACTION_DOWN return false
activity: dispatchTouchEvent: ACTION_DOWN return false
activity: dispatchTouchEvent: ACTION_UP begin
activity: onTouchEvent: ACTION_UP begin
activity: onTouchEvent: ACTION_UP return false
activity: dispatchTouchEvent: ACTION_UP return false
带着上面的结论,看下源码验证。
上面的分析中我们已经看到,事件传递最关键就是三个函数:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent
onInterceptTouchEvent | dispatchTouchEvent | onTouchEvent | |
---|---|---|---|
View | 无 | 有 | 有 |
ViewGroup | 有 | 有 | 有 |
Activity | 无 | 有 | 有 |
ViewGroup的dispatchTouchEvent方法比较长,删去不重要的部分,关键地方加上了注释:
public boolean dispatchTouchEvent(MotionEvent ev) {...boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();...// Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {// 这里的结果受自己的requestDisallowInterceptTouchEvent有没有被调用影响final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {// 调用onInterceptTouchEvent。默认实现是返回false,不拦截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;}// Check for cancelation.final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;// 如果前面没有拦截,下面就尝试分发给合适的childif (!canceled && !intercepted) {final int childrenCount = mChildrenCount;// 遍历childrenif (newTouchTarget == null && childrenCount != 0) {final float x = ev.getX(actionIndex);final float y = ev.getY(actionIndex);// Find a child that can receive the event.// Scan children from front to back.final ArrayList<View> preorderedList = buildTouchDispatchChildList();final boolean customOrder = preorderedList == null&& isChildrenDrawingOrderEnabled();final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// 这里判断事件是不是落在这个child上,根据坐标判断if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}// 找到了后续事件应该分发的child,记录起来newTouchTarget = getTouchTarget(child);if (newTouchTarget != null) {// Child is already receiving touch within its bounds.// Give it the new pointer in addition to the ones it wTouchTarget.pointerIdBits |= idBitsToAssign;break;}}if (preorderedList != null) preorderedList.clear();}}}// Dispatch to touch targets.if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.// 前面没有找到合适的分发事件的child,分发给自己// 这里的dispatchTransformedTouchEvent最终会调到自身的onTouchEvent// 默认不处理事件,返回falsehandled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// Dispatch to touch targets, excluding the new touch target if we already// dispatched to it. Cancel touch targets // 分发给前面找到的合适的child// 这里dispatchTransformedTouchEvent最终会调到child的onTouchEventif (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}...}}// 向parent报告本次事件的处理结果,true表示消耗了事件,false表示没有消耗return handled;
}
从代码上看大概是这么几个步骤:
实际应用
RefreshLayout
app开发中有很多场景要用到列表的形式来展示,往下滑需要不断出来新的内容,在列表回到最顶上的时候再往下滑要把整个列表而不是内容往下拉,松开刷新,也就是下拉刷新。
分析一下就会发现,列表我们一般用recyclerView来做,要实现下拉刷新,就得把recyclerView放到另一个可以支持滑动的ViewGroup里,有时候需要把滑动事件分发给外层ViewGroup让它把这个recyclerView往下移动,露出上面的loading样式,大部分时候需要把滑动事件分发给recyclerView让列表内容正常滚动。当两个可以滑动的的View有嵌套关系的时候,就需要小心处理滑动冲突。
不过,在经过上面的分析之后,其实实现一个简陋的下拉刷新控件不难。先看下实现效果:
源码如下:
import android.animation.ValueAnimator;
t.Context;
aphics.Color;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;public class RefreshLayout extends FrameLayout {private RecyclerView child;private TextView loadding;private final String RELEASE_TO_REFRESH = "松手刷新";private final String REFRESHING = "刷新中";public RefreshLayout(@NonNull Context context) {super(context);setBackgroundColor(Color.WHITE);child = new RecyclerView(context);child.setBackgroundColor(Color.WHITE);child.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) {@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);}});child.setAdapter(new RecyclerView.Adapter() {@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {TextView tv = new TextView(getContext());tv.setGravity(Gravity.CENTER);tv.setTextColor(Color.BLACK);return new RecyclerView.ViewHolder(tv) {};}@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {((TextView) holder.itemView).setText(String.valueOf(position));}@Overridepublic int getItemCount() {return 100;}});loadding = new TextView(getContext());loadding.setTextColor(Color.BLACK);loadding.setText(RELEASE_TO_REFRESH);loadding.setGravity(Gravity.CENTER);addView(loadding, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200));addView(child, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));}private boolean childAtTop() {View firstView = ChildAt(0);int firstPosition = ((RecyclerView.LayoutParams) LayoutParams()).getViewAdapterPosition();return firstPosition == 0 && Top() >= 0;}int lastY;boolean childOnTopWhenDown = false;boolean needHandle = false;boolean firstMove = true;@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {int action = Action();int eventY = (int) Y();int dy = 0;switch (action) {case MotionEvent.ACTION_DOWN:lastY = eventY;childOnTopWhenDown = childAtTop();break;case MotionEvent.ACTION_MOVE:dy = eventY - lastY;if (firstMove) {needHandle = childOnTopWhenDown && dy > 0;}lastY = eventY;firstMove = false;break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:needHandle = false;childOnTopWhenDown = false;firstMove = true;scrollChildBack();}if (needHandle) {if (dy < 0) dy = Math.max(-Top(), dy);child.offsetTopAndBottom(dy);return true;} else {return super.dispatchTouchEvent(event);}}private void scrollChildBack() {if (Top() == 0) return;final ValueAnimator animator = ValueAnimator.Top(), 0);animator.setDuration(200L);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {int currentChildTop = (int) AnimatedValue();child.offsetTopAndBottom(currentChildTop - Top());if (currentChildTop < 10) loadding.setText(RELEASE_TO_REFRESH);}});loadding.setText(REFRESHING);postDelayed(new Runnable() {@Overridepublic void run() {animator.start();}}, 1000L);}
}
本文发布于:2024-01-31 07:39:20,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170665796126762.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |