Android中触摸事件MotionEvent的来源

来源:互联网 发布:网络真人赌博骗局 编辑:程序博客网 时间:2024/05/17 00:58

MotionEvent的来源

在activity中我们经常需要处理触摸事件,要了解android触摸事件event的传递是比较复杂的,今天先看看activity中触摸事件的来源。
首先看Activity的实现,如下,Activity实现了一个特殊的接口:Window.Callback。

public class Activity extends ContextThemeWrapper        implements LayoutInflater.Factory2,        Window.Callback, KeyEvent.Callback,        OnCreateContextMenuListener, ComponentCallbacks2,        Window.OnWindowDismissedCallback {    .......    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }    ........}

那么Window.Callback到底是什么东西呢?它是Window.java里的一个内部类,如下:

 public interface Callback {        /**         * Called to process key events.  At the very least your         * implementation must call         * {@link android.view.Window#superDispatchKeyEvent} to do the         * standard key processing.         *         * @param event The key event.         *         * @return boolean Return true if this event was consumed.         */        public boolean dispatchKeyEvent(KeyEvent event);        /**         * Called to process touch screen events.  At the very least your         * implementation must call         * {@link android.view.Window#superDispatchTouchEvent} to do the         * standard touch screen processing.         *         * @param event The touch screen event.         *         * @return boolean Return true if this event was consumed.         */        public boolean dispatchTouchEvent(MotionEvent event);        .......}

到这里我们可以猜测,如果外界想要传递点击事件给Activity,那么它就必须持有Activity的引用,这没错,我们在Activity的attach方法中,有如下一段:

  final void attach(Context context, ActivityThread aThread,            Instrumentation instr, IBinder token, int ident,            Application application, Intent intent, ActivityInfo info,            CharSequence title, Activity parent, String id,            NonConfigurationInstances lastNonConfigurationInstances,            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {        mWindow = new PhoneWindow(this);        mWindow.setCallback(this);        mUiThread = Thread.currentThread();        mMainThread = aThread;        mInstrumentation = instr;    }

显然,mWindow(PhoneWindow)持有了Activity的引用,它通过setCallback方法来持有Activity,因此,事件是从Window传递给了Activity。当然这里就不得不提到activity的启动过程,简单提一下:
当launcher或我们自己启动一个activity时,调用流程如下:
1). startActivity(Intent) —>
2). Instrumentation.execStartActivity(IApplicationThread caller,Intent intent) —>
3). ActivityManagerService.startActivity(IApplicationThread caller,Intent intent) —>
4). ActivityStackSupervisor.startActivityLocked (IApplicationThread caller,Intent intent) : 这里会根据Intent的信息建立一个 ActivityRecord类保存要启动activity的相关信息

startActivityLocked(IApplicationThread caller,    Intent intent, String resolvedType, ActivityInfo aInfo,){    //生成一个要启动的activity信息    ActivityRecord r = new ActivityRecord(mService,        callerApp, callingUid, callingPackage,        intent, resolvedType, aInfo, container, options);     err = startActivityUncheckedLocked(r, sourceRecord,            voiceSession, voiceInteractor,             startFlags, true, options, inTask);}

5). ActivityStackSupervisor.startActivityUncheckedLocked,这一步生成一个ActivityStack信息,再生成一个TaskRecord信息,并保存到mTaskHistory全局变量里面,后面启动这个activity时会用到

startActivityUncheckedLocked(final ActivityRecord r,.....) {    ActivityStack targetStack;    newTask = true;    //这里会生成一个要启动的activity的ActivityStack    targetStack = computeStackFocus(r, newTask);     r.setTask(targetStack.createTaskRecord(......));    //createTaskRecord生成一个TaskRecord,    //并将它放入到mTaskHistory的第一个位置mTaskHistory.add(0, task);    targetStack.startActivityLocked(r, newTask, doResume,             keepCurTransition, options);}

6)ActivityStack.startActivityLocked (ActivityRecord r, boolean newTask, boolean doResume) —>
7). ActivityStackSupervisor.startSpecificActivityLocked(ActivityRecord r,boolean andResume, boolean checkConfig)
startSpecificActivityLocked代码如下:

 void startSpecificActivityLocked(ActivityRecord r,boolean andResume, boolean checkConfig) {        // Is this activity's application already running?        //mProcessNames map里保存着系统所有运行的app的Process信息        ProcessRecord app = mService.getProcessRecordLocked(r.processName,                r.info.applicationInfo.uid, true);         if (app != null && app.thread != null) {//如果app还在运行,包括还在后台                realStartActivityLocked(r, app, andResume, checkConfig);                return;        }        // app 没有运行,mService = ActivityManagerService        mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,                "activity", r.intent.getComponent(), false, false, true);    }

当启动没有运行的app 时,按下面流程
8). ActivityManagerService.startProcessLocked()

