触摸事件分发阅读笔记

简介

Android开发中触摸事件是经常用到,除了对基本事件的处理,另外一个很重要的就是事件的分发,事件的分发是指在一个屏幕中那么多的视图,究竟是遵循什么规则分发,从而将整个事件完成
以下代码均来源与Android的API25的源码

事件

首先说一下Android中常用的触摸事件
ACTION_DOWN:手指按下触摸,一定是某一个事件的开端
ACTION_MOVE:手指移动
ACTION_UP:手机松开
正常来说一个序列会有
DOWN->UP(手指按下然后松开,类似点击,虽然点击的判断没有那么简单)
DOWN->MOVE...->MOVE->UP(手指按下然后移动,最后松开)
等等
ACTION_POINTER_DOWN:当前已经有一个手指触发了DOWN事件,此时有其它手指按下的时候会触发的事件。
ACTION_POINTER_UP:当前有手指不再触摸,并且一定还有手指在继续触摸的时候触发的事件。
getActionIndex:每一个手指触摸到屏幕之后都会分配一个index,这个index会随着手指离开之类的情况变化,但是可以通过index获取当前手指唯一对应的PointerId。
getPointerId:每一个手指触摸到屏幕之后都会分配一个唯一的id,后续有手指离开之类的话也不会发生变化。

分发

在Android中,触摸事件要想到指定的视图上面,那么当手指按下的时候,事件需要层层向下进行分发。而事件首先是来到activity上
Activity

    /**
     * Activity进行触摸事件的分发
     * @param ev 当前对应的事件
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //可以看到,将事件的分发交给了window
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;//如果window成功将事件分发下去,则当前事件结束
        }
        //否则事件回到Activity的onTouchEvent(ev)进行处理
        //该方法默认为空,需要自己处理
        return onTouchEvent(ev);
    }

Activity将当前事件分发给了window,
其中window的实现类为PhoneWindow

    //实际上就是Activity的顶层视图
    private DecorView mDecor;
    
    /**
     * 实际上就是将事件分发给了DecorView
     */
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

PhoneWindow将事件再次分发到DecorView,DecorView是一个Activity的视图的起点,Android的视图结构本身是一个视图树,那么对于一个Activity来说,DecorView就是视图树的根节点。

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks

    /**
     * 交给父类处理
     */
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

