Android进阶(3)| View的事件体系

本节目录

一.View的基础知识

1.View的概念

View:View是Android中所有控件的基类,即我们经常使用的Button、TextView或者是其他的控件等都是继承自View的。

ViewGroup:顾名思义,即是一组控件的集合,需要注意的是ViewGroup也继承了View,也就是说View本身不仅是单个控件也可以是多个控件组成的一组控件。所以LinearLayout与其他的布局也是继承自View的。

联系:ViewGroup的内部可以有子View,而这个子View也可以是一个ViewGroup。

2.View的位置参数

决定因素:View的位置主要是由它的四个顶点来决定的,而这四个顶点分别对应于View的四个属性:top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)和bottom(右下角纵坐标)。需要注意的是View的四个坐标只是相对坐标,即它们只是相对于View的父布局而言。

View的位置坐标

由上我们可以得到View的宽度和高度:

width = right - left;
height = bottom - top;

获取参数的方法:在实际中我们想要获取View的位置参数也很简单,只需要调用相应的方法即可,具体方法如下:

left = getLeft();
top = getTop();
right = getRight();
bottom = getBottom();

补充:从Android 3.0开始系统为View增添了另外四个参数:x(View左上角的横坐标)、y(View左上角的纵坐标)、translationX(View左上角的横轴相对于父布局的偏移量)和translationY(View左上角的纵轴轴相对于父布局的偏移量)。它们与之前四个参数的关系如下:

x = left + translationX;
y = top + translationY;

3.有关View的事件

1.MotionEvent

指的是手指接触屏幕后所产生的一系列事件,典型的事件类型有如下几种:

  • ACTION_DOWN
    手指刚刚接触屏幕

  • ACTION_MOVE
    手指开始在屏幕上滑动

  • ACTION_UP
    手指从屏幕上面移开

按照一般用户使用手机的方式,事件发生的序列为:DOWN->MOVE->MOVE->......->UP。

通过MotionEvent对象,我们可以获取点击事件发生的x和y坐标。系统提供了两组方法,下面来分别介绍:

  • getX()/getY()
    返回相对于当前View左上角的x和y坐标。

  • getRawX()/getRawY()
    返回相对于手机屏幕左上角的x和y坐标。

2.TouchSlop

TouchSlop是系统所能识别的最小活动距离。简单来说如果用户在屏幕上用手指的滑动距离小于TouchSlop,则系统会识别为这不是滑动,也就不会触发相关的事件。TouchSlop是一个常量,它具体的大小是和设备有关的。如果想要获得该常量,我们可以使用ViewConfiguration.get(getContext()).getScaledTouchSlop()方法来获得。

3.VelocityTracker

用于追踪手指在滑动过程中的速度,包括水平和竖直方向上的速度。它的使用方法如下:

VelocityTracker velocityTracker = VelocityTracker.obtain(); //获得该对象
velocityTracker.addMovement(event); //添加需要追踪的事件

velocityTracker.computeCurrentVelocity(1000); //计算速度
int xVelocity = (int) velocityTracker.getXVelocity(); //获得水平方向的速度
int yVelocity = (int) velocityTracker.getYVelocity(); //获得竖直方向的速度

velocityTracker.clear(); //重置
velocityTracker.rectcle(); //回收内存

在使用过程中我们需要注意以下三点:
1)在获得速度之前必须要先计算速度,即对VelocityTracker对象使用computeCurrentVelocity()方法,在这个方法中传入的参数是计算速度的时间间隔,单位是毫秒。
2)这里计算的速度是带有方向的,简单来说就是从左向右滑动是正,从右向左滑动是负。
3)当我们使用完之后必须要对VelocityTracker对象重置并且回收。

4.GestureDetector

用于辅助检测用户的单击、滑动、长按和双击等行为。它的使用方法如下:

GestureDetector gestureDetector = new GestureDetector(this); //创建对象
gestureDetector.setIsLongpressEnable(false); //解决长按屏幕后无法拖动

//接管目标View的onTouchEvent方法,在View中的onTouchEvent()方法中完成
boolean consume = gestureDetector.onTouchEvent(event);
return consume;

在完成上述操作步骤之后我们就可以根据自己的需要来选择要实现OnGestureListener()和OnDoubleTapListener()接口中的方法,它们中的具体方法如下:


接口方法
5.Scroller

用于实现View的弹性滑动。所谓弹性滑动,就是指让View在移动的过程中不是瞬间完成的,而是有一定动画过度的滑动。它的使用方法如下:

Scroller scroller = new Scroller(mContext);

private void smoothScrollTo(int destX,int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    scroller.startScroll(scrollX,0,delta,0,1000); //在1000ms内滑到destX
    invalidate();
  }

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

二.View的滑动

View的滑动简单来说就是指改变View的位置。下面介绍三种方法来实现View的滑动:

1.使用scrollTo/scrollBy

这两种方法是View自带的用于实现滑动操作的方法,它们的具体使用方式如下:

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

mScrollX:指的是View的左边缘和View内容的左边缘之间的距离。当View从左向右滑动时mScrollX是负值,反之为正值。
mScrollY:指的是View的上边缘和View内容的上边缘之间的距离。当View从上向下滑动时mScrollY是负值,反之为正值。