startProcessLocked(ProcessRecord app, String hostingType,String hostingNameStr, String entryPoint,){    ProcessRecord  app = newProcessRecordLocked(info, processName, isolated, isolatedUid); //这里会创建一个ProcessRecord 并保存到mProcessNames map里面    entryPoint = "android.app.ActivityThread"; //这里新app的入口点,zygote fork出新的进程最开始执行的类    Process.ProcessStartResult startResult = Process.start(entryPoint,                    app.processName, uid, uid, gids, debugFlags, mountExternal,                    app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,                    app.info.dataDir, entryPointArgs);    ......    synchronized (mPidsSelfLocked) {        this.mPidsSelfLocked.put(startResult.pid, app); //保存当前的app信息    }}

9). Process.start(final String processClass,int uid, int debugFlags, int targetSdkVersion, String appDataDir,String[] zygoteArgs) —>

 public static final ProcessStartResult start(final String processClass,                                  final String niceName,                                  int uid, int gid, int[] gids,                                  int debugFlags,                                   int mountExternal,                                  int targetSdkVersion,                                  String seInfo,                                  String abi,                                  String instructionSet,                                  String appDataDir,                                  String[] zygoteArgs) {    ArrayList<String> argsForZygote = new ArrayList<String>();    argsForZygote.add("--runtime-args");    argsForZygote.add("--setuid=" + uid);    ......    //primaryZygoteState 是与Zygote进程相连的一个套接字封装结构   if (primaryZygoteState == null || primaryZygoteState.isClosed()) {       primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);       }   if (primaryZygoteState.matches(abi)) {//系统构架匹配,比如,x86,arm            final BufferedWriter writer = zygoteState.writer;            final DataInputStream inputStream = zygoteState.inputStream;            writer.write(Integer.toString(argsForZygote .size()));            writer.newLine();            int sz = argsForZygote .size();            for (int i = 0; i < sz; i++) {                String arg = argsForZygote .get(i);                if (arg.indexOf('\n') >= 0) {                    throw new ZygoteStartFailedEx(                            "embedded newlines not allowed");                }                writer.write(arg);                writer.newLine();            }            writer.flush(); //把启动app参数写入zygote进程,注意这个参数里面就有要启动的app的入口点                     //android.app.ActivityThread,还有要启动的app具体的某个类名,在/data/data下的app目录            // Should there be a timeout on this?            ProcessStartResult result = new ProcessStartResult();            result.pid = inputStream.readInt();            if (result.pid < 0) {                throw new ZygoteStartFailedEx("fork() failed");            }            return result;   }}

10) 经过上面分析Zygote就会启动我们app的入口点ActivityThread的main()方法

ActivityThread{    final ApplicationThread mAppThread = new ApplicationThread();    final Looper mLooper = Looper.myLooper();    final H mH = new H();    public static void main(String[] args) {        Looper.prepareMainLooper(); //生成主线程Looper        ActivityThread thread = new ActivityThread();        thread.attach(false);        sMainThreadHandler = thread.getHandler(); // sMainThreadHandler = mH        Looper.loop(); //运行looper    }}

上面生成了很多整个app运行期间唯一的对象,比如,mAppThread,mH ,mLooper 。再看看 thread.attach(false):

private void attach(boolean system){      //取得ActivityManagerProxy.javab 对象      final IActivityManager mgr = ActivityManagerNative.getDefault();      mgr.attachApplication(mAppThread);}

11). ActivityManagerProxy.attachApplication(IApplicationThread app) //ActivityManagerProxy对象里有ActivityManagerService的binder对象 —>
12). ActivityManagerService.attachApplicationLocked(IApplicationThread thread, int pid) —>
13). ActivityStackSupervisor.attachApplicationLocked(ProcessRecord app)

attachApplicationLocked(ProcessRecord app) {    //这里取出最顶层的要运行的activity信息,这里就是将要启动app的activity    //注意这个要启动的activity在前面第五步保存到mTaskHistory里面了    ActivityRecord hr = stack.topRunningActivityLocked(null);    if (hr != null) {         realStartActivityLocked(hr, app, true, true)    }}

14). ActivityStackSupervisor.realStartActivityLocked(ActivityRecord r,ProcessRecord app, boolean andResume, boolean checkConfig)

