View的事件体系总结

一、基础知识

1、View的坐标系

View的坐标系统是相对于父控件的,如下图:
图片来自GcsSloop

  getTop();       //获取子View左上角距父View顶部的距离
  getLeft();      //获取子View左上角距父View左侧的距离
  getBottom();    //获取子View右下角距父View顶部的距离
  getRight();     //获取子View右下角距父View左侧的距离
getX()、getTranslationX()

Android3.0后增加了:

x、y :	表示View左上角坐标。用getX()、 getY()获得
translationX、translationY : 	表示View的左上角相对于父容器的偏移量,
	通过 getTranslationX()、getTranslationY()获得。 默认为0

其中:

其中:
x = getLeft() + translationX ;
y = getTop() + translationY ;
2、MotionEvent

表示触摸屏幕产生的一系列事件。常用的有如下三种:

  • ACTION_DOWN : 手指刚开始触摸屏幕,事件的起始位置。
  • ACTION_MOVE :手指在屏幕上移动。
  • ACTION_UP :手指离开屏幕的瞬间触发。

从事件开始到结束任意时间内,都可以通过 MotionEvent 内部的 getX/getY和getRawX/getRayY获得相应坐标,两种方式的区别如下图:
在这里插入图片描述
两种方式的含义:

    event.getX();       //触摸点相对于其所在组件坐标系的坐标
    event.getY();

    event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
    event.getRawY();
3、VelocityTracker、GestureDetector、Scroller
①、VelocityTracker速度追踪

用法如下:

        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        
        //1000ms内速度
        velocityTracker.computeCurrentVelocity(1000);
        //x轴方向速度
        int xVelocty = (int) velocityTracker.getXVelocity();
        //y方向速度
        int yVelocty = (int) velocityTracker.getYVelocity();
        
        //释放
        velocityTracker.clear();
        velocityTracker.recycle();

速度的单位是: 像素/毫秒(px/ms),eg:100像素/每毫秒

②、GestureDetector 手势检测

包含一下方法:

  • onDown:触摸到屏幕
  • onShowPress:
  • onSingleTapUp:单击
  • onScroll:手指滚动
  • onLongPress:长按
  • onFling: 手指离开,页面滑动
③、Scroller

弹性滑动对象,用于实现view的弹性滑动。

二、View的滑动

1、scrollTo/scrollBy
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

可以看到scrollBy也是调用scrollTo方法实现移动。

特点:
  • 无论是scrollTo还是scrollBy都无法改变view的位置,移动的是view的内部位置。
  • scrollTo属于绝对滑动,移动的位置是相对于View的。即:无论移动多少次,位置都是在第一次移动的位置。
  • scrollBy属于相对滑动,移动的位置是相对自己的。即:每次点击移动,都会相对自己的位置再次移动。
  • 移动的距离scrollX和scrollY正负和Android坐标系相反。即x移动正100,view的内容向左移动100(不是向右),y移动负100,view内容向下移动100(不是向上)。
2、使用动画实现view的滑动

★ 使用属性动画可以实现view的滑动。

view动画,不能真正改变动画的位置。即位置改变了,但是view的事件还留在原来的位置

nineoldandroids动画兼容库
3、使用LayoutParams改变位置参数。

可用通过改变view的margin属性,或者改变父view的padding属性。实现view的滑动

三、弹性滑动

1、使用scroller
2、使用动画
3、使用延时策略

四、View的事件分发机制

view的事件分发机制指的是从手指按下屏幕开始,事件从屏幕传递到指定view的一系列过程。

1、点击事件的传递规则

View的事件分发其实是对MotionEvent事件的分发过程。

而事件的分发过程由三个很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

  • dispatchTouchEvent(MotionEvent event) :用来分发事件。返回结果受当前view的onTouchEvent和下级View的dispatchTouchEvent方法影响。
  • onInterceptTouchEvent(MotionEvent ev) :用来拦截事件。
  • onTouchEvent(MotionEvent event) :在dispatchTouchEvent方法中调用,表示是否消耗当前事件