注意:使用scrollTo/scrollBy对View进行滑动时只是对View的内容进行滑动,反过来说就是在滑动过程中View在布局中的位置是不会被改变的。

变换规律

2.使用动画

1.使用View动画

使用View动画的操作方法是:

<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">

<translate
    android:duration="100"
    android:fromXDelta="0"
    android:fromYDelta="0"
    android:interpolator="@adroid:anim/linear_interpolator"
    android:toXDelta="100"
    android:toYDelta="100"
/>

特性:是用View动画改变的只是View的影像,它并不能真正改变View的位置参数。并且如果希望View在滑动完只后能够保持状态还必须将android:fillAfter中的值变为true,否则滑动完成之后View还是会回到它之前的位置。

使用后果:因为上述View动画的局限性,所以有可能出现这样一直情况,即我们适用View动画将一个带有单击事件的Button进行滑动之后,你会发现点击Button的新位置不会有任何反应,而点击Button的原位置则会有反应。这就是View动画中的点击问题。

2.使用属性动画

使用属性动画必须要在Android3.0以上的手机中才能够实现,它的具体使用方法是:

objectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100)
.start(); //在100ms内将View向右平移100像素

注意:如果我们想要在Android3.0以下的手机中使用属性动画,则必须使用Android开源动画库nineoldandroids来进行兼容适配。

3.改变布局参数

即改变View的LayoutParms。这种方法比较好理解,就是将View中的位置信息进行修改,让View能够到达新的位置。除了修改View本身的位置信息以外,我们还可以在该View的前面新建一个View2并将View2的默认宽度设置为0,当我们需要右移View时,只需要将View2的宽度增加即可,这就相当于是用View2来挤View来发送移动。

4.各种方式的比较

  • scrollTo/scrollBy
    操作简单,适合对View的内容进行滑动

  • 动画
    操作简单,适用于没有交互的View和实现复杂的交互效果

  • 改变布局参数
    操作复杂,但是很适用于有交互的View

三.弹性滑动

弹性滑动顾名思义,就是让View不那么生硬的进行活动,而是实现一种渐进式的滑动。实现弹性滑动的主要思想是:将一次大的滑动分成几次小的滑动,并且在一个时间段内完成。下面介绍三种方法:

1.使用Scroller

Scroller在前面有关View的事件中已经介绍过了它的典型用法,这里就不多赘述,在这里我们重点来看看它的内部实现。

startScroll():

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

computeScrollOffset():

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(y * mDeltaY);
                break;
            ......
        }
    }
}

工作原理:先是看到在startScroll()方法中系统中并没有实现View的滑动,而实现View滑动的主要方法是invalidate(),该方法会导致View重绘,在View的draw方法中又会去调用computeScroll()方法,computeScroll()方法需要我们自己来实现,在computeScroll()方法中的工作流程是:当View重绘后会在draw方法中调用computeScroll(),而computeScroll()会去向Scroller获取当前的scrollX和scrollY,接着是通过scrollTo()方法来实现滑动,接着又会调用postInvalidate()来进行第二次重绘,以此往复,直到整个滑动过程的结束

2.动画

动画本身就是一种渐进式的滑动,因此它的使用方法和上面介绍View滑动时的动画的使用方法是一样的。

3.使用延时策略

使用延时的核心思想是通过发送一系列的延时消息来达到一种渐进式的效果,具体来说就是可以使用Handler或者是View的postDelayed方法,也可以使用线程的sleep()方法。总体来说这些方法的思路都是相同的。下面来看一下Handler的实例,其他的方法都是可以举一反三的:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;

private int mCount = 0;

private Handler mHandler = new Handler() {
    public void handleMessage(Message msg){
        switch(msg.what){
            case MESSAGE_SCROLL_TO:{
                mCount++; 
                if(mCount < FRAME_COUNT){  //开始进行延时操作
                    float fraction = mCount / (float) FRAME_COUNT;
                    int scrollX = (int) (fraction * 100);
                    mButton.scrollTo(scrollX,0);
                    mHandler.sendEmptyMesageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                }
                break;
            }
            default:
                 break;

        }
    };
};

注意:在上述代码中时使用Hnadler进行延时操作,即让View的内容在大约1000ms内向左移动100像素,这种方法是无法进行精确电定时的原因是系统消息的调度也是需要时间的,并且所需要的时间是不能够确定的。

四.View事件的分发机制

1.点击事件的传递规则

点击事件的分发,其实就是对MotionEvent的事件的分发过程,即当一个MotionEvent产生以后,系统需要把事件传递给一个具体的View,而这个传递的过程就是分发的过程。点击事件的分发过程由三个很重要的方法来共同完成,下面来具体介绍这三个方法。

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用于进行事件的分发,如果事件能够传递给当前的View,那么此方法一定会被调用。方法的返回结果表示是否消耗当前事件。

  • public boolean onInterceptTouchEvent(MotionEvent ev)
    是在上述方法的内部进行调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中此方法都不会被再次调用,返回的结果表示是否拦截当前的事件(true表示拦截,false表示不拦截)。

  • public boolean onTouchEvent(MotionEvent ev)
    也是在dispatchTouchEvent()方法中进行调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中当前的View无法再接收到事件。

