View·InputEvent事件投递源码分析(一)

总结

本文从源码角度,描述了如下4个阶段:

  • 从底层硬件产生的触摸事件,并将事件传递到InputEventReceiver中。
  • InputEventReceiver沟通ViewRootImpl,将事件计入ViewRootImpl的事件队列中。
  • ViewRootImpl通过多个Stage职责对象构成职责链,来按序处理事件。
  • 不同的Stage对象将触摸事件投递到不同的对象中(触摸板、导航栏、实体按键或视图)。

概述

这里的事件是指来源于硬件的事件,诸如:屏幕的按压、触摸(屏幕解锁),实体按键的按压(调整音量),甚至于实体按键的组合使用(截屏)。

事件分类

// 公共基础类输入事件。
// 提供了获取输入设备,修改事件来源,深拷贝,序列化,事件重用的功能
public abstract class InputEvent implements Parcelable {

}

// 用于反馈:键和按钮事件的对象。
public class KeyEvent extends InputEvent implements Parcelable {

}

// 用于报告运动(鼠标,笔,手指,轨迹球)事件的对象。
// 运动事件可以保持绝对或相对运动和其他数据,取决于设备的类型。
public final class MotionEvent extends InputEvent implements Parcelable {
}

依据事件的分类描述,则对屏幕的触摸、滑动事件都应由MotionEvent处理。事实也是如此:

// android.view.View
public boolean dispatchTouchEvent(MotionEvent event) {
}

事件产生 —— 通过命令创造

MotionEvent事件为例,先尝试分析 MotionEvent对象是如何创建的。提到创建对象必然逃不开new关键字,所以全文搜索下不难找到下面代码片段。

因为MotionEvent存在大量的创建与释放,所以在这里构建了一个大小为MAX_RECYCLED (10)的池,以便于复用。

    static private MotionEvent obtain() {
        final MotionEvent ev;
        synchronized (gRecyclerLock) {
            ev = gRecyclerTop;
            if (ev == null) {
                return new MotionEvent();
            }
            gRecyclerTop = ev.mNext;
            gRecyclerUsed -= 1;
        }
        ev.mNext = null;
        ev.prepareForReuse();
        return ev;
    }

   /**
     * 回收MotionEvent,以供稍后调用者重复使用。
     * 调用此函数后,您不能再次触摸事件(因为其已被释放)。
     */
    @Override
    public final void recycle() {
        super.recycle();

        synchronized (gRecyclerLock) {
            if (gRecyclerUsed < MAX_RECYCLED) {
                gRecyclerUsed++;
                mNext = gRecyclerTop;
                gRecyclerTop = this;
            }
        }
    }

如果你打开编辑器并且进入到 MotionEvent类中,你会发现该类有多个obtain方法(如下图)。�

考虑到 MotionEvent的无参数obtain()方法是私有的,并且其他的obtain(xxx)方法都在内部引用了无参数obtain()方法。所以外部一定需要调用obtain(xxx)的方法,并且该参数一定不是MotionEvent

MotionEvent

所以排除上图中的倒数两个方法,并对其它Android推荐的方法(倒数4、5已不被推荐依次进行全文查找。

最终查找结果如下:

public class Input {
   
    private void sendMove(int inputSource, float dx, float dy) {
        long now = SystemClock.uptimeMillis();
        injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }
}

    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
        final float DEFAULT_SIZE = 1.0f;
        final int DEFAULT_META_STATE = 0;
        final float DEFAULT_PRECISION_X = 1.0f;
        final float DEFAULT_PRECISION_Y = 1.0f;
        final int DEFAULT_DEVICE_ID = 0;
        final int DEFAULT_EDGE_FLAGS = 0;
        MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
                DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
                DEFAULT_EDGE_FLAGS);
        event.setSource(inputSource);
        InputManager.getInstance().injectInputEvent(event,
                InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

没什么好说的,继续跟进 InputManager

public final class InputManager {
    private final IInputManager mIm;
   
    public boolean injectInputEvent(InputEvent event, int mode) {
        if (event == null) {
            throw new IllegalArgumentException("event must not be null");
        }
        if (mode != INJECT_INPUT_EVENT_MODE_ASYNC
                && mode != INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
                && mode != INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT) {
            throw new IllegalArgumentException("mode is invalid");
        }

        try {
            return mIm.injectInputEvent(event, mode);
        } catch (RemoteException ex) {
            return false;
        }
    }
}

到底问题的关键变成了mImmIm的类型是IInputManager

public static InputManager getInstance() {
        synchronized (InputManager.class) {
            if (sInstance == null) {
                IBinder b = ServiceManager.getService(Context.INPUT_SERVICE);
                sInstance = new InputManager(IInputManager.Stub.asInterface(b));
            }
            return sInstance;
        }
    }

看来IInputManagerInputManager的远程代理对象,他们之间通过Binder通讯。

IInputManager:用代码模拟屏幕点击、触摸事件

事件产生 —— 通过屏幕触摸

底层硬件调用

你对设备的触摸事件,将通过底层方法进行调用。因为目前我对 Native 部分的知识比较匮乏,所以就不对具体调用过程展开分析了。此处就将底层会处理相关的事件,并生成MotionEvent的对象并调用InputEventReceiver#dispatchInputEvent()方法作为论据,为下一步的分析提供支撑。

InputEventReceiver是事件的源头

public abstract class InputEventReceiver {
    // 被 native 方法调用
    @SuppressWarnings("unused")
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }
}

