源码解析Android中的事件处理
一,事件处理
Android提供了两套事件处理机制:
1.基于监听的事件处理。
2.基于回调的事件处理。
基于回调的事件处理用于处理一些具有通用性的事件,基于监听的事件处理用于处理与具体业务相关的事件。
基于监听的事件处理
基于监听的事件处理是在指定view组件上绑定指定的监听器。比如点击事件:
可以以匿名内部类形式绑定监听器:
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { } });
或者:
// Create an anonymous implementation of OnClickListener private OnClickListener mCorkyListener = new OnClickListener() { public void onClick(View v) { // do something when the button is clicked } }; protected void onCreate(Bundle savedValues) { ... // Capture our button from layout Button button = (Button)findViewById(R.id.corky); // Register the onClick listener with the implementation above button.setOnClickListener(mCorkyListener); ... }
还可以将 OnClickListener 作为 Activity 的一部分来实现更为方便。这样可以避免加载额外的类和分配对象:
public class ExampleActivity extends Activity implements OnClickListener { protected void onCreate(Bundle savedValues) { ... Button button = (Button)findViewById(R.id.corky); button.setOnClickListener(this); } // Implement the OnClickListener callback public void onClick(View v) { // do something when the button is clicked } ... }
view类声明监听器和执行点击事件中:
//监听器 public interface OnClickListener { void onClick(View v); }//注册监听器 方法 public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }//声明监听器static class ListenerInfo { public OnClickListener mOnClickListener;}//执行监听动作public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this);//调用监听器里的方法 result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
基于回调的事件处理
基于回调的事件处理就是自定义UI组件或者Activity重写事件方法,比如View类和Activity的父类都实现了KeyEvent.Callback接口中的一系列回调函数,因此在自定义View或Activity中可以通过重写相关方法来实现特定功能:
KeyEvent.Callback接口:
public interface Callback { //该返回值用于标识该处理函数是否能完全处理该事件 // 返回true,表明该函数已完全处理该事件,该事件不会传播出去 // 返回false,表明该函数未完全处理该事件,该事件会传播出去 boolean onKeyDown(int keyCode, KeyEvent event); boolean onKeyLongPress(int keyCode, KeyEvent event); boolean onKeyUp(int keyCode, KeyEvent event); boolean onKeyMultiple(int keyCode, int count, KeyEvent event); }
重写相关方法:
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { return super.onKeyDown(keyCode, event); }
二,相关类概述
关于事件,android中有一个InputEvent类,它是针对所有事件的共有特点提取出来的一个统一的抽象类,此类结构简单,可以参看InputEvent .java 。其有两个子类:MotionEvent(位移事件)和KeyEvent(按键事件)。这两个类都定义了大量的常量,弄明白这些常量对读懂这两个类至关重要。还有InputDevice类,这个类包含关于输入设备定义的常量和其他一些常量。前面两个类中的常量几乎都是此类的输入设备的相关配置。
InputDevice类
输入设备:
public static final int SOURCE_CLASS_MASK = 0x000000ff;public static final int SOURCE_CLASS_NONE = 0x00000000;public static final int SOURCE_CLASS_BUTTON = 0x00000001;public static final int SOURCE_CLASS_POINTER = 0x00000002;public static final int SOURCE_CLASS_TRACKBALL = 0x00000004;public static final int SOURCE_CLASS_POSITION = 0x00000008;public static final int SOURCE_CLASS_JOYSTICK = 0x00000010;public static final int SOURCE_UNKNOWN = 0x00000000;public static final int SOURCE_KEYBOARD = 0x00000100 | SOURCE_CLASS_BUTTON;public static final int SOURCE_DPAD = 0x00000200 | SOURCE_CLASS_BUTTON;public static final int SOURCE_GAMEPAD = 0x00000400 | SOURCE_CLASS_BUTTON;public static final int SOURCE_TOUCHSCREEN = 0x00001000 | SOURCE_CLASS_POINTER;public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER;public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER;public static final int SOURCE_BLUETOOTH_STYLUS = 0x00008000 | SOURCE_STYLUS;public static final int SOURCE_TRACKBALL = 0x00010000 | SOURCE_CLASS_TRACKBALL;public static final int SOURCE_TOUCHPAD = 0x00100000 | SOURCE_CLASS_POSITION;public static final int SOURCE_TOUCH_NAVIGATION = 0x00200000 | SOURCE_CLASS_NONE;public static final int SOURCE_ROTARY_ENCODER = 0x00400000 | SOURCE_CLASS_NONE;public static final int SOURCE_JOYSTICK = 0x01000000 | SOURCE_CLASS_JOYSTICK;public static final int SOURCE_HDMI = 0x02000000 | SOURCE_CLASS_BUTTON;public static final int SOURCE_ANY = 0xffffff00;
MotionEvent类
当用户触摸屏幕时将创建一个MotionEvent对象。MotionEvent包含关于发生触摸的位置和时间等细节信息。MotionEvent对象被传递到程序中合适的方法比如View对象的onTouchEvent()方法中。在这些方法中我们可以分析MotionEvent对象那个,以决定要执行的操作。
MotionEvent对象是与用户触摸相关的时间序列,该序列从用户首次触摸屏幕开始,经历手指在屏幕表面的任何移动,直到手指离开屏幕时结束。手指的初次触摸(ACTION_DOWN操作),滑动(ACTION_MOVE操作)和松开(ACTION_UP)都会创建MotionEvent对象。移动过程中会产生大量事件,每个事件都会产生对应的MotionEvent对象记录发生的操作,触摸的位置,使用的多大压力,触摸的面积,合适发生,以及最初的ACTION_DOWN和时发生等相关的信息。
action:触摸:
/** * 按下 */ public static final int ACTION_DOWN = 0; /** * 松开 */ public static final int ACTION_UP = 1; /** * 移动 */ public static final int ACTION_MOVE = 2; /** * 触摸动作取消 */ public static final int ACTION_CANCEL = 3; /** * 触摸动作超出边界 */ public static final int ACTION_OUTSIDE = 4; /** * 多点触摸按下动作 */ public static final int ACTION_POINTER_DOWN = 5; /** * 多点离开动作 */ public static final int ACTION_POINTER_UP = 6;
action:鼠标:
/** * 鼠标在view上移动 */ public static final int ACTION_HOVER_MOVE = 7; /** * 滚动 */ public static final int ACTION_SCROLL = 8; /** * 鼠标进入view */ public static final int ACTION_HOVER_ENTER = 9; /** * 鼠标离开view */ public static final int ACTION_HOVER_EXIT = 10; /** * 鼠标按住按键 */ public static final int ACTION_BUTTON_PRESS = 11; /** * 鼠标释放按键 */ public static final int ACTION_BUTTON_RELEASE = 12;
action:特殊:这几个比较难以理解,建议阅读
android触控,先了解MotionEvent(一)
/** * 动作位掩码 */ public static final int ACTION_MASK = 0xff;/** * 指针位掩码 */ public static final int ACTION_POINTER_INDEX_MASK = 0xff00; /** * */ public static final int ACTION_POINTER_INDEX_SHIFT = 8;
边缘:
/** * 上边 */ public static final int EDGE_TOP = 0x00000001;/** * 下边 */ public static final int EDGE_BOTTOM = 0x00000002;/** * 左边 */ public static final int EDGE_LEFT = 0x00000004; /** * 右边 */ public static final int EDGE_RIGHT = 0x00000008;
触摸屏幕相关的常量:AXIS_打头
BUTTON鼠标按键:
/** * 左键 */ public static final int BUTTON_PRIMARY = 1 << 0;/** * 右键 */ public static final int BUTTON_SECONDARY = 1 << 1;/** * 中间键 */ public static final int BUTTON_TERTIARY = 1 << 2;/** * 后退键 */ public static final int BUTTON_BACK = 1 << 3;/** * 前进键 */ public static final int BUTTON_FORWARD = 1 << 4;/** * */ public static final int BUTTON_STYLUS_PRIMARY = 1 << 5;/** * */ public static final int BUTTON_STYLUS_SECONDARY = 1 << 6;
TOOL触摸的工具:
/** * 未知 */ public static final int TOOL_TYPE_UNKNOWN = 0;/** * 手指 */ public static final int TOOL_TYPE_FINGER = 1;/** * 手写笔 */ public static final int TOOL_TYPE_STYLUS = 2;/** * 鼠标 */ public static final int TOOL_TYPE_MOUSE = 3;/** * 橡皮擦 */ public static final int TOOL_TYPE_ERASER = 4;
类中的方法大多方法是对触摸对象属性的获取,大都是对jni层的封装。
并且类中给出触摸事件的判断方法:
public final boolean isTouchEvent() { return nativeIsTouchEvent(mNativePtr); }
值得注意的是此类只是对触摸事件的定义,没有触摸事件的分发方法和相关监听器接口。
KeyEvent类
类中定义的key常量都是以KEYCODE_开头,定义了android系统所有的各种按键。包括手机键盘按键,类似电脑键盘的功能键(KEYCODE_F+数字),小键盘键(KEYCODE_NUMPAD_),字母键,符号键等几乎所有的按键,多媒体键(KEYCODE_MEDIA_),游戏手柄按键(KEYCODE_BUTTON_),TV按键(KEYCODE_TV_),还有组合键(比如KEYCODE_ALT_LEFT)等。在方法中并提供了一些方法判断是否是某个或某组按键,比如:boolean isGamepadButton(int keyCode),boolean isShiftPressed()等。
然后定义了按键动作的常量:
//按下 public static final int ACTION_DOWN = 0;//松开 public static final int ACTION_UP = 1;//按多个键 public static final int ACTION_MULTIPLE = 2;
和大量META_开头与FLAG_开头的常量。
KeyEvent类中主要的逻辑方法是KeyEvent的事件分发,其他方法主要是获取按键的属性和判断是否是某个键或某个组里的键。
三,事件处理
事件传递过程:
推荐阅读Android事件处理流程分析和深入理解android内核设计思想12.2 事件投递流程。
由上图可知,事件经一系列处理,最终传到DecorView。DecorView是PhoneWindow类的内部类并且该类继承FrameLayout。PhoneWindow类位于/frameworks/policies/base/phone/com/android/internal/policy/impl/PhoneWindow.java。该类继承于Window类,是Window类的具体实现。
KeyEvent事件处理
PhoneWindow#DecorView#dispatchKeyEvent(KeyEvent event):
@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)) { //首先处理面板上的键,如果按住一个键不松,尝试执行包含它的快捷键 if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) { boolean handled = dispatchKeyShortcutEvent(event);//分发快捷键事件 if (handled) { return true; } } // 如果面板打来,执行一个快捷键,不用按面板上的按键 if ((mPreparedPanel != null) && mPreparedPanel.isOpen) { if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) { return true; } } } //如果按键事件发生时窗口没销毁 if (!isDestroyed()) { final Callback cb = getCallback(); //mFeatureId表示窗口的ID,应用的DecorView为-1 final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)//交给实现Callback的类处理,比如大多情况下都是使用的是Activity。 : super.dispatchKeyEvent(event);//交给父类ViewGroup处理//执行这步的这种情况比较少 if (handled) { return true; } } //此语句很少情况用到,比如开机键用 //如果按键事件发生时窗口已经销毁 //如果是KeyEvent.ACTION_DOWN,执行PhoneWindow.this.onKeyDown //如果不是,执行PhoneWindow.this.onKeyUp //返回isDown return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); }
Activity#dispatchKeyEvent(KeyEvent event)
public boolean dispatchKeyEvent(KeyEvent event) { onUserInteraction(); // 如果是菜单键事件, action bars打开菜单 if (event.getKeyCode() == KeyEvent.KEYCODE_MENU && mActionBar != null && mActionBar.onMenuKeyEvent(event)) { return true; } Window win = getWindow(); if (win.superDispatchKeyEvent(event)) {//事件交给Window处理 return true; } View decor = mDecor; if (decor == null) decor = win.getDecorView(); //如果view层次结构没处理,即win.superDispatchKeyEvent(event)返回是flase //则交给KeyEvent本身的dispatch方法, //Activity的各种回调方法会被触发则交给KeyEvent本身的dispatch方法,Activity的各种回调方法会被触发 return event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this); }
Window是一个抽象类,其中的方法都是抽象方法:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
唯一实现类是 PhoneWindow:
@Override public boolean superDispatchKeyEvent(KeyEvent event) { return mDecor.superDispatchKeyEvent(event); }
PhoneWindow#DecorView#superDispatchKeyEvent(KeyEvent event):
public boolean superDispatchKeyEvent(KeyEvent event) { // 如果按键是后退键. 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处理,并返回处理结果 return super.dispatchKeyEvent(event); }
ViewGroup#dispatchKeyEvent(KeyEvent event):
@Override public boolean dispatchKeyEvent(KeyEvent event) { //keyevent一致性检测用的,可忽略 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 1); } if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) { //如果此ViewGroup是focused或者具体的大小被设置了,交给父类View处理 if (super.dispatchKeyEvent(event)) { return true; } } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) {//mFocused为处于焦点的View //否则交给处于焦点的View处理 if (mFocused.dispatchKeyEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } //如果都没处理返回false return false; }
View的dispatchKeyEvent(KeyEvent event):
public boolean dispatchKeyEvent(KeyEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 0); } // 给出所有的关联的key 监听器, a first crack at the event. //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //调用onKeyListener,如果它非空且view是ENABLED状态,监听器优先触发 if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } //如果上面if语句没执行,则调用KeyEvent.dispatch方法,并将view对象本身作为参数传递进去, //view的各种callback方法在这里被触发 if (event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)) { return true; } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } //还没处理掉返回false return false; }
KeyEvent#dispatch(Callback receiver, DispatcherState state, Object target):
public final boolean dispatch(Callback receiver, DispatcherState state, Object target) { switch (mAction) { case ACTION_DOWN: { //a&=~b的意思就是 a= a & (~b),& ~都是位操作运算符,&是与运算,~是取反运算。 mFlags &= ~FLAG_START_TRACKING;//先清掉START_TRACKING标记 if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state + ": " + this); boolean res = receiver.onKeyDown(mKeyCode, this);//receiver是Callback实例,调用Callback接口的onKeyDown方法,View和Activity都是此接口的实现者 if (state != null) {// 一般都成立 //如果 receiver.onKeyDown返回true了且不是repeated,并且也没有开始tracking if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) { if (DEBUG) Log.v(TAG, " Start tracking!"); state.startTracking(this, target);//则开始tracking当前的KeyEvent和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);//记录此event已经有long press发生了 res = true;//设置为事件已经处理 } } catch (AbstractMethodError e) { } } } return res;//返回down事件处理的结果 } case ACTION_UP: if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state + ": " + this); if (state != null) { state.handleUpEvent(this); } return receiver.onKeyUp(mKeyCode, this);// 最后调用receiver.onKeyUp方法,并返回结果 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; }
KeyEvent.Callback
public interface Callback { boolean onKeyDown(int keyCode, KeyEvent event); boolean onKeyLongPress(int keyCode, KeyEvent event); boolean onKeyUp(int keyCode, KeyEvent event); boolean onKeyMultiple(int keyCode, int count, KeyEvent event); }
Activity和View都实现了KeyEvent.Callback。
方法调用流程图大致可以如下:
TouchEvent事件分发
PhoneWindow#DecorView#dispatchTouchEvent(MotionEvent event):
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); //mFeatureId表示窗口的ID,应用的DecorView为-1 return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)//调用Window.Callback实现类的dispatchTouchEvent(ev)方法,常用的为Activity,另外还用对话框等类 : super.dispatchTouchEvent(ev);//调用DecorView父类dispatchTouchEvent(ev)方法 }
Activity#dispatchTouchEvent(MotionEvent ev):
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //交给Window去处理 if (getWindow().superDispatchTouchEvent(ev)) { return true; } // Window处理失败,退回来自己在onTouchEvent中处理 return onTouchEvent(ev); }
Window是一个抽象类,其中的方法都是抽象方法:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
唯一实现类是 PhoneWindow:
@Override public boolean superDispatchTouchEvent(MotionEvent event) {//PhoneWindow 把事件分发的任务交给DecorView处理 return mDecor.superDispatchTouchEvent(event); }
PhoneWindow#DecorView#superDispatchTouchEvent(MotionEvent event):
public boolean superDispatchTouchEvent(MotionEvent event) {//DecorView又把事件分发的任务交给父类即ViewGroup处理 return super.dispatchTouchEvent(event); }
ViewGroup#dispatchTouchEvent(MotionEvent ev):
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false;//event是否被处理 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; //判断是否是ACTION_DOWN事件 // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) {//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. /* * 在这个函数中最终将mFirstTouchTarget设为null。 * mFirstTouchTarget代表的就是一个事件序列中第一个拦截的对象, * 所以这里需要重置。 */ cancelAndClearTouchTargets(ev);//清除以前的所有状态 /* * 如果事件是ACTION_DOWN, * ViewGroup就会在resetTouchState中重置下面的FLAG_DISALLOW_INTERCEPT标志位。 * 重置的方式是这样的:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; */ resetTouchState();//重置触摸状态 } // 检查interception(拦截). final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /* * 这个if中需要满足两个条件: * (1)actionMasked == MotionEvent.ACTION_DOWN: * 该事件是否为点击下按事件时成立,就是说新的一轮事件到来 * (2)mFirstTouchTarget != null: * 当ViewGroup不拦截事件并将事件交给子元素处理时,成立,mFirstTouchTarget指向这个子元素。 * 而且在ViewGroup中,默认onInterceptTouchEvent返回false,它是不拦截任何事件的, * 但是在LinearLayout中可能就会拦截啊,可以改写啊。 * 而且,当第二个条件成立时,此时发生的事件序列就是ACTION_MOVE或者ACTION_UP,都会进入到这个if语句中。 * */ /* * 所以说呢,当子元素成功拦截了事件或者下按事件发生的时候就会进入if语句。 * 所以说呢,如果子元素没有处理,并且是move和up发生的时候就无法进入该if语句。 * 但为什么这样设定呢,因为如果子元素没有处理的话,事件序列中的其他事件就会直接由ViewGroup来处理了, * 不需要来这里来判断一下到底要不要拦截事件了。那如果是move和up也是同样的,不需要来这里来判断要不要拦截事件。 * */ /* * 也就相当于说,一个事件,第一次因为ACTION_DOWN进入这里,然后ViewGroup判断是否来拦截。 * 之后在子元素成功处理后,因为子元素是可以通过FLAG_DISALLOW_INTERCEPT标志位来干预父元素的事件分发过程,所以又来这里来要看是否拦截。 * */ /* * 为什么总说一旦父元素拦截ACTION_DOWN以后其他的事件序列就只能由父元素来处理呢? * 是因为如果父元素拦截了ACTION_DOWN,那么mFirstTouchTarget == null * 当ACTION_MOVE和ACTION_UP到来的时候,这条if语句就不会进入了, * 然后intercepted = true;表示事件序列由父元素全拦截了。 * */ if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /* * 通常事件传递过程是由外向内的, * 但是通过 requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程, * 不过ACTION_DOWN事件除外。 * 干预表现在子元素已经拦截了事件, * 但是可以通过requestDisallowInterceptTouchEvent来控制 * ACTION_MOVE和ACTION_UP能不能够进入到这里来。 * */ /* * FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截处理ACTION_DOWN以外的其他点击事件了。 * 因为在事件分发时,ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT标志位,表示另一次事件开始。 * */ /* * 子View干涉ViewGroup的过程: * 初始化:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; * 在子View中FLAG_DISALLOW_INTERCEPT被重置,也就是要去干扰, * 然后mGroupFlags & FLAG_DISALLOW_INTERCEPT为1 * 然后(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 为true * 然后disallowIntercept为true * 然后导致if (!disallowIntercept)无法进入。 * */ /* * FLAG_DISALLOW_INTERCEPT标志位有什么用呢? * 当面对滑动冲突时,我们可以考虑用这种方法去解决问题。 * */ final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) {//disallowIntercept为true 如果允许拦截 /* * 所以说onInterceptTouchEvent并不是每次事件都会被调用的。 * 而dispatchTouchEvent却会在每次都调用。 * 对于原始的ViewGroup,onInterceptTouchEvent会返回false, * 但是对于你自己写的LinearLayout,则可以修改这个函数, * 让它对ACTION_DOWN、ACTION_MOVE、ACTION_UP做出不同的选择。 * */ intercepted = onInterceptTouchEvent(ev);//判断是否要真正执行拦截 ev.setAction(action); // restore action in case it was changed } else { intercepted = false;//不需要拦截 } } else {//如果不是down事件并且mFirstTouchTarget为空 // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true;//继续拦截 } // 如果已经拦截intercepted,开始正常的事件分发. // 或者已经有view正在处理这个手势,也正常分发. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // 检查 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; /* * 当ViewGroup不拦截事件的时候,intercepted=false,事件会向下分发由它的子View进行处理 * 所以说一旦ViewGroup拦截了事件,intercepted=true, * 意味着事件序列中的任何事件都不再会传给子元素了,由父元素全权处理。 * 所以intercepted=true一定要谨慎设置。 */ if (!canceled && !intercepted) {//不拦截的情况,它的子对象处理它 // If the event is targeting accessiiblity 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) {//子对象个数不为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 preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; /* * 遍历ViewGroup的所有子元素,判断子元素是否能够接收到点击事件。 */ for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(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; } /* * 判断子元素是否能够接收到点击事件: * (1)canViewReceivePointerEvents:子元素是否可以接受触摸事件。 * (2)isTransformedTouchPointInView:点击事件的坐标是否落在子元素的区域内。 * */ if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } /* * 如果上面那个if语句没有成立,说明这个子元素是可以拦截事件的, * 所以新的TouchTarget出现了,就是这个子元素。 */ 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); /* * 这个子元素已经拦截该事件了,现在要子元素传递给它自己的子元素去分派这个事件了: * dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。 * 下面的第三个参数中child一定不为null,所以child的dispatchTouchEvent一定会被调用。 * 子元素的dispatchTouchEvent返回true, * 意味着dispatchTransformedTouchEvent也返回ture, * 表示事件被子元素分发成功,并break跳出循环。 */ 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(); 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; } } } /* * 有两种情况遍历所有的子元素后事件也没有处理: * (1)ViewGroup根本没有子元素 * (2)子元素的dispatchTouchEvent都返回了false。 * 这种情况下只能ViewGroup自己来处理事件了。 * */ // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. /* * 注意第三个参数:null,在上面变量子元素的时候这里放的是child。 * 如果是null,dispatchTransformedTouchEvent内部就会调用: * super.dispatchTouchEvent(event); * 很显然,这里就转到了View的dispatchTouchEvent(event)方法,即点击事件开始交由View来处理。在View中有onTouchEvent。 * 其实父元素ViewGroup的onTouchEvent就是指的是View中的onTouchEvent方法,它自己这里是没有的。因为ViewGroup是继承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; 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; }
ViewGroup#dispatchTransformedTouchEvent。向子对象分发事件或调用父类View的分发方法处理事件:
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 (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; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. 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 { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. 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); } // Done. transformedEvent.recycle(); return handled; }
Activity#onTouchEvent(MotionEvent event):
/**如果触摸动作没有被任何的view处理,调用它。 * 这是最有效的方式去加工触摸事件,发生在窗口边界之外,没有任何的view处理。 */ public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) {//是否需要关闭窗口 finish(); return true; } return false; }
View#dispatchTouchEvent(MotionEvent event):
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)) { //noinspection SimplifiableIfStatement /** * 首先会判断有没有设置OnTouchListener。 * 如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会调用, * 这样做的好处是方便外界处理点击事件。 */ ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } 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; }
View#onTouchEvent(MotionEvent event):
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX();//获取触摸事件坐标 final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction();//获取触摸事件动作 /* * 当View处于不可用状态下时,View照样会消耗点击事, * 但它并不对事件做出任何的反映 * */ if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } /* * 如果View设置有代理,那么还会执行mTouchDelegate的onTouchEvent方法, * 这个onTouchEvent的工作机制看起来和OnTouchListener类似,这里我们不做研究 * */ if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /* * 这里是对点击事件的具体处理。 * 可以发现的是View的CLICKABLE和LONG_CLICKABLE只要有一个为true, * 那么这个View就消耗这个事件,即onTouchEvent返回ture,不管他是不是DISABLE状态。 * */ if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP://松开 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // 如果我们处于按下状态,只执行采取”单击“操作 if (!focusTaken) { // 使用一个 Runnable and post this 而不是直接调用performClick // 这样做让view其他的visual state在click动作开始之前更新. if (mPerformClick == null) { mPerformClick = new PerformClick();//执行点击 } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN://按下 mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) {//鼠标动作事件 break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // 如果view在滚动容器内, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 不在滚动容器内, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL://取消动作 setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE://移动 drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
当发生触摸事件时,由DecorView判断是交给Activity处理还是直接交给ViewGroup处理,但几乎所有时候是交给Activity处理,Activity会先交给Window处理,在这里Window经常只是起到一个链接的作用,链接到ViewGroup处理,ViewGroup如果拦截事件,调用父类View的方法处理事件,如果不拦截事件,则交给子对象处理。如果子对象不处理事件,ViewGroup也不处理,则最终Activity来处理事件。所以触摸事件经常是要么Activity处理要么ViewGroup或View处理。
触摸事件处理事件方法调用流程可以表示为:
知识点
1,事件的处理顺序?
事件处理逻辑简单概述为:从上到下传递事件,然后从下到上处理事件。分发顺序为Activity-Window-View,从上到下依次传递。如果你最低的那个view onTouchEvent返回false 那就说明他不想处理 那就再往上抛,都不处理的话最终就还是让Activity自己处理了。
举个例子,pm下发一个任务给leader,leader自己不做 给架构师a,小a也不做 给程序员b,b如果做了那就结束了这个任务。b如果发现自己搞不定,那就找a做,a要是也搞不定 就会不断向上发起请求,最终可能还是pm做。
2, onTouch(或OnTouchListener)和onTouchEvent?
如果没有给一个View设置OnTouchListener,也就不存在什么问题了,
但如果有设置了OnTouchListener,那么它里面的 onTouch 方法就会被调用。这两个方法都是在View的dispatchTouchEvent中调用的,OnTouchListener先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。如果 onTouch 方法返回的是 false,则当前View的onTouchEvent 方法才会被调用;
View#dispatchTouchEvent相关源码:
/** * 首先会判断有没有设置OnTouchListener。 * 如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会调用, * 这样做的好处是方便外界处理点击事件。 */ ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } }
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
3,onTouchEvent和onClick(或OnClickListener)?
onClick在View的performClick()被调用,但performClick()早View中有多处地方被调用(在不同的事件的点击操作)。在触摸事件中performClick()只在onTouchEvent中被调用(手指触摸屏幕点击某个view)。
当手指点击某个屏幕时,如果没有onTouch,程序会执行onTouchEvent,如果给view设置了OnClickListener监听,在 onTouchEvent的MotionEvent.ACTION_UP动作中会执行点击操作performClick(),然后performClick()再回调onClick 。所以说onClick是onTouchEvent的一部分。如果触摸的时候,我们只想触发ontouch监听,屏蔽onclick监听的话,就需要在ontouch里面返回true就可以了。
View#onTouchEvent相关源码:
case MotionEvent.ACTION_UP://抬起 ... // // 如果我们处于按下状态,只执行采取”单击“操作 if (!focusTaken) { // 使用一个 Runnable and post this 而不是直接调用performClick // 这样做让view其他的visual state在click动作开始之前更新. if (mPerformClick == null) { mPerformClick = new PerformClick();//执行点击 } if (!post(mPerformClick)) { performClick(); } } } ... break;
View内部类PerformClick:
private final class PerformClick implements Runnable { @Override public void run() { performClick(); } }
View#performClick():
/** * 调用view的OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
4,onTouchEvent和GestureDetector ?
当用户触摸屏幕的时候,会产生许多手势,例如down,up,scroll,filing等等。
一般情况下,我们知道View类有个View.OnTouchListener内部接口,通过重写他的onTouch(View v, MotionEvent event)方法,我们可以处理一些touch事件,但是这个方法太过简单,如果需要处理一些复杂的手势,用这个接口就会很麻烦(因为我们要自己根据用户触摸的轨迹去判断是什么手势)。
Android sdk给我们提供了GestureDetector(Gesture:手势Detector:识别)类,通过这个类我们可以识别很多的手势,主要是通过他的onTouchEvent(event)方法完成了不同手势的识别。虽然他能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。
只有滑动需求的时候 就用前者,如果有双击等比较复杂行为的时候 就用后者。
推荐阅读用户手势检测-GestureDetector使用详解
5,ViewGroup 默认拦截事件吗?
默认的onInterceptTouchEvent返回false,就是不拦截touch事件,直接分发给了子控件:
ViewGroup#onInterceptTouchEvent(MotionEvent ev):
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
所以假如我们自定义了组合控件,譬如image+文字的组合控件,并且在activity里面注册监听期待点击它的时候会产生响应,那么我们就需要重写onInterceptTouchEvent了让它返回true,将事件拦截下来。
6,一旦有事件传递给view,view的onTouchEvent一定会被调用吗?
因为view 本身没有onInterceptTouchEvent方法,所以只要事件来到view这里 就一定会走onTouchEvent方法。
并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale
Button的clickable就是true ,textview是false。
推荐阅读Android View事件机制 21问21答
更多相关文章
- Android热补丁技术—dexposed原理简析(阿里Hao)
- Android(安卓)Architecture LifeCycle
- Android(安卓)Wi-Fi Peer-to-Peer(Android的Wi-Fi P2P对等网络)
- Android音视频学习路线
- Android(安卓)进阶 教你打造 Android(安卓)中的 IOC 框架 【View
- Android面试题(五)—— Android的消息机制
- Android(安卓)调用js,传对象到js里面使用addJavascriptInterface
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用