final boolean realStartActivityLocked(ActivityRecord r,ProcessRecord app,                         boolean andResume, boolean checkConfig){    //这里app.thread就是上面第十步中ActivityThread中的mAppThread    app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken, ......);}

15). 所以最终调用到ActivityThread类中:

ApplicationThread{    public final void scheduleLaunchActivity(Intent intent, IBinder token, ......){        ActivityClientRecord r = new ActivityClientRecord();        r.intent = intent;        r.activityInfo = info;        sendMessage(H.LAUNCH_ACTIVITY, r); //mH.sendMessage(msg);    }}

16). ActivityThread中的H类

H extends Handler {   public static final int LAUNCH_ACTIVITY         = 100;   public static final int PAUSE_ACTIVITY          = 101;   public static final int SHOW_WINDOW             = 105;   public static final int HIDE_WINDOW             = 106;   public static final int RESUME_ACTIVITY         = 107;   public static final int SEND_RESULT             = 108;   public void handleMessage(Message msg){         switch (msg.what) {            case LAUNCH_ACTIVITY: {            final ActivityClientRecord r = (ActivityClientRecord) msg.obj;            handleLaunchActivity(r, null);        }    }}

17). ActivityThread.handleLaunchActivity(…..)

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent){     // Initialize before creating the activity    WindowManagerGlobal.initialize();    //启动activity    Activity a = performLaunchActivity(r, customIntent);    if (a != null) {        //执行resume        handleResumeActivity(r.token, false, r.isForward,                !r.activity.mFinished && !r.startsNotResumed);    }}

18). 先执行ActivityThread.performLaunchActivity()

 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {   ActivityInfo aInfo = r.activityInfo;   ComponentName component = r.intent.getComponent();   //加载要执行activity的java文件   activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);   Application app = r.packageInfo.makeApplication(false, mInstrumentation);   activity.attach(appContext, this,            getInstrumentation(), r.token,           r.ident, app, r.intent,            r.activityInfo, title, r.parent,           r.embeddedID, r.lastNonConfigurationInstances, config,           r.referrer, r.voiceInteractor);    //调用activity的onCreate    mInstrumentation.callActivityOnCreate(activity, r.state); }

到这里activity终于启动起来,并调用了attach方法,回到最前面,我们看attach方法:

  final void attach(Context context, ActivityThread aThread,            Instrumentation instr, IBinder token, int ident,            Application application, Intent intent, ActivityInfo info,            CharSequence title, Activity parent, String id,            NonConfigurationInstances lastNonConfigurationInstances,            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {        mWindow = new PhoneWindow(this);        mWindow.setCallback(this);        mUiThread = Thread.currentThread();        mMainThread = aThread;        mInstrumentation = instr;    }

所以每个activity有一个mWindow (PhoneWindow), 并且mWindow持有了Activity的引用。接着会调用 mInstrumentation.callActivityOnCreate()方法。
19).mInstrumentation.callActivityOnCreate(Activity activity, Bundle icicle)

    public void callActivityOnCreate(Activity activity, Bundle icicle) {        prePerformCreate(activity);        activity.performCreate(icicle);        postPerformCreate(activity);    }

20). Activity.performCreate(icicle); —>
21). onCreate(icicle); 我们自己覆写的onCreate方法 —>
22). Activity.setContentView(@LayoutRes int layoutResID) —>
23). PhoneWindow.setContentView(int layoutResID)

 public void setContentView(int layoutResID) {     installDecor();      //将我们自定义的activity layout布局添加到mContentParent view里面     mLayoutInflater.inflate(layoutResID, mContentParent);}

24). PhoneWindow.installDecor() 这里会 new 一个 DecorView

 private void installDecor() {    if (mDecor == null) {        mDecor = generateDecor();  //new DecorView(getContext(), -1) 是一个FrameLayout    }    if (mContentParent == null) {        mContentParent = generateLayout(mDecor);    }}

25). PhoneWindow.generateLayout(DecorView decor)

 protected ViewGroup generateLayout(DecorView decor) {     //这里会生成一个LinearLayout, 并且有两个child view,      //这里会根据activity不同属性生成相应的child view,但是content view是肯定有的     //一个是android.view.ViewStub{android:id/action_mode_bar_stub},     //一个是android.widget.FrameLayout{android:id/content}     View in = mLayoutInflater.inflate(layoutResource, null);    // 加入到前面生成的DecorView里面    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));    mContentRoot = (ViewGroup) in;    //ID_ANDROID_CONTENT = com.android.internal.R.id.content;     //前面23步,就是把我们自己的layout布局加载到这个view里,这就是我们所看见的activity布局    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);    return contentParent;}

26). 执行完activity的onCreate后返回前面17步,执行ActivityThread的handleResumeActivity方法

 final void handleResumeActivity(IBinder token,            boolean clearHide, boolean isForward, boolean reallyResume) {      //执行activity里onResume            ActivityClientRecord r = performResumeActivity(token, clearHide);      if (r != null) {            final Activity a = r.activity;            if (r.window == null && !a.mFinished && willBeVisible) {                r.window = r.activity.getWindow(); //PhoneWindow                View decor = r.window.getDecorView();                decor.setVisibility(View.INVISIBLE);                ViewManager wm = a.getWindowManager(); //WindowManagerImpl                      if (a.mVisibleFromClient) {                    a.mWindowAdded = true;                    wm.addView(decor, l);                }            }      }}

27). WindowManagerImpl.addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) 开始界面的绘制工作了

    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {        applyDefaultToken(params);        mGlobal.addView(view, params, mDisplay, mParentWindow); //mGlobal在前面17步已经初始化了    }

28). WindowManagerGlobal.addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow)

 public void addView(View view, ViewGroup.LayoutParams params,            Display display, Window parentWindow) {    ViewRootImpl root;    root = new ViewRootImpl(view.getContext(), display);    view.setLayoutParams(wparams);    mViews.add(view);    mRoots.add(root);    root.setView(view, wparams, panelParentView); //view == DecorView}

29). 我们先看看ViewRootImpl类

