View的事件体系

一、View基础

1、view

  • view的位置主要由四个顶点来决定,top、left、right、bottom,这些坐标都是相对于view的父容器,是一种相对坐标。
  • view的另外几个参数:x、y、translationX、translationY,其中x和y是View左上角的坐标,而translationX、translationY是view左上角相对于父容器的偏移量。
    x = left + translationX
    y = top + translationY
  • view在平移的过程中,top和left表示的是原始左上角的位置信息,值不会发生变化,发生变化的是x、y、translationX、translationY这四个参数。

2、MotionEvent

  • 三个事件:ACTION_DOWN、ACTION_MOVE、ACTION_UP
  • 点击事件坐标:getX/getY()返回当前view左上角的坐标,getRawX/getRawY返回相对于屏幕左上角坐标
  • TouchSlop:系统识别被认为的最小滑动距离。获取这个常量方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()

3、VelocityTracker

  • 含义:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
  • 用法:
    ①在view的onTouchEvent方法中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event); 

②获取当前速度

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在获取速度之前需要调用computeCurrentVelocity来计算速度,当前的速度是这1000ms手指划过的像素数,此时计算速度:速度=(终点位置 - 起点位置)/时间段,其中速度可以为负,比如是从右向左滑动
③最后不需要使用时需要回收

velocityTracker.clear();
velocityTracker.recycle();

4、GestureDetector

  • 含义:手势检测,用于辅助检测用户的单机、滑动、长按、双击等行为
  • 用法:
    ①新建一个类继承OnGestureListener,OnDoubleTapListener是双击,选择性实现
public static class MyGestureListener implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener

②创建一个GestureDetector对象,GestureDetector有三个构造函数

  • public GestureDetector(Context context, OnGestureListener listener)
  • public GestureDetector(Context context, OnGestureListener listener, Handler handler)
  • public GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean unused)
GestureDetector mDetector=new GestureDetector(getContext(),new MyGestureListener());
//解决长按屏幕后无法拖动的现象
mDetector.setIsLongpressEnabled(false);

③接管目标view的onTouchEvent方法,在待监听的View的onTouchEvent方法中添加实现

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

5、Scroller

  • 含义:弹性滑动对象,用于实现view的弹性滑动
  • 用法:代码比较固定
Scroller scroller = new Scroller(mContext);

//缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY){
   int scrollX = getScroller();
   int deltaX = destX - scrollX;
   //1000ms内滑向destX,效果就是慢慢滑动
   scroller.startScroll(scrollX, 0, deltaX, 0, 1000);
   invalidate();
}

@override
public void computeScroll(){
  if (scroller.computeScrollOffset()){
     scrollTo(mScrollrt.getCurrX(),mScroller.getCurrY());
     postInvalidate();
}

原理:
在startScroll中记录开始时间,mDuration 是整个滑动的周期,然后调用invalidate方法,这个方法会导致view重绘,继而调用view的draw方法,而draw方法会调用computeScroll方法(这个方法在view中是一个空实现),上面已经实现了computeScroll,然后继续看computeScrollOffset方法,这个方法里面记录了相应时间里面滑动了的距离,用scrollTo滑动到相应位置然后调用postInvalidate方法继续调用draw方法循环重绘直到滑动周期结束。

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        //记录开始时间
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }
    
    public boolean computeScrollOffset() {
       ...
       //当前时间减去滑动开始时间,得到已经过去的时间
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
      //过去的时间小于滑动周期时
        if (timePassed < mDuration) {
            switch (mMode) {
            	case SCROLL_MODE:
            	//距离按时间等分,每次滑动响应时间的距离
                	final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
               		mCurrX = mStartX + Math.round(x * mDeltaX);
                	mCurrY = mStartY + Math.round(x * mDeltaY);
               	 	break;
    ...
        	}
        }
        ...
        return true;
    }

二、View的滑动

1、使用scrollTo/scrollBy

scrollTo(int x, int y) —> x,y是view即将滑动的目标位置
scrollBy(int x, int y) —> x,y是view基于横向、竖向的滑动距离
滑动的是view内容的位置,而不能改变view在布局中的位置

2、使用动画

①view动画
②属性动画

 ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

3、改变布局参数

比如view1要向右移动100px,在这个view左边放一个0px的view2,此时设置view2宽度100px,让view2把view1挤到右边完成滑动

总结:
scrollTo/scrollBy:操作简单,适合对view内容的滑动
使用动画:操作简单,适合没有交互的view和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于又交互的view

4、弹性滑动

  • 使用Scroller
  • 使用动画
  • 使用延时策略,可以运用延时策略百分比分割滑动过程分次平滑的滑动

三、事件分发

1、三个方法

boolan dispatchTouchEvent:来进行事件的分发,如果事件能够传递给这个view则一定会被调用,返回结果受当前view的onTouchEvent方法和下级的dispatchTouchEvent方法的影响

boolean onInterceptTouchEvent:在dispatchTouchEvent方法中调用,判断是否拦截某个事件

boolean onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,当前view不会再收到这个事件的后续move、up等事件

总结:他们的关系view(ViewGroup):

//事件传递到这个view,调用dispatchTouchEvent方法判断是否拦截消费
public boolean dispatchTouchEvent(MotionEvent ev) {
		//如果这个view不消费
        boolean consume = false;
        //判断这个view是否拦截
        if(onInterceptTouchEvent(ev)) {
        //这个view拦截,调用这个view的onTouchEvent方法
            consume = onTouchEvent(ev);
        } else {
         //这个view拦截,当前事件传递给子view
            consume = child.dispatchTouchEvent(ev);
        }
        
        return consume;
    }

2、总体分发逻辑

事件的源头在activity的dispatchTouchEvent方法,会接着传递到phoneWindow、decorView,这个decorview是顶层ViewGroup,此时层层向下分发,判断Viewgroup是否拦截事件消费事件,如果不拦截不消费则发给他的子view。

  • 整个分发过程中没有拦截和消费,down事件层层下发,并层层向上返回false,move和up事件会交给actiivity的onTouchEvent处理
  • 整个分发过程中没有拦截但是有消费,down事件层层下发,并层层向上返回false,知道有消费返回true,move和up事件就交给这个消费事件的View处理。
  • 整个分发过程中有拦截有消费,down事件层层下发,直到有拦截后,交给消费的view处理
  • 分发过程中不拦截down事件,但拦截move事件并消费,之前的收到down事件的子view会收到cancel事件,后续的move和up会层层下发给拦截move事件的view
  • 分发过程中不拦截down事件,但拦截move事件且不消费,这个down事件被cancel,后续的move等事件交给actvity的OnTouchEvent处理

四、滑动冲突

1、出现滑动冲突的场景

  • 外部滑动和内部滑动方向不一致
  • 外部滑动和内部滑动方向一致
  • 上面两种情况的嵌套

2、外部拦截法

含义:点击事件经过父容器的拦截处理,判断父容器是否拦截

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            //ACTION_DOWN事件父容器不可以拦截
               intercepted = false; 
               break;
            case MotionEvent.ACTION_MOVE:
            //根据需求拦截
                if(父容器需要相应的事件) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
}

3、内部拦截法

含义:父容器的不拦截任何事件,所有的事件都传给子元素,如果子元素需要这个事件就自己处理,否则交给父容器处理

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if("父容器需要相应的事件") {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值