实际上DecorView就是一个FrameLayout,那么这里委托最后就是到了ViewGroup来处理

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //... 去掉一些代码
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 手指按下事件,也是所有事件的开头
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 还原一些标志,主要是还原Touch链表为null,即mFirstTouchTarget为null
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            final boolean intercepted;
            //1.当前事件为DOWN事件,可以理解为一次新的事件的开始
            //2.mFirstTouchTarget != null意味着之前DOWN事件已经有子视图处理,现在是非DOWN的后续事件
            //当DOWN事件发下去之后,对于任何ViewGroup来说如果还有子视图(ViewGroup/View)来处理事件
            //那么对于这些ViewGroup来说自身的mFirstTouchTarget必然不会为空,从顶向下看形成了一个事件链条
            //对于后续事件来说,如果没有禁止当前ViewGroup拦截事件,则事件还是会先询问ViewGroup是否拦截
            //也就是说每一个事件都会尝试先问父布局是否拦截事件,也就是说父布局有处理事件的优先权
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //获得当前ViewGroup是否被禁止尝试拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//允许当前ViewGroup去尝试拦截事件
                    intercepted = onInterceptTouchEvent(ev);//尝试去拦截当前事件
                    ev.setAction(action); //还原事件,避免在onInterceptTouchEvent(ev)过程中事件被修改
                } else {//当前ViewGroup不拦截事件
                    intercepted = false;
                }
            } else {//DOWN事件没有子视图处理,非DOWN的事件默认都由当前ViewGroup处理
                intercepted = true;
            }
            //...

            // 标记当前是否为ACTION_CANCEL事件
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //这个属性默认为true,实际上可以在xml中通过splitMotionEvents设置
            //实际是用于标志当前ViewGroup是否允许将事件分发到两个同级的子视图
            //后面会说到
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {//当前并非取消事件,并且当前ViewGroup不拦截事件
                // ...
                //1.当前是处理DOWN事件
                //2.当前是否允许分发同级事件
                //如果不允许,则不会重新查找子视图来接收ACTION_POINTER_DOWN
                //那么将会把ACTION_POINTER_DOWN交给之前处理了事件的视图
                //如果允许,会重新查找子视图来接收ACTION_POINTER_DOWN,此时mFirstTouchTarget有可能出现多个节点的情况
                //ACTION_HOVER_MOVE不考虑,基本没用过
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex();
                    //每一个手指的触摸事件的PointerId都是固定的,0-1-2...N
                    //设计的时候通过移位来表示即 0000 .... 0010表示id为1
                    //0000 .... 0111 表示当前手指有3个,id分别为0,1,2
                    //这里如果允许同级分发的话
                    //idBitsToAssign实际上就是用于记录当前事件的PointerId,类似0000 .... 0001
                    //不允许同级,使用默认的-1
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    //将之前TouchTarget链表中有着相同PointerId的节点移除
                    removePointersFromTouchTargets(idBitsToAssign);
                    //获得当前ViewGroup的所有孩子数目
                    final int childrenCount = mChildrenCount;
                    //如果当前有可以分发的子视图
                    if (newTouchTarget == null && childrenCount != 0) {
                        //获取当前DOWN事件的坐标
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        //获得一个子视图组,这里的子视图组顺序会影响到那一个子视图可以优先处理DOWN事件
                        //在setChildrenDrawingOrderEnabled的基础上,可以通过getChildDrawingOrder改变映射
                        //具体见buildTouchDispatchChildList
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序遍历,如果没有设置setChildrenDrawingOrderEnabled的情况,默认是按照正序
                        //简单说就是在一个xml中,节点位置越靠后的越先
                        //比方说FrameLayout中有两个全屏的View,那么在xml中后面的View会优先(不考虑Z轴的前提)
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //之前可能通过setChildrenDrawingOrderEnabled,然后重写了getChildDrawingOrder改变了位置映射
                            //所以后续都要映射回原来View[]的下标
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //...

                            //判断当前view是否可以处理当前事件,
                            //主要是要求当前view可见并且当前事件的坐标在view的范围内
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;//否则继续循环查找下一个可以接收这个事件的view
                            }
                            //当前子视图可以接收当前事件
                            //尝试从当前事件节点链表中查找当前view,链表中不应该重复
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                //可能出现DOWN和ACTION_POINTER_DOWN同时由一个子视图处理
                                //此时复用链表中的一个节点即可
                                //修改节点对应的PointerId
                                //类似于0000 .... 0011表示当前视图处理id为0和1的两个手指事件
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //尝试进行DOWN、ACTION_POINTER_DOWN的事件分发
                            //这里实际上是一个递归调用,将事件交给当前ViewGroup的下一级ViewGroup
                            //然后下一级ViewGroup重新走DispatchTouchEvent进行分发
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                //有视图成功处理了当前事件

                                //... 记录一些DOWN事件的信息

                                //将事件节点添加到链表的头部
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //标记DOWN事件已经分发给了新的节点处理
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                        //...
                    }
                    //上面主要是处理当前ViewGroup不拦截事件,然后分发DOWN事件给子视图的逻辑
                    //如果之前有子视图处理了DOWN事件,同时默认split为true
                    //此时如果是ACTION_POINTER_DOWN事件,但是却没有找到合适的子视图处理
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        //这里相当于找到最开始处理DOWN事件的节点
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        //标记当前节点处理当前手指,简单理解就是一个手指对应一个id
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            if (mFirstTouchTarget == null) {
                //1.当前DOWN事件没有子视图处理,那么最后会到这里由当前ViewGroup处理
                //形象来说就是把事件从顶向下发送,如果最下面的视图都不处理,那么事件会向上传递
                //2.当前事件本来就被当前ViewGroup拦截,无论是DOWN还是MOVE之类的事件,mFirstTouchTarget也会一直为null
                //直接把事件交给当前ViewGroup处理即可
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {//之前事件已经分发给了子视图处理
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //链表在一个手指的前提下基本可以认为只有一个节点,但是有的时候比方说允许split,而且当前DOWN和ACTION_POINTER_DOWN所处理的子视图不同,此时的节点数量就会大于1
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //因为之前专门处理了DOWN/POINTED_DOWN事件,这里可以直接标记事件已经处理
                        handled = true;
                    } else {//到这里的一般都是MOVE、UP之类的事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //在之前DOWN事件已经分发给了指定的子视图的前提下
                        //1.如果当前ViewGroup要求拦截事件
                        //假设现在有一个MOVE事件到来,但是当前ViewGroup要拦截MOVE事件
                        //这意味这事件序列不能交给之前的子视图处理
                        //此时向之前处理事件的子视图分发一个cancel事件,告诉它这个事件序列被取消了
                        //此时子视图的事件序列完成
                        //2.当前ViewGroup不拦截事件
                        //当前事件直接交给上一次处理事件的子视图即可
                        //这里会通过一直向下分发起到最终到之前的子视图
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //因为当前ViewGroup拦截当前事件,导致子视图的事件被取消
                        //此时回收节点,意味着mFirstTouchTarget链表为null,事件会给当前ViewGroup处理
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //当前事件已经取消、或者是ACTION_UP
                //还原状态,主要是清空TouchTarget链表等操作
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                //当前允许同级分发,此时为ACTION_POINTER_UP事件,意味着有一个手指离开
                //此时将该手指对应的链表节点移除,后续不应该处理其它事件
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }
        //...
        return handled;
    }