ViewRootImpl 绝佳的中介

ViewRootImpl 绝佳的中介,它既联系了WMS、Window、PhoneWindow、View这条线,同时也联系了InputEvent。所以它具备将InputEvent派发到指定的View的能力。

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {

    void enqueueInputEvent(InputEvent event,
                           InputEventReceiver receiver, int flags, boolean processImmediately) {
        adjustInputEventForCompatibility(event);
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

        // 始终按顺序将输入事件排队,而不管其时间戳。
        // 我们这样做是因为应用程序或IME可以注入关键事件以响应触摸事件,
        // 并且我们要确保注入的键按照它们被接收的顺序被处理,
        // 并且我们不能相信注入的事件的时间戳是单调的。
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);

        if (processImmediately) {
            doProcessInputEvents();
        } else {
            scheduleProcessInputEvents();
        }
    }

    private QueuedInputEvent obtainQueuedInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags) {
        QueuedInputEvent q = mQueuedInputEventPool;
        if (q != null) {
            mQueuedInputEventPoolSize -= 1;
            mQueuedInputEventPool = q.mNext;
            q.mNext = null;
        } else {
            q = new QueuedInputEvent();
        }

        q.mEvent = event;
        q.mReceiver = receiver;
        q.mFlags = flags;
        return q;
    }

}

QueuedInputEvent理解成封装了InputEventInputEventReceiver的封装类即可。

另外processImmediately类为true,那么enqueueInputEvent会立即调用doProcessInputEvents。否则会加入到消息队列中,然后按顺序处理。当然无论以何种方式,最终都会调用到doProcessInputEvents

输入事件的处理

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {
    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);
        }
    }
}

看到事件分发时,InputStage类作为基类它实现了职责链的模板,并且stage.deliver(q)方法实现了启动了职责链的处理流程。

职责链番外篇 ——职责链模板之职责对象 “输入阶段”

abstract class InputStage {
        private final InputStage mNext;

        protected static final int FORWARD = 0;
        protected static final int FINISH_HANDLED = 1;
        protected static final int FINISH_NOT_HANDLED = 2;

        public InputStage(InputStage next) {
            mNext = next;
        }

        // ... 省略无关代码

        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));
            }
        }

        protected void finish(QueuedInputEvent q, boolean handled) {
            q.mFlags |= QueuedInputEvent.FLAG_FINISHED;
            if (handled) {
                q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;
            }
            forward(q);
        }

        protected void forward(QueuedInputEvent q) {
            onDeliverToNext(q);
        }

        protected void apply(QueuedInputEvent q, int result) {
            if (result == FORWARD) {
                forward(q);
            } else if (result == FINISH_HANDLED) {
                finish(q, true);
            } else if (result == FINISH_NOT_HANDLED) {
                finish(q, false);
            } else {
                throw new IllegalArgumentException("Invalid result: " + result);
            }
        }

        protected void onDeliverToNext(QueuedInputEvent q) {
            if (DEBUG_INPUT_STAGES) {
                Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
            }
            if (mNext != null) {
                mNext.deliver(q);
            } else {
                finishInputEvent(q);
            }
        }
    }

