最近研究了下Android 的事件传递机制,觉得有必要记录下,以免忘记。   站在app研发的角度来看android ,开发者主要做俩个事。   1是渲染希望用户看到的界面;   2是响应用户的操作。可见,事件传递机制是多么的重要。   我们常见的android事件传递机制主要有motionEvent和keyEvent,下面我们直接分析俩个事件   的源码。分析源码的过程中也看了很多大牛的博客分析源码,结合自己的demo测试,记录下。   先分析touchEvent。Android 的事件首先会被系统封装成motionEvent和keyEvent,由系统服务InputManagerService通过WMS找到要接收消息的window,然后将消息分发到该window,android window只是个抽象的概念,真正承载显示的是我们常见的DecorView,那么我们从DecorView来开始分析。

motionEvent分析

DecorView.java    public boolean dispatchTouchEvent(MotionEvent ev) {        final Window.Callback cb = mWindow.getCallback();        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);    }

Window.Callback cb就是Activity,也就是说说首先会回调到activity的dispatchTouchEvent。

Activity.Javapublic boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }

getWindow().superDispatchTouchEvent(ev)由回调到了DecorView

public boolean superDispatchTouchEvent(MotionEvent event) {        return super.dispatchTouchEvent(event);    }

这里调用了ViewGroup的dispatchTouchEvent。为什么要这么绕呢?看activity的
dispatchTouchEvent的注释,You can override this to intercept all touch screen events

 /**     * Called to process touch screen events.  You can override this to     * intercept all touch screen events before they are dispatched to the     * window.  Be sure to call this implementation for touch screen events     * that should be handled normally.     *     * @param ev The touch screen event.     *     * @return boolean Return true if this event was consumed.     */