上述三个方法的使用如下所示:

public boolean dispatchTouchEvent(MotionEvent ev){  //开始进行事件的分发
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){  //如果返回的是true,即拦截
        consume = onTouchEvent(ev);  //开始处理点击事件
    }
    else{
        consume = child.dispatchTouchEvent(ev); //如果不拦截则事件会被传递给子元素
    }
    return consume;
}

通过上述的代码,我们可以总结出事件的传递规则:对于一个根ViewGroup来说,点击事件产生以后首先会传递给它,这时它的dispatchTouchEvent()方法就会被调用,在该方法的内部如果onInterceptTouchEvent()返回true,则表示该事件会被拦截,接着事件就会被交给ViewGroup来进行处理;如果返回的是false,就表示该根ViewGroup不会拦截该事件,则点击事件就会被传递给ViewGroup的子元素。

2.点击事件的传递过程

传递顺序:当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View

传递过程:事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级的View,顶级的View接收到事件后就会按照事件的分发机制去分发事件。这里需要注意到一种情况,就是如果事件传递到顶级的View,但它的onTouchEvent()方法返回的是false,则它的父容器的的onTouchEvent()方法会被再次调用,以此类推,如果所有的元素都不处理该事件,则该事件最终会交给Activity来进行处理。

3.有关传递机制的结论

(1) 同一个事件序列是指从手指接触屏幕开始,到手指离开屏幕为止,在这个过程中产生的一系列事件。

(2) 正常情况下一个事件序列只能被一个View所拦截。一旦事件序列中的某个事件被View拦截,则该事件序列中的所有事件都会被交给该View进行处理。

(3) 某个View一旦决定拦截,那么这一个事件序列都只能由它来进行处理。

(4) 某个View一旦开始处理事件,如果它没有消耗ACTION_DOWN事件,即onTouchEvent()返回了false,则同一事件序列中的其他事件都不会交由它来处理,而是会交给它的父元素。

(5) ViewGroup默认不拦截任何事件。

(6) View中没有onInterceptTouchEvent()方法,也就是说一旦点击事件传递给它,那么它的onTouchEvent()方法就会被调用。

(7) 如果View不消耗出ACTION_DOWN以外的其他事件,那么这个点击事件会消失,但是并不会调用父元素的onTouchEvent(),并且View还是会接收到该事件序列的其他事件,只是最终这些消失的点击事件会交给Activity处理。

(8) View的onTouchEvent()默认都是返回true,即都会消耗事件,除非它是不可点击的。

五.View的滑动冲突

如果App界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。下面来介绍有关滑动冲突的一些场景以及相应的解决方法。

1.常见的滑动冲突场景

滑动冲突一般是分为以下三种:

  • 场景1——外部滑动方向和内部滑动方向不一致

  • 场景2——外部滑动方向和内部滑动方向一致

  • 场景3——上述两种的嵌套

2.滑动冲突的处理规则

  • 场景1处理规则
    对于场景1,由于它内外两种滑动的滑动方向不一样,所以我们就可以根据滑动的方向判断谁来拦截事件。关于判断是上下滑动还是左右滑动,可根据滑动的距离或者滑动的角度去判断。

  • 场景2处理规则
    对于场景2,由于它的内外两种滑动的滑动方向是一样的,所以我们一般是从业务上找突破点。即根据业务需求,规定何时让外部View拦截事件何时由内部View拦截事件。

  • 场景3处理规则
    场景3的滑动冲突较为复杂,所以我们同样根据需求在业务上找到突破点。

3.滑动冲突的处理方法

1.外部拦截法

概念:外部拦截法指的是点击事件都先经过父容器拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。使用外部拦截法需要重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可,代码如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    boolean intercept = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:{ //必须不拦截ACTION_DOWN,否则后续的点击事件都会被父容器处理
            intercept = false;
            break;
        }

        case MotionEvent.ACTION_MOVE:{ //根据需要来决定是否拦截
            if(父容器需要当前的点击事件){
                intercept = true;
            }
            else{
                intercept = false;
            }
            break;
        }

        case MotionEvent.ACTION_UP:{ //因为ACTION_UP本身没有太多意义,所以可以不拦截
            intercept = false;
            break;
        }
        
        default:
            break;

    }

    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercept;
}
2.内部拦截法

概念:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器来处理。该方法需要配合requestDisallowInterceptTouchEvent()方法才能正常工作,我们需要重写子元素的dispatchTouchEvent()方法。代码如下:

public boolean dispatchTouchEvent(MotionEvent event){
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:{
            parent.requestDisallowInterceptTouchEvent(true); //不拦截ACTION_DOWN点击事件,交给子元素
            break;
        }

        case MotionEvent.ACTION_MOVE:{
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if(父容器需要此类点击事件){
                parent.requestDisallowInterceptTouchEvent(false); //拦截
            }
            break;
        }

        case MotionEvent.ACTION_UP:{
            break;
        }

        default:
            break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容