既然基类InputStage提供了职责链的模板,也提供了一系列onProcess、forward、finish、apply方法,其目的也不言而喻:提供子类进行扩展的便捷。


// ----------------- InputStage的子类 ----------------------------
// 将预先输入事件提供给视图层次结构。
final class ViewPreImeInputStage extends InputStage {}
// 执行事后输入事件的早期处理。
final class EarlyPostImeInputStage extends InputStage {}
// 将后期输入事件提供给视图层次结构。
final class ViewPostImeInputStage extends InputStage {}
// 从未处理的输入事件执行新输入事件的合成。
final class SyntheticInputStage extends InputStage {}
// 用于实现支持输入事件的异步和无序处理的输入流水线级的基类。
abstract class AsyncInputStage extends InputStage {}
// ----------------- AsyncInputStage的子类----------------------------
// 将预先输入事件提供给 NativeActivity。
final class NativePreImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {}
// 将预先输入事件提供给视图层次结构。
final class ImeInputStage extends AsyncInputStage
            implements InputMethodManager.FinishedInputEventCallback {}
// 将事后输入事件提交到 NativeActivity
final class NativePostImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {}

职责链番外篇 ——职责链模板之拼装职责链并执行事件处理

ViewRootImpl中已经定义了很多Stage职责,那么这些职责由是在什么时候被拼装成链式调用的呢?

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks {

    InputStage mFirstInputStage;
    InputStage mFirstPostImeInputStage;
    InputStage mSyntheticInputStage;
    // ... 省略无关的代码
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                // ... 省略无关的代码
                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;

    }

}

最终构造成如下的职责链:mSyntheticInputStage --> viewPostImeStage --> nativePostImeStage --> earlyPostImeStage --> imeStage --> viewPreImeStage --> viewPreImeStage


Stage对象的详解

SyntheticInputStage

综合性的事件处理阶段,该类主要轨迹球、操作杆、导航面板及未捕获的事件使用键盘进行处理。

    final class SyntheticInputStage extends InputStage {
        @Override
        protected int onProcess(QueuedInputEvent q) {
            q.mFlags |= QueuedInputEvent.FLAG_RESYNTHESIZED;
            if (q.mEvent instanceof MotionEvent) {
                final MotionEvent event = (MotionEvent) q.mEvent;
                final int source = event.getSource();
                if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    // 轨迹球
                    mTrackball.process(event);
                    return FINISH_HANDLED;
                } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
                    // 操作杆
                    mJoystick.process(event);
                    return FINISH_HANDLED;
                } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION)
                        == InputDevice.SOURCE_TOUCH_NAVIGATION) {
                    // 导航面板
                    mTouchNavigation.process(event);
                    return FINISH_HANDLED;
                }
            } else if ((q.mFlags & QueuedInputEvent.FLAG_UNHANDLED) != 0) {
                // 未捕获的事件,交由键盘去处理
                mKeyboard.process((KeyEvent) q.mEvent);
                return FINISH_HANDLED;
            }
            // 继续转发事件
            return FORWARD;
        }
}

ViewPostImeInputStage

视图输入处理阶段,主要处理按键、轨迹球、手指触摸及一般性的运动事件,触摸事件的分发对象是View。

processPointerEvent方法是对触摸事件的预处理,在View执行拖拽时将会使用到预处理后的值。

final class ViewPostImeInputStage extends InputStage {
        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                // 处理按键事件
                return processKeyEvent(q);
            } else {
                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);
                }
            }
        }

        private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;

            mAttachInfo.mUnbufferedDispatchRequested = false;
            boolean handled = mView.dispatchPointerEvent(event);
            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                mUnbufferedInputDispatch = true;
                if (mConsumeBatchedInputScheduled) {
                    scheduleConsumeBatchedInputImmediately();
                }
            }
            return handled ? FINISH_HANDLED : FORWARD;
        }
}

所以经由processPointerEvent分析可知,你对屏幕上的某个按钮的点击事件,将总是先调用按钮的dispatchTouchEvent 的方法。

先提出上面这句论断,但也别忽视上面论断中的几个待确认的疑点:

  • mView是什么(是根视图?还是焦点触发的视图)?
  • dispatchPointerEvent是如何派发事件的?

这些疑点我们另开一篇文章进行描述,这里先不打断原有的流程,我们接着继续分析。

NativePostImeInputStage