public final class ViewRootImpl implements ViewParent,        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {  public ViewRootImpl(Context context, Display display) {        mContext = context;        mWindowSession = WindowManagerGlobal.getWindowSession();        mDisplay = display;        mThread = Thread.currentThread();        mWindow = new W(this);    }}

30). 初始化ViewRootImpl后,接着ViewRootImpl.setView(View view, WindowManager.LayoutParams attrs, View panelParentView)

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {        synchronized (this) {            if (mView == null) {                mView = view;  // com.android.internal.policy.PhoneWindow$DecorView                mInputChannel = new InputChannel();                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,                            getHostVisibility(), mDisplay.getDisplayId(),                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,                //输入事件接收者                mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,                            Looper.myLooper());                  // Set up the input pipeline.                CharSequence counterSuffix = attrs.getTitle();                mSyntheticInputStage = new SyntheticInputStage();                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,                        "aq:native-post-ime:" + counterSuffix);                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);                InputStage imeStage = new ImeInputStage(earlyPostImeStage,                        "aq:ime:" + counterSuffix);                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,                        "aq:native-pre-ime:" + counterSuffix);                mFirstInputStage = nativePreImeStage;                mFirstPostImeInputStage = earlyPostImeStage;               }        }}

31). WindowInputEventReceiver是ViewRootImpl的一个内部类

  final class WindowInputEventReceiver extends InputEventReceiver {        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {            super(inputChannel, looper);        }        @Override        public void onInputEvent(InputEvent event) {            enqueueInputEvent(event, this, 0, true);        }        @Override        public void dispose() {            unscheduleConsumeBatchedInput();            super.dispose();        }    }

32). WindowInputEventReceiver父类InputEventReceiver

public abstract class InputEventReceiver {  // Map from InputEvent sequence numbers to dispatcher sequence numbers.  private final SparseIntArray mSeqMap = new SparseIntArray();  private static native long nativeInit(WeakReference<InputEventReceiver> receiver,            InputChannel inputChannel, MessageQueue messageQueue);  private static native void nativeDispose(long receiverPtr);  private static native void nativeFinishInputEvent(long receiverPtr, int seq, boolean handled);  public InputEventReceiver(InputChannel inputChannel, Looper looper) {        mInputChannel = inputChannel;        mMessageQueue = looper.getQueue();        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),                inputChannel, mMessageQueue); //与底层input事件提供者建立接收通道        mCloseGuard.open("dispose");    }  private void dispose(boolean finalized) {        nativeDispose(mReceiverPtr);        mInputChannel = null;        mMessageQueue = null;    }  // Called from native code.  //点击事件来时,就会由底层把事件传递到这里  private void dispatchInputEvent(int seq, InputEvent event) {       mSeqMap.put(event.getSequenceNumber(), seq);       onInputEvent(event);  }}

33). 当event事件到来时,最终调用到WindowInputEventReceiver的onInputEvent(event) —>
34). ViewRootImpl.enqueueInputEvent(event, this, 0, true) —>
35). ViewRootImpl.deliverInputEvent(QueuedInputEvent q)

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