1.首先可以看到对于每一个ViewGroup来说都会有一个TouchTarget链表,这个是用来存储当前ViewGroup确定分发事件的下一级视图(ViewGroup或者View),如果只是单纯的单指操作,那么TouchTarget只会有一个节点,如果有多指操作,并且接收的子视图不一样,此时会有多个节点。每一个ViewGroup都有一个链表,从顶往下看就可以形成一个事件传递链表。
2.可以通过在xml中设置splitMotionEvents="false"来禁止多个同级视图处理事件,常用的场景就是一个页面,有时候不希望有人同时点击两个地方,导致同时拉起两个页面或者出现一些不可预知问题。
3.对于简单的单指操作来说,事件本身可以看做一个序列,只要有一个子视图处理了DOWN事件,后续事件都会交给它继续处理,但是每一个事件是否可以到达子视图,要看当前ViewGroup是否拦截事件,如果ViewGroup拦截了事件,则子视图只会收到一个ACTION_CANCEL事件,然后事件重新开始。
4.可以通过requestDisallowInterceptTouchEvent禁止ViewGroup拦截事件,不过注意这个标志每一次DOWN事件开始分发的时候都会被重置。

    /**
     * getAndVerifyPreorderedIndex的实际执行方法
     * 获得当前ViewGroup的视图组
     * 后面默认是倒序遍历,主要用于确认哪一个视图优先可以尝试处理事件
     */
    ArrayList<View> buildOrderedChildList() {
        final int childrenCount = mChildrenCount;
        if (childrenCount <= 1 || !hasChildWithZ()) return null;

        if (mPreSortedChildren == null) {
            mPreSortedChildren = new ArrayList<>(childrenCount);
        } else {
            // callers should clear, so clear shouldn't be necessary, but for safety...
            mPreSortedChildren.clear();
            mPreSortedChildren.ensureCapacity(childrenCount);
        }
        //这个可以通过setChildrenDrawingOrderEnabled设置,默认false
        final boolean customOrder = isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            //如果customOrder为true,这里可以通过重写ViewGroup的getChildDrawingOrder来改变接收事件的视图顺序
            //默认就是i
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            //拿出当前视图
            final View nextChild = mChildren[childIndex];
            //在Android5.0之后提出的Z轴概念
            final float currentZ = nextChild.getZ();

            //无论是否重新映射了下标位置,但是都会按照Z轴排序
            //简单说就是Z坐标越大的在列表的位置越后
            //列表是按照Z坐标的升序排列
            int insertIndex = i;
            while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                insertIndex--;
            }
            mPreSortedChildren.add(insertIndex, nextChild);
        }
        return mPreSortedChildren;
    }