三者之间的关系

viewgroup的事件分发可以用下面伪代码表示三者之间的关系:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean consume = false;
        //当前view是否拦截
        if (onInterceptTouchEvent(event)){
        	//拦截后,则调用自己的onTouchEvent,
        	//如果onTouchEvent消耗事件则返回true,否则false,交由父控件处理
            consume = onTouchEvent(event);
        }else {
        	//如果不拦截,则获得子view是否消耗
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
    }

2、事件分发源码

当我们点击屏幕产生事件时,最先接收事件的是Activity。所以事件先从Activity的dispatchTouchEvent开始分发。

1、Activity事件分发

Activity中 dispatchTouchEvent 方法源码如下:

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

从上面源码可以看到,activity事件分发受到Widow的superDispatchTouchEvent方法影响。
在这里插入图片描述
可以看到Window是一个抽象方法。注释方法里面说它有一个子类PhoneWindow。

可以全局搜索PhoneWindow。找到PhoneWindow的位置,在com.android.internal.policy包中

PhoneWindow

查看PhoneWindow的superDispatchTouchEvent方法:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到该方法的返回值又受到 mDecor 中的方法影响。
查看mDecor 声明的地方

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

从注释中可以看到,DecorView 是PhoneWindow的顶层视图。

DecorView

在这里插入图片描述
可以看到DecorView 继承FrameLayout。DecorView 的superDispatchTouchEvent方法源码如下:

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

因为DecorView 继承自FrameLayout,所以这里DecorView 调用ViewGroup的dispatchTouchEvent将事件向下传递分发。

这个时候我们的事件已经传递到了DecorView 了。 传递顺序如下:
Activity --> PhoneWindow --> DecorView

事件是怎么从DecorView传递到我们自己的Layout中的?
Activity & setContentView()

在Activity中我们通过 setContentView()来加载我们的布局。源码如下:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

可以看到,它会调用PhoneWindow的setContentView()方法来加载我们的布局文件。

PhoneWindow & setContentView()
    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
		//加载布局
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

1、当 mContentParent为空时,会执行 installDecor()方法。因为mContentParent是在installDecor()方法中赋值的,所以一定会先执行installDecor()方法来初始化。

2、当mContentParent不为空,则移除mContentParent内部的view,将布局文件添加到mContentParent中。

PhoneWindow & installDecor()
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
           ...
        }
    }

可以看到,在installDecor中会初始化mDecormContentParent
mContentParent = generateLayout(mDecor);

PhoneWindow & generateLayout(mDecor)

从方法名就可以看出来了,这个方法是在mDecor 中生成一个layout布局。

    protected ViewGroup generateLayout(DecorView decor) {
    	...//省略资源加载
        mDecor.startChanging();
        //layoutResource 在上面加载过了,省略
        //mDecor 加载layoutResource布局
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
         //通过findViewById找到contentParent 
         // int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
         ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        mDecor.finishChanging();
        return contentParent;
    }

布局文件如下:
在这里插入图片描述
在上面的方法中,DecorView会加载layoutResource布局文件,layoutResource如上图,通过findviewbyid找到contentParent 控件,也就是上图红框代表的FrameLayout。

而我们加载的布局文件就是放在红框的contentParent 中。

用一张图来展示他们之间的层级关系如下:

在这里插入图片描述
这个时候我们的事件就传递到了ContentParent中了,然后再由ContentParent传递到我们布局文件的最外层View即根View

findViewById(id)

这里面既然用到了findViewById(id)那我们不妨看一下findViewById的源码:

Activity & findViewById
    @Nullable
    public View findViewById(@IdRes int id) {
        return getWindow().findViewById(id);
    }
Window & findViewById
    @Nullable
    public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

我的findViewById其实也是在DecorView中查找控件id的