36). 这里stage是前面30步里实例化的ViewPostImeInputStage类,并且它是ViewRootImpl的一个内部类,所以,ViewPostImeInputStage.deliver(QueuedInputEvent q) —>
37). ViewPostImeInputStage.onProcess(QueuedInputEvent q) —>
38). ViewPostImeInputStage.processPointerEvent(QueuedInputEvent q)

 private int processPointerEvent(QueuedInputEvent q) {      final MotionEvent event = (MotionEvent)q.mEvent;      boolean handled = mView.dispatchPointerEvent(event); //mView == PhoneWindow.decorView      return handled ? FINISH_HANDLED : FORWARD; }

39). mView 就是PhoneWindow.decorView实例,decorView本身是继承了FrameLayout,所以上面event事件会传递到View.java 的dispatchPointerEvent()方法

    public final boolean dispatchPointerEvent(MotionEvent event) {        if (event.isTouchEvent()) {            return dispatchTouchEvent(event);        } else {            return dispatchGenericMotionEvent(event);        }    }

40). 最后传递到DecorView的dispatchTouchEvent()

 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {        @Override        public boolean dispatchTouchEvent(MotionEvent ev) {            // Callback  就是activity, 前面activity 实现了window 的 callback 方法            final Callback cb = getCallback();            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)                    : super.dispatchTouchEvent(ev);        }}

41). 因为Callback就是前面18步attach()方法里 通过mWindow.setCallback(this)方法设置的activity,所以,事件最终传到了activity的dispatchTouchEvent(ev)方法

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

总结

下面再简化总结下流程:
1. 由Instrumentation执行execStartActivity(intent)
2. ActivityManagerService调用startActivity(intent)
3. ActivityStackSupervisor执行startSpecificActivityLocked(ActivityRecord r),这里ActivityStackSupervisor会生成:ActivityRecord,代表要启动的activity信息,ActivityStack,代表activity栈信息,ProcessRecord ,代表一个运行的app应用信息
4. ActivityManagerService调用startProcessLocked(ProcessRecord app)这里把app应用的入口点“android.app.ActivityThread”类,和相关启动参数传给Process类
5. Process类调用start()方法将app应用的启动信息传给Zygote进程,有Zygote fork一个新的进程出来
6. 执行app应用的入口点ActivityThread类的main()方法,这里会生成mLooper 应用的Looper, 还有ApplicationThread ,mH(Handler)实例
7. 执行ActivityThread类的attach()方法,得到ActivityManagerService实例,并调用它的attachApplication()方法
8. 执行ActivityStackSupervisor的realStartActivityLocked()最终会调用到ActivityThread类中ApplicationThread子类的scheduleLaunchActivity()方法
9. 由mH发送LAUNCH_ACTIVITY消息,执行performLaunchActivity()方法
10. 执行Instrumentation的newActivity()加载要启动的activity
11. 执行activity.attach()方法,生成PhoneWindow实例,并把activity的Window.Callback传到PhoneWindow里
12. 执行Instrumentation的callActivityOnCreate()方法,调用到activity里面的OnCreate方法,生成DecorView,并把DecorView传给PhoneWindow
13. 执行ActivityThread的handleResumeActivity()方法,调用到activity里的OnResume方法,WindowManagerGlobal增加DecorView
14. 生成ViewRootImpl实例,初始化ViewRootImpl成员InputChannel,InputStage,WindowInputEventReceiver用于接收底层传来的event事件
15. 当event事件传递到ViewRootImpl的WindowInputEventReceiver后,经过InputStage的onProcess处理后,传递给PhoneWindow类的decorView成员的dispatchPointerEvent方法
16. 最终传递个activity的dispatchTouchEvent()方法

例子

写一个简单的例子,验证下。在activity的dispatchTouchEvent(MotionEvent ev)里加上Thread.dumpStack(),通过dumpStack方法来打印出当前线程的调用栈信息。

    public boolean dispatchTouchEvent(MotionEvent ev) {        Thread.dumpStack();        if(ev.getAction() == MotionEvent.ACTION_DOWN){            Log.e(TAG, "dispatchTouchEvent, ev=" + ev.getAction() + " : ACTION_DOWN");        }else if(ev.getAction() == MotionEvent.ACTION_UP){            Log.e(TAG, "dispatchTouchEvent, ev=" + ev.getAction() + " : ACTION_UP");        }else if(ev.getAction() == MotionEvent.ACTION_MOVE){            Log.e(TAG, "dispatchTouchEvent, ev=" + ev.getAction() + " : ACTION_MOVE");        }        return super.dispatchTouchEvent(ev);    }

打印信息如下:
这里写图片描述

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟


这里写图片描述

0 0