本地方法处理阶段,则构建可延迟的重用队列,此时执行操作将会异步回调结果。

final class NativePostImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (mInputQueue != null) {
                mInputQueue.sendInputEvent(q.mEvent, q, false, this);
                return DEFER;
            }
            return FORWARD;
        }
}

EarlyPostImeInputStage

输入法早期处理阶段。

final class EarlyPostImeInputStage extends InputStage {

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                final int source = q.mEvent.getSource();
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                }
            }
            return FORWARD;
        }

private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent) q.mEvent;

            // Translate the pointer event for compatibility, if needed.
            if (mTranslator != null) {
                mTranslator.translateEventInScreenToAppWindow(event);
            }

            // Enter touch mode on down or scroll.
            final int action = event.getAction();
            if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
                ensureTouchMode(true);
            }

            // Offset the scroll position.
            if (mCurScrollY != 0) {
                event.offsetLocation(0, mCurScrollY);
            }

            // Remember the touch position for possible drag-initiation.
            if (event.isTouchEvent()) {
                mLastTouchPoint.x = event.getRawX();
                mLastTouchPoint.y = event.getRawY();
                mLastTouchSource = event.getSource();
            }
            return FORWARD;
        }
}

ImeInputStage

输入法事件处理阶段,处理一些输入法字符等。如果对输入的内容无法识别,则继续往下转发。

final class ImeInputStage extends AsyncInputStage
            implements InputMethodManager.FinishedInputEventCallback {

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (mLastWasImTarget && !isInLocalFocusMode()) {
                InputMethodManager imm = InputMethodManager.peekInstance();
                if (imm != null) {
                    final InputEvent event = q.mEvent;
                    int result = imm.dispatchInputEvent(event, q, this, mHandler);
                    if (result == InputMethodManager.DISPATCH_HANDLED) {
                        return FINISH_HANDLED;
                    } else if (result == InputMethodManager.DISPATCH_NOT_HANDLED) {
                        // IME 无法处理,则往下转发
                        return FORWARD;
                    } else {
                         // 会执行异步回调
                        return DEFER;
                    }
                }
            }
            return FORWARD;
        }
}

ViewPreImeInputStage

视图预处理输入法事件阶段,将输入法的事件派发到视图的树。

final class ViewPreImeInputStage extends InputStage {

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            }
            return FORWARD;
        }

        private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent) q.mEvent;
            if (mView.dispatchKeyEventPreIme(event)) {
                return FINISH_HANDLED;
            }
            return FORWARD;
        }
    }
}

NativePreImeInputStage

本地方法预处理输入法事件阶段,可用于实现类似adb 输入的功能。

final class NativePreImeInputStage extends AsyncInputStage
            implements InputQueue.FinishedInputEventCallback {
        public NativePreImeInputStage(InputStage next, String traceCounter) {
            super(next, traceCounter);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (mInputQueue != null && q.mEvent instanceof KeyEvent) {
                mInputQueue.sendInputEvent(q.mEvent, q, true, this);
                return DEFER;
            }
            return FORWARD;
        }

        @Override
        public void onFinishedInputEvent(Object token, boolean handled) {
            QueuedInputEvent q = (QueuedInputEvent) token;
            if (handled) {
                finish(q, true);
                return;
            }
            forward(q);
        }
    }
}

经过{综合性处理阶段}{视图处理阶段}{本地处理阶段},接着从{早起输入法处理阶段}{输入法阶段}输入法视图预处理阶段最后到{输入法本地预处理阶段}

当然按照正常情况肯定是在职责链靠前的阶段,被处理的机会越大。(这不是废话么:-P)。我们最关心的视图的触摸事件的派发的阶段就在{视图处理阶段}中。

结束语

下一篇文章将会分析,如何将 inputEvent 事件触达到 dispatchTouchEvent 。
View·从 InputEvent 到 dispatchTouchEvent 源码分析(二)


参考资料:

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • ViewRootImpl & ViewGroup & View 触摸事件派发机制源码分析 Android 6.0 ...
    Nvsleep阅读 2,656评论 3 9
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,871评论 4 26
  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 54,731评论 51 596
  • 小镇。 正午的艳阳是一种狂野的肆略。 颤抖的手中,小小的雪白的馒头在阳光下的美丽颜色,让肚子里的馋虫惊喜得几乎要顺...
    燕来小记阅读 521评论 0 1