重点来了,看viewGroup是如何分发motionEvent的。

 @Override    public boolean dispatchTouchEvent(MotionEvent ev){        boolean handled = false;        if (onFilterTouchEventForSecurity(ev)) {            final int action = ev.getAction();            final int actionMasked = action & MotionEvent.ACTION_MASK;//motionEvent的一系列消息以ACTION_DOWN为起点,因此要先清空各种标志位,//保证是一个全新的事件。            // Handle an initial down.            if (actionMasked == MotionEvent.ACTION_DOWN) {                // Throw away all previous state when starting a new touch gesture.                // The framework may have dropped the up or cancel event for the previous gesture                // due to an app switch, ANR, or some other state change.                cancelAndClearTouchTargets(ev);                resetTouchState();            }//是否拦截呢?只有在MotionEvent.ACTION_DOWN和mFirstTouchTarget     //!= null(表示该viewGroup还没有找到可以消耗consume这一系列事件的view)            // Check for interception.            final boolean intercepted;            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {  //调用viewGroup是否进行拦截该事件。可以重写进行定制。                    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;            }            // If intercepted, start normal event dispatch. Also if there is already            // a view that is handling the gesture, do normal event dispatch.            if (intercepted || mFirstTouchTarget != null) {                ev.setTargetAccessibilityFocus(false);            }            // Check for cancelation.            final boolean canceled = resetCancelNextUpFlag(this)                    || actionMasked == MotionEvent.ACTION_CANCEL;            // Update list of touch targets for pointer down, if needed.            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;            TouchTarget newTouchTarget = null;            boolean alreadyDispatchedToNewTouchTarget = false;//重点来了,不拦截的时候就去找子view是否能消耗事件。            if (!canceled && !intercepted) {                // If the event is targeting accessibility focus we give it to the                // view that has accessibility focus and if it does not handle it                // we clear the flag and dispatch the event to all children as usual.                // We are looking up the accessibility focused host to avoid keeping                // state since these events are very rare.                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()                        ? findChildWithAccessibilityFocus() : null;                if (actionMasked == MotionEvent.ACTION_DOWN                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                    final int actionIndex = ev.getActionIndex(); // always 0 for down                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)                            : TouchTarget.ALL_POINTER_IDS;                    // Clean up earlier touch targets for this pointer id in case they                    // have become out of sync.                    removePointersFromTouchTargets(idBitsToAssign);                    final int childrenCount = mChildrenCount;                    if (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);                            // If there is a view that has accessibility focus we want it                            // to get the event first and if not handled we will perform a                            // normal dispatch. We may do a double iteration but this is                            // safer given the timeframe.                            if (childWithAccessibilityFocus != null) {                                if (childWithAccessibilityFocus != child) {                                    continue;                                }                                childWithAccessibilityFocus = null;                                i = childrenCount - 1;                            }//子view满足接收该消息的条件,比如按下的位置在该子view的区域内。                            if (!child.canReceivePointerEvents()                                    || !isTransformedTouchPointInView(x, y, child, null)) {                                ev.setTargetAccessibilityFocus(false);                                continue;                            }                            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 is handling.                                newTouchTarget.pointerIdBits |= idBitsToAssign;                                break;                            }                            resetCancelNextUpFlag(child);                            //重中之重,会调用子view的dispatchTouchEvent,看子view是否消耗事件,如果消耗事件则表示找到了处理事件的对象,如果没有子view则自己处理TouchEvent.                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                // Child wants to receive touch within its bounds.                                mLastTouchDownTime = ev.getDownTime();                                if (preorderedList != null) {                                    // childIndex points into presorted list, find original index                                    for (int j = 0; j < childrenCount; j++) {                                        if (children[childIndex] == mChildren[j]) {                                            mLastTouchDownIndex = j;                                            break;                                        }                                    }                                } else {                                    mLastTouchDownIndex = childIndex;                                }                                mLastTouchDownX = ev.getX();                                mLastTouchDownY = ev.getY();                                //重要,整个的事件机制都与整个有关。该方法会设置ViewGroup的对象mFirstTouchTarget为消耗事件的view,整个是事件传递的机制都是要判断这个对象是否存在来分发给该对象的。                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }                            // The accessibility focus didn't handle the event, so clear                            // the flag and do a normal dispatch to all children.                            ev.setTargetAccessibilityFocus(false);                        }                        if (preorderedList != null) preorderedList.clear();                    }                    if (newTouchTarget == null && mFirstTouchTarget != null) {                        // Did not find a child to receive the event.                        // Assign the pointer to the least recently added target.                        newTouchTarget = mFirstTouchTarget;                        while (newTouchTarget.next != null) {                            newTouchTarget = newTouchTarget.next;                        }                        newTouchTarget.pointerIdBits |= idBitsToAssign;                    }                }            }//重要,发现没有找到消耗事件的子view,那只能自己上去处理了。            // Dispatch to touch targets.            if (mFirstTouchTarget == null) {                // No touch targets so treat this as an ordinary view.                handled = 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 if necessary.                          TouchTarget predecessor = null;                TouchTarget target = mFirstTouchTarget;                while (target != null) {                    final TouchTarget next = target.next;                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                        handled = true;                    } else {                        final boolean cancelChild = resetCancelNextUpFlag(target.child)                                || intercepted;//调用已经找到的子view去处理接下来的事件。                        if (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = true;                        }                        if (cancelChild) {                            if (predecessor == null) {                                mFirstTouchTarget = next;                            } else {                                predecessor.next = next;                            }                            target.recycle();                            target = next;                            continue;                        }                    }                    predecessor = target;                    target = next;                }            }            // Update list of touch targets for pointer up or cancel, if needed.            if (canceled                    || actionMasked == MotionEvent.ACTION_UP                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                resetTouchState();            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {                final int actionIndex = ev.getActionIndex();                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);                removePointersFromTouchTargets(idBitsToRemove);            }        }        if (!handled && mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);        }        return handled;    }

在来看下View的dispatchTouchEvent.

  public boolean dispatchTouchEvent(MotionEvent event) {        // If the event should be handled by accessibility focus first.        if (event.isTargetAccessibilityFocus()) {            // We don't have focus or no virtual descendant has it, do not handle the event.            if (!isAccessibilityFocusedViewOrHost()) {                return false;            }            // We have focus and got the event, then use normal event dispatch.            event.setTargetAccessibilityFocus(false);        }        boolean result = false;        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        final int actionMasked = event.getActionMasked();        if (actionMasked == MotionEvent.ACTION_DOWN) {            // Defensive cleanup for new gesture            stopNestedScroll();        }        if (onFilterTouchEventForSecurity(event)) {            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {                result = true;            }            //noinspection SimplifiableIfStatement            //重点,有设置的mOnTouchListener的会先调用            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }//重点,调用onTouchEvent            if (!result && onTouchEvent(event)) {                result = true;            }        }        if (!result && mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        // Clean up after nested scrolls if this is the end of a gesture;        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest        // of the gesture.        if (actionMasked == MotionEvent.ACTION_UP ||                actionMasked == MotionEvent.ACTION_CANCEL ||                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {            stopNestedScroll();        }        return result;    }

源代码就这么多,用通俗的语言讲:接到一个项目,这个项目要30天完成,公司接口人把这个项目分给了部门老大,老大说先给公司Boss汇报下,于是有的acticvity的回调,boss说干吧,小伙子,于是部门老大开始分活了,分给谁好呢?部门有张三、李四、王五三个项目组,部门领导我要不要自己干呢?太累了(不拦截),张三适合干啊(因为按下的位置落在了张三的位置),于是说张三,你干吧,张三想,我干不干呢,我好歹是个头头,怎么能自己干呢?于是遍历了自己的几个手下,发现小胡可以干(在这个按下的位置区域内),就安排给了小胡,小胡没有手下啊,因为小胡是个view,咋整,官大一级压死人啊,自己干了,这时候,小胡就告诉张三,我干了,张三说好的,你们记录在我的mFirstTouchTarget,这个项目今后你来负责,然后就去找部门老大汇报,老大说好的,我的mFirstTouchTarget就存你张三了啊,这个项目你来负责,于是乎,第二天、第三天、。。。的任务直接发给了小胡(对用action_move和action_up事件)。

那么问题来了,如果小胡是个拆二代,他不干呢,他很任性,于是乎张三就没办法了,只能自己干了,这样,张三的mFirstTouchTarget 就是Null,这个项目的后面的工作都直接发给了他。

那么,问题来了,如果项目到第5天的时候部门领导拦截了会发生什么呢?直接上demo看下。

public class MyLayout extends ConstraintLayout {    public MyLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        Log.d("haha1","dispatchTouchEvent " + ev.getAction());        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.d("haha1","onInterceptTouchEvent " + ev.getAction());        int action = ev.getAction();        if(action != MotionEvent.ACTION_DOWN){            return true;        }        /*return super.onInterceptTouchEvent(ev);*/        return false;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.d("haha1","onTouchEvent " + event.getAction());        return false;    }}public class MyButtom extends androidx.appcompat.widget.AppCompatButton {    public MyButtom(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        Log.d("haha","dispatchTouchEvent " + event.getAction());        return super.dispatchTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.d("haha","onTouchEvent " + event.getAction());        return true;    }}点击button后看日志2020-07-07 17:43:24.305 11524-11524/com.example.myapplication D/haha1: dispatchTouchEvent 02020-07-07 17:43:24.305 11524-11524/com.example.myapplication D/haha1: onInterceptTouchEvent 02020-07-07 17:43:24.305 11524-11524/com.example.myapplication D/haha: dispatchTouchEvent 02020-07-07 17:43:24.305 11524-11524/com.example.myapplication D/haha: onTouchEvent 02020-07-07 17:43:24.329 11524-11524/com.example.myapplication D/haha1: dispatchTouchEvent 22020-07-07 17:43:24.329 11524-11524/com.example.myapplication D/haha1: onInterceptTouchEvent 22020-07-07 17:43:24.329 11524-11524/com.example.myapplication D/haha: dispatchTouchEvent 32020-07-07 17:43:24.329 11524-11524/com.example.myapplication D/haha: onTouchEvent 32020-07-07 17:43:24.385 11524-11524/com.example.myapplication D/haha1: dispatchTouchEvent 12020-07-07 17:43:24.385 11524-11524/com.example.myapplication D/haha1: onTouchEvent 1

what?3是什么鬼,看代码发现是motion_cancel,怎么回事呢,看代码

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                        handled = true;                    } else {                    //这里intercepted 为true导致cancelChild = true                        final boolean cancelChild = resetCancelNextUpFlag(target.child)                                || intercepted;                        if (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = true;                        } 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();        //cancel为true, event.setAction(MotionEvent.ACTION_CANCEL);        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {            event.setAction(MotionEvent.ACTION_CANCEL);            if (child == null) {                handled = super.dispatchTouchEvent(event);            } else {                handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            return handled;        }

豁然开朗,原来是这个原因。设置cancel后应该重置了mFirstTouchTarget为Null(猜的),才有了上面日志的结果。OK,motionEvent分析完了,来看keyEvent.

同理从decorView开始。

 @Override    public boolean dispatchKeyEvent(KeyEvent event) {        final int keyCode = event.getKeyCode();        final int action = event.getAction();        final boolean isDown = action == KeyEvent.ACTION_DOWN;        if (isDown && (event.getRepeatCount() == 0)) {            // First handle chording of panel key: if a panel key is held            // but not released, try to execute a shortcut in it.            if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {                boolean handled = dispatchKeyShortcutEvent(event);                if (handled) {                    return true;                }            }            // If a panel is open, perform a shortcut on it without the            // chorded panel key            if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {                if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {                    return true;                }            }        }        if (!mWindow.isDestroyed()) {        //调用activity的dispatchKeyEvent,原因同motionevent            final Window.Callback cb = mWindow.getCallback();            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)                    : super.dispatchKeyEvent(event);            if (handled) {                return true;            }        }        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);    }
Activity.java public boolean dispatchKeyEvent(KeyEvent event) {        onUserInteraction();        // Let action bars open menus in response to the menu key prioritized over        // the window handling it        final int keyCode = event.getKeyCode();        if (keyCode == KeyEvent.KEYCODE_MENU &&                mActionBar != null && mActionBar.onMenuKeyEvent(event)) {            return true;        }        Window win = getWindow();        //又反调decorview.superDispatchKeyEvent.        if (win.superDispatchKeyEvent(event)) {            return true;        }        View decor = mDecor;        if (decor == null) decor = win.getDecorView();        return event.dispatch(this, decor != null                ? decor.getKeyDispatcherState() : null, this);    } public boolean superDispatchKeyEvent(KeyEvent event) {        // Give priority to closing action modes if applicable.        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {            final int action = event.getAction();            // Back cancels action modes first.            if (mPrimaryActionMode != null) {                if (action == KeyEvent.ACTION_UP) {                    mPrimaryActionMode.finish();                }                return true;            }        }//调用viewGroup的dispatchKeyEvent        if (super.dispatchKeyEvent(event)) {            return true;        }        return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);    }

来看ViewGroup的dispatchKeyEvent。

@Override    public boolean dispatchKeyEvent(KeyEvent event) {        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onKeyEvent(event, 1);        }        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {                //调用view的dispatchKeyEvent            if (super.dispatchKeyEvent(event)) {                return true;            }                 } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)                == PFLAG_HAS_BOUNDS) {                //调用的focusView的dispatchKeyEvent            if (mFocused.dispatchKeyEvent(event)) {                return true;            }        }        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);        }        return false;    }

来看view的dispatchKeyEvent。

 public boolean dispatchKeyEvent(KeyEvent event) {        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onKeyEvent(event, 0);        }        // Give any attached key listener a first crack at the event.        //noinspection SimplifiableIfStatement        ListenerInfo li = mListenerInfo;        //有设置的mOnKeyListener会调用        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {            return true;        }//调用keyevent的dispatch        if (event.dispatch(this, mAttachInfo != null                ? mAttachInfo.mKeyDispatchState : null, this)) {            return true;        }        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        return false;    }

来看keyEvent的dispatch。

 public final boolean dispatch(Callback receiver, DispatcherState state,            Object target) {        switch (mAction) {            case ACTION_DOWN: {                mFlags &= ~FLAG_START_TRACKING;                if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state                        + ": " + this);                 //我们常见的onKeyDown                boolean res = receiver.onKeyDown(mKeyCode, this);                if (state != null) {                    if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {                        if (DEBUG) Log.v(TAG, "  Start tracking!");                        state.startTracking(this, target);                    } else if (isLongPress() && state.isTracking(this)) {                        try {                            if (receiver.onKeyLongPress(mKeyCode, this)) {                                if (DEBUG) Log.v(TAG, "  Clear from long press!");                                state.performedLongPress(this);                                res = true;                            }                        } catch (AbstractMethodError e) {                        }                    }                }                return res;            }            case ACTION_UP:                if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state                        + ": " + this);                if (state != null) {                    state.handleUpEvent(this);                }                //我们常见的onKeyUp                return receiver.onKeyUp(mKeyCode, this);            case ACTION_MULTIPLE:                final int count = mRepeatCount;                final int code = mKeyCode;                if (receiver.onKeyMultiple(code, count, this)) {                    return true;                }                if (code != KeyEvent.KEYCODE_UNKNOWN) {                    mAction = ACTION_DOWN;                    mRepeatCount = 0;                    boolean handled = receiver.onKeyDown(code, this);                    if (handled) {                        mAction = ACTION_UP;                        receiver.onKeyUp(code, this);                    }                    mAction = ACTION_MULTIPLE;                    mRepeatCount = count;                    return handled;                }                return false;        }        return false;    }

看view 的onkeyUp

public boolean onKeyUp(int keyCode, KeyEvent event) {        if (KeyEvent.isConfirmKey(keyCode)) {            if ((mViewFlags & ENABLED_MASK) == DISABLED) {                return true;            }            if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {                setPressed(false);                if (!mHasPerformedLongPress) {                    // This is a tap, so remove the longpress check                    removeLongPressCallback();                    if (!event.isCanceled()) {                    //常见的设置onClickListener.                        return performClickInternal();                    }                }            }        }        return false;    }

KeyEvent的消息传递比较简单,会将keyEvent分发给当前焦点view,如果当前焦点view消耗了keyevent,则keyEvent没有消耗,则最终调用actiivity中的onkeyDown和onKeyUp的默认实现。还有,按键过程中系统会根据按键的上下左右做相应的视图滑动,代码可以参考其他文章。

记录下今天看的源码,做一下总结,可能有错的地方。暂时先这样了。

更多相关文章

  1. 没有一行代码,「2020 新冠肺炎记忆」这个项目却登上了 GitHub 中
  2. 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
  3. 浅谈android view事件分发机制
  4. [置顶] 那些你应该知道却不一定知道的——View坐标分析汇总
  5. 最近排查android webview https的发热耗电和加载速度慢问题解决
  6. Android构建01-前言
  7. Flutter for Android开发详细配置开创建项目
  8. Android学习11--事件处理
  9. 第二篇:实现uni-app和原生(Android)以及H5项目混编

随机推荐

  1. Android之UI学习篇九:使用TabHost实现卡片
  2. android 高级之旅 (十七)FFmpeg移植android
  3. 定时任务——Android之Alarm机制讲解
  4. 从Android到React Native开发(三、自定义
  5. Android中不用Service跨Avtivity仍然可以
  6. 阿里Android开发规范:安全与其他
  7. 学习入门-寻宝篇-android开发者官网
  8. Android(安卓)获取状态栏和标题栏的高度
  9. android顶部导航栏的封装
  10. Android:实现TabWidget选项卡按钮在屏幕下