View体系详解(3)

                                                                   View体系详解(3)

  前言:看了大概一个月SystemUI的相关源码,里面关于自定义View的知识比较多,迫使自己要去了解以前不太懂的显示子系统的知识,以前只知道一些粗略的view知识,如它是用来显示具体画面的,它的载体是window,它可以复写事件处理函数去处理某些点击事件,自定义view要实现onMeasure, onLayout, onDraw等,但是一直比较模糊,只是知道个大概,经过一阵子的源码和博客的阅读,对view体系有了许多新认识和领悟,因此记录下来。计划分以下几部分:

     View体系详解(1):View体系总览

      View体系详解(2):自定义View流程以及系统相关行为

      View体系详解(3):View事件处理机制

*写的不好请理解,由于自己知识水平和技术经验所限,不可避免有错漏的地方,恳请指正。***


     View事件处理机制

    input事件对应的类是InputEvent,它是基类,具体的扩展类为KeyEvent和MotionEvent,即按键事件和运动事件,按键一般是点击相关,场景比较简单,比如电源键和音量键,而motion事件一般是用户在屏幕上(触摸等)的和有运动轨迹(鼠标移动等)的操作,场景会比较多样复杂。

    在View体系详解(2):自定义View流程以及系统相关行为 中,我们知道在调用WindowManager的addView后,会在WMS中创建WindowState,并且创建InputChannel,注册InputReceicer,从而可以在对应window产生了input事件后,可以接收到。 从宏观角度讲,当我们点击或者触摸产生后,会有电压之类的变化,产生中断,驱动程序处理完成后传递到EventHub,然后唤醒systemserver的InputReader线程读取,再由InputDispatcher线程去分发,然后根据当前事件产生的类型,以及所属的window,最后派发到对应上层的ViewRootImpl类的注册的接收器InputReceiver中,从这里才开始派发流程。这些流程有个概念就行,那么现在看看InputReceiver这个抽象类的入口函数:

// Called from native code.

@SuppressWarnings("unused")

private void dispatchInputEvent(int seq, InputEvent event) {

   mSeqMap.put(event.getSequenceNumber(), seq);  //保存此次接受到的input事件

   onInputEvent(event);  //开始派发

}

    ViewRootImpl中的WindowInputEventReceiver中复写了onInputEvent:

@Override

public void onInputEvent(InputEvent event) {

   enqueueInputEvent(event, this, 0, true);

}

  由此可见,input事件的在队列中等待处理的,这是一个链表队列,有几个变量需要知道:

// Pool of queued input events.

   private static final int MAX_QUEUED_INPUT_EVENT_POOL_SIZE = 10;

   private QueuedInputEvent mQueuedInputEventPool; //InputEvent对象的回收池

   private int mQueuedInputEventPoolSize;  //当前回收池用掉的容量

--------------------------------------------------------------------------

   QueuedInputEvent mPendingInputEventHead; //链表头

   QueuedInputEvent mPendingInputEventTail; //链表尾

   int mPendingInputEventCount;  //链表中对象的数量

  然后再看看入队操作:

void enqueueInputEvent(InputEvent event,

       InputEventReceiver receiver, int flags, boolean processImmediately) {

   adjustInputEventForCompatibility(event);

   QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); // 尝试从回收池中复用空对象,没有则新建一个返回

   QueuedInputEvent last = mPendingInputEventTail;  

    if (last == null) {  //链表尾为空,说明链表为空,则让链表头指向此event,链表尾同理

       mPendingInputEventHead = q;

       mPendingInputEventTail = q;

    } else {  //否则链表尾指向该事件,即从尾部插入

       last.mNext = q;

       mPendingInputEventTail = q;

    }

    mPendingInputEventCount += 1;

   if (processImmediately) {  

      doProcessInputEvents();  //马上处理,从native层上来的事件都走这

   } else {

      scheduleProcessInputEvents();  //稍后处理

   }

}

    处理过程:

void doProcessInputEvents() {

   // Deliver all pending input events in the queue.

   while (mPendingInputEventHead != null) {  //链表头不为空,说明队列中有待处理的input事件

       QueuedInputEvent q = mPendingInputEventHead;

       mPendingInputEventHead = q.mNext;  //链表头指向队列中的第二个input事件

       if (mPendingInputEventHead == null) {  //如果指向为null,说明队列中的事件全部处理完了,链表尾也置null

           mPendingInputEventTail = null;

       }

       q.mNext = null;  //本次处理的事件从链表中脱离出来,方便待会回收

       mPendingInputEventCount -= 1;

       deliverInputEvent(q); //开始派发事件

   }

}

  至此开始进入派发流程,前面都是对于队列的操作,安卓中像这种回收复用对象的处理方式很多,如handler机制中的Message也是这样,避免了大量的new和destroy对象的操作,节省了内存操作,对于频繁的操作来说是可以提升性能的

  派发流程是责任链设计模式,一旦input事件被这条链上的任一对象处理了,就中断input事件的传递,然后返回。在setView()的时候,最后会创建一系列的InputStage对象,然后组成一个责任链:

// Set up the input pipeline.

CharSequence counterSuffix = attrs.getTitle();

mSyntheticInputStage = new SyntheticInputStage();

InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);

InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,

       "aq:native-post-ime:" + counterSuffix);

InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);

InputStage imeStage = new ImeInputStage(earlyPostImeStage,

       "aq:ime:" + counterSuffix);

InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);

InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,

       "aq:native-pre-ime:" + counterSuffix);

mFirstInputStage = nativePreImeStage;

mFirstPostImeInputStage = earlyPostImeStage;

mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;

      开始派发事件的函数:

private void deliverInputEvent(QueuedInputEvent q) {

   InputStage stage;

   if (q.shouldSendToSynthesizer()) {

       stage = mSyntheticInputStage;

   } else {

       stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;

   }

   if (stage != null) {

       stage.deliver(q);

   } else {

       finishInputEvent(q);

   }

}

    一般是从mFristInputStage中开始传递,其他特殊情况先不考虑,我也暂时没碰到过,那么从责任链我们知道,mFirstInputStage指向的是NativePreImeStage对象,调用的deliver函数为父类的,并未复写:

/**

* Delivers an event to be processed.

*/

public final void deliver(QueuedInputEvent q) {

   if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {

       forward(q);

   } else if (shouldDropInputEvent(q)) {

       finish(q, false);

   } else {

       apply(q, onProcess(q));  //走这,调用子类的onProcess,使其有机会去处理,如果没处理,那么就传递给责任链上的下一个对象,直到遍历完

   }

}

    该对象的onProcess函数:

@Override

protected int onProcess(QueuedInputEvent q) {

   if (mInputQueue != null && q.mEvent instanceof KeyEvent) {

       mInputQueue.sendInputEvent(q.mEvent, q, true, this);

       return DEFER;

   }

   return FORWARD;

}

    这是往native传递keyevent事件,我们先不看,假设它没被处理,那么返回的是FORWARD,这表明这个对象不处理此事件,传递给下一个,这样最后到了ViewPostImeInputStage对象,又走一遍流程,所以我们主要看ViewPostImeInputStage对象的onProcess函数能否处理掉此次事件:

@Override

protected int onProcess(QueuedInputEvent q) {

   if (q.mEvent instanceof KeyEvent) {  //如果是keyevent

       return processKeyEvent(q);

   } else {  //是motionevent

       final int source = q.mEvent.getSource();

       if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {

           return processPointerEvent(q);

       } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {

           return processTrackballEvent(q);

       } else {

           return processGenericMotionEvent(q);  

       }

   }

}

  从这里开始,进入到我们熟悉的View树遍历环节了。这里着重讲processPointerEvent(q)这个函数,因为我们用的最多,如果是keyevent,遍历逻辑是类似的。

private int processPointerEvent(QueuedInputEvent q) {

   final MotionEvent event = (MotionEvent)q.mEvent;

   boolean handled = mView.dispatchPointerEvent(event);

   return handled ? FINISH_HANDLED : FORWARD;

}

    可以看到这里调用了顶层View的dispatchPointerEvent()进入view树的遍历了,如果返回了true,说明找到了事件的处理者,那么我们就返回FINISH_HANDLED,这样就完成了此次input事件的处理,这个稍后谈。从这回到了我们的View里面:

public final boolean dispatchPointerEvent(MotionEvent event) {

   if (event.isTouchEvent()) {  //是否为touch事件是底层根据手势等决定的,我们一般情况下认为是

       return dispatchTouchEvent(event);

   } else {

       return dispatchGenericMotionEvent(event);

   }

}

    由于这里是顶层View,所以我们要找ViewGroup中的复写的dispatchTouchEvent():

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

   boolean handled = false;

       // Check for interception.

       final boolean intercepted;

       if (actionMasked == MotionEvent.ACTION_DOWN

               || mFirstTouchTarget != null) {

           final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

           if (!disallowIntercept) {   //如果允许拦截事件

               intercepted = onInterceptTouchEvent(ev);  //决定是否拦截本次touch事件

               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;

       }

       // Dispatch to touch targets.

       if (mFirstTouchTarget == null) {

           // No touch targets so treat this as an ordinary view.

           handled = dispatchTransformedTouchEvent(ev, canceled, null,  //从这里往子view派发事件,直到有人返回true

                   TouchTarget.ALL_POINTER_IDS);

       } 

   return handled;

}

  注意到我们可以复写onInterceptTouchEvent函数去拦截从顶层view开始派发的input事件,这个可以根据业务场景复写,现在我们不考虑拦截的情况,让派发流程进行下去。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,

       View child, int desiredPointerIdBits) {

   final boolean handled;

   // Canceling motions is a special case.  We don't need to perform any transformations

   // or filtering.  The important part is the action, not the contents.

   final int oldAction = event.getAction();

       if (child == null) {

           handled = super.dispatchTouchEvent(event);  //没有子view,那就自己处理

       } else {

           handled = child.dispatchTouchEvent(event);  //交给子view去处理

       }

       event.setAction(oldAction);

       return handled;

   }

  终于到子view去处理了,这里就有熟悉的身影了:

public boolean dispatchTouchEvent(MotionEvent event) {

   boolean result = false;

   final int actionMasked = event.getActionMasked();

   if (onFilterTouchEventForSecurity(event)) {

       if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {

           result = true;

       }

       //noinspection SimplifiableIfStatement

       ListenerInfo li = mListenerInfo;

       if (li != null && li.mOnTouchListener != null

               && (mViewFlags & ENABLED_MASK) == ENABLED

               && li.mOnTouchListener.onTouch(this, event)) {  //先调用onTouch,如果处理了就返回true

           result = true;

       }

       if (!result && onTouchEvent(event)) {  //再调用onTouchEvent,这里前提是onTouch没被复写或者返回了false

           result = true;

       }

   }

   return result;

}

---------------------------------------------------------------------------------------------

  public boolean onTouchEvent(MotionEvent event) {

       final float x = event.getX();

       final float y = event.getY();

       final int viewFlags = mViewFlags;

       final int action = event.getAction();

       final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE

               || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)

               || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

           switch (action) {

               case MotionEvent.ACTION_UP:  //手指抬起

             //省略

                performClick();   //如果手指抬起来,复写的click事件处理监听会被调用

               case MotionEvent.ACTION_DOWN:

                   if (!clickable) {

                       checkForLongClick(0, x, y);

                       break;

                   }

                   if (performButtonActionOnTouchDown(event)) {  //可以复写此函数然后阻断longclick被调用,表明view被按下

                       break;

                   }

                   checkForLongClick(0, x, y);  //按下去,那么有可能是长按事件,判断是长按的话,复写的onLongClick会被调用

               case MotionEvent.ACTION_CANCEL:  //cancel事件我们就不处理

                   if (clickable) {

                       setPressed(false);

                   }

                   removeTapCallback();

                   removeLongPressCallback();

                   mInContextButtonPress = false;

                   mHasPerformedLongPress = false;

                   mIgnoreNextUpEvent = false;

                   mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

                   break;

               case MotionEvent.ACTION_MOVE:   //move事件是手指在屏幕上划动

                   if (clickable) {

                       drawableHotspotChanged(x, y);

                   }

                   // Be lenient about moving outside of buttons

                   if (!pointInView(x, y, mTouchSlop)) {

                       // Outside button

                       // Remove any future long press/tap checks

                       removeTapCallback();

                       removeLongPressCallback();

                       if ((mPrivateFlags & PFLAG_PRESSED) != 0) {

                           setPressed(false);

                       }

                       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

                   }

                   break;

           }

           return true;

       }

       return false;

   }

  这里就明确了,我们平常调用的api,诸如setOnClickEventListener等,是怎么被调用到我们复写的函数了,就是在派发过程中派发到了具体某个case,触发的。这里也揭示了我们监听的优先级,即onTouch > onTouchEvent > onClick > performButtonActionOnTouchDown > onLongClick。至此,就完成了view树的整个遍历了。贴上一张图方便理解:

分发流程

  由于篇幅关系,很多代码省略了,以后如果遇到某些情况,还是得仔细看看源码怎么回事,这里还是介绍一个较为详细的inputevent是如何派发的,它们之间的优先级是什么,此处也只是java层的framework代码,至于如何从eventhub中读取到事件,事件怎么知道要派发给哪个窗口等逻辑,此处均不涉及。

  这里假设我们复写了onTouchEvent事件,然后返回了true,那么ViewRootImpl那边会怎样呢?之前就说过,一旦onProcess的dispatchPointerEvent返回了true,那么我们就会返回FINISH_HANDLED,最后调用finishInputEvent:

private void finishInputEvent(QueuedInputEvent q) {

Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "deliverInputEvent",

            q.mEvent.getSequenceNumber());

    if (q.mReceiver !=null) {

boolean handled = (q.mFlags & QueuedInputEvent.FLAG_FINISHED_HANDLED) !=0;

        q.mReceiver.finishInputEvent(q.mEvent, handled);   //告诉底层派发的线程,该事件处理完毕

    }else {

q.mEvent.recycleIfNeededAfterDispatch();

    }

recycleQueuedInputEvent(q);   //回收该事件的对象

}

   这样,一次完整的事件派发就结束了。

总结:

     1.input事件的处理是责任链模式,让每个对象和view都能遍历到,一旦产生了处理者,那么就交给这个处理者去处理,不再传递下去。

      2.input事件的处理优先级是代码中的调用顺序决定的,onTouch如果不处理,那么onTouchEvent才会被调用,然后如果有子view,那么就又优先让子view去遍历一遍,直到找到处理者,当然我们也可以复写某些函数去阻断和处理input事件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341