事件从Activity到根View传递顺序:
Activity -> PhoneWindow -> DecorView -> ContentParent -> 根View
3、根View对点击事件的分发
①、ViewGroup事件分发

如果根View是ViewGroup,则会调用ViewGroup 的 dispatchTouchEvent方法,

dispatchTouchEvent 拦截部分源码如下:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                 //子view是否调用requestDisallowInterceptTouchEvent()
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

默认ViewGroup是不会拦截事件分发的。
可以看到子View可以调用requestDisallowInterceptTouchEvent来影响父view是否拦截。

1、ViewGroup不拦截事件
  • 如果viewgroup不拦截事件的话,viewgroup会遍历所有子view,并调用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;
        }
        ...//省略部分情况判断
	}

可以看到,当子view不为空时,如果child是ViewGroup则会再次执行ViewGroup的dispatchTouchEvent。如果子View为空则执行View的dispatchTouchEvent

view的dispatchTouchEvent方法放到下面讲。

2、ViewGroup拦截事件

如果ViewGroup拦截分发事件,则执行自己的OnTouchEvent()方法。而ViewGroup没有专门实现自己的OnTouchEvent方法的逻辑,仍然使用的是view的OnTouchEvent逻辑。view的OnTouchEvent方法下面讲。

②View的事件分发

上面说viewgroup的事件分发的时候,在ViewGroup的dispatchTouchEvent方法中,不拦截的话最终会执行view的dispatchTouchEvent方法。
view的dispatchTouchEvent部分源码如下:

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

1、可以看到,当view设置了setOnTouchListener的时候,mOnTouchListener不为null,此时view的dispatchTouchEvent方法的返回值受mOnTouchListener.onTouch()方法影响。
如果在onTouch()方法中返回true,则view的dispatchTouchEvent方法返回值就为true。而如果view上面还有ViewGroup,则ViewGroup的dispatchTouchEvent方法也就返回true,则不再继续分发事件。
2、如果没有设置setOnTouchListener或者mOnTouchListener.onTouch()方法返回false,则执行View的onTouchEvent(event)方法

View的onTouchEvent方法
onTouchEvent部分源码如下:

public boolean onTouchEvent(MotionEvent event) {
	...
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    ...
        switch (action) {
            case MotionEvent.ACTION_UP:
            	...
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                ...
}

1、如果给view设置setTouchDelegate()此时onTouchEvent方法返回值受mTouchDelegate.onTouchEvent(event)方法影响。
2、在MotionEvent.ACTION_UP的时候,会执行performClick()方法,即点击事件的方法。源码如下:

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

当我们给View设置点击事件的时候,则在此执行mOnClickListener.onClick()方法。

到这里一个事件从Activity的dispatchTouchEvent方法开始分发,一直到View的onClick()方法响应的整个过程已经分析完了。

View事件优先级总结

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick

五、View的滑动冲突解决方式

1、外部拦截法

在外部布局的onInterceptTouchEvent 方法中ACTION_MOVE事件中判断是否拦截子view的事件,并 在ACTION_UP和ACTION_DOWN中释放拦截。
伪代码如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前事件){
                    //拦截
                    intercept = true;
                }else {
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }

        return intercept;
    }

2、内部拦截法
指父容器不拦截任何事件,所有的事件都交给子view处理,如果子view需要就消耗掉,否则交给父容器处理。需要配合requestDisallowInterceptTouchEvent使用。

子元素的dispatchTouchEvent方法如下:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //屏蔽父容器事件
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前事件){
                    //交给父容器处理
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

父容器需要将ACTION_DOWN的拦截事件接触,不然在需要父容器接收的时候,父容器也没有地方接收。

父元素修改如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() ==  MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }
相比较内部拦截法,外部拦截更加方便,只需要在一个view内做拦截就行了

参考:Android开发艺术探索。
安卓中的坐标系

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值