可以通过setChildrenDrawingOrderEnabled和重写getChildDrawingOrder来改变优先尝试处理事件的位置,比方说ViewA的index为0,ViewB的index为1,此时事件同时在ViewA和ViewB的区域,事件默认会先到ViewB,如果要到ViewA,可以倒序映射。不过实际上这两个参数就是用于改变绘制的顺序,传统的是index小的先画即ViewA,然后ViewB可能会盖住它,此时事件先到ViewB也是正常,如果颠倒绘制顺序,那么就会先画ViewB,ViewA会盖住ViewB,此时事件先到ViewA也是正常的。
之前提到事件的分发

/**
     * 分发指定的事件给指定的视图
     * @param event 当前应该被分发的事件
     * @param cancel 当前事件是否被取消
     * @param child 当前应该被分发事件的子视图,null表示当前ViewGroup处理
     * @param desiredPointerIdBits 当前事件的id
     * @return true表示当前事件分发完成,并且被处理,false表示没视图处理当前事件
     */
    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) {//由当前ViewGroup处理
                handled = super.dispatchTouchEvent(event);
            } else {//将事件交由子视图处理
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // 获得当前有多少个手指触摸中,并且可以获得对应id
        // 比方0000 .... 0111表示3个手指,其中ID为0,1,2
        final int oldPointerIdBits = event.getPointerIdBits();
        //与当前事件id
        //比方说当前事件id为1,即0000 .... 0010
        //得到就是0000 .... 0010
        //除非当前事件id不在记录的触摸点中,此时会得到0
        //对于一个手指的事件来说,oldPointerIdBits总是等于newPointerIdBits
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;


        //因为一些原因可能产生了一个没有触摸点对应的事件
        //丢弃当前事件
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            //当前是同一个触摸手指的事件,不需要进行事件的转换
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            //当前有多个手指的触摸中,此时要转换事件
            //比方说在Split模式下,POINTED_DOWN和POINTED_UP在某一个视图中会被转换为DOWN和UP
            //从而触发点击事件等等
            transformedEvent = event.split(newPointerIdBits);
        }

        //将转换后的事件分发下去
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // 回收当前事件
        transformedEvent.recycle();
        return handled;
    }

最终处理事件的时候会到super.dispatchTouchEvent,其实就是View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
        //...
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            //...
            //首先回调了OnTouchListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //如果OnTouchListener返回true
            //不会继续调用OnTouchEvent
            if (!result && onTouchEvent(event)) {
                //实际上默认的OnClickListener是在默认的onTouchEvent里面处理的
                result = true;
            }
        }
        //...

        return result;
    }

可以看到,实际上处理事件的时候,如果通过setOnTouchListener设置了OnTouchListener,则优先回调OnTouchListener,如果OnTouchListener处理后返回true,则当前事件处理完成。
否则继续onTouchEvent方法,其中onTouchEvent方法里面默认实现了onClickListener的回调。

结语

可以简单的总结Android中的事件传递成几个规则
1.事件是序列化的,即一般情况下,DOWN先开始,然后是MOVE或者UP等操作,这就是一个完整序列。
2.每一个事件父布局总是有优先拦截权利的,子视图可以通过requestDisallowInterceptTouchEvent禁止父布局拦截事件,合理利用这些就可以处理大部分手势冲突。
3.当事件到一个视图处理的时候onTouchListener>onTouchEvent>onClickListener

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

推荐阅读更多精彩内容

  • 本篇和触摸事件的分发(View篇)将侧重于Android源码的分析。略显枯燥,但Read the fucking ...
    张利强阅读 2,824评论 4 5
  • 摘录自:wangkuiwu' Homepage ps: 需要具体源码分析的童鞋可以转战原文博客,内容十分详尽,此文...
    Jimmy_gjf阅读 1,946评论 0 6
  • View的事件分发机制 View的事件分发机制简单来说就是将用户与手机屏幕的交互事件交由正确的控件进行处理,从而可...
    蕉下孤客阅读 852评论 0 4
  • 设计UI时,亲爱的交互设计师们总会有一些天马行空的想法,大多数情况下原生的控件已不能支持这些“看似简单”的交互逻辑...
    布隆阅读 2,358评论 9 25
  • 欢迎来到1000亿美元的数据挖掘市场,这仿佛是一部国际大片的开场,刷刷刷屏幕上无数的数据令你眼花缭乱。你的心未动,...
    秦广玲阅读 369评论 0 0