View体系之View的位置与事件

144
作者 MeloDev
2017.02.07 21:44 字数 1768

View体系之View的位置与事件

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

写在前面:
最近完成了开发任务,接下来工作上做一些优化和修修补补的工作就可以了,所以难得有一些完整的时间来巩固知识。我们知道基本上 RecyclerView 每个人都有接触过,但是看过源码或者理解原理的并不多,以前我们用 ListView,包括后来又出现了的 CoordinatorLayout 来完成复杂炫酷的联动效果,ConstraintLayout 来给子 View 之间添加约束。完成这些高大上功能的都是自定义 View,所以真正掌握理解自定义 View,几乎成了 Android 开发者的必备技能。所以我也通过看书和官方文档,来学习巩固这里的知识,整理成系列的文章,方便记忆和交流。

View 的概念

View 是什么?我理解 View 有两层含义。首先 View 是 Android 所有视图中顶层的基类,是一个抽象的概念。其次 View 也可以特指某一个不再可以有子 View 的 View。

如果第一次理解起来可能不太容易,不过我画了一张图,应该好理解多了。


View 树状结构

首先顶层的 View 是一个抽象概念,无论 ViewGroup(视图组,比如 RelativeLayout)还是一个具体的 View(Button TextView),他们都继承自 View。而 ViewGroup 本身可以包含很多个 ViewGroup 和 View。

View 的位置

当一个 View 摆在屏幕上时,你能想到它最基本有哪些属性?对了,就是它自身的大小和位置。

View 本身提供了一些 get set 方法,让我们获得它的成员变量,其中就包括位置的信息。

来写一个 Demo 更好的理解。

布局文件如下,非常简单:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="melo.leja.com.viewdemo.MainActivity">

    <melo.leja.com.viewdemo.DemoRelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp">

        <Button
            android:id="@+id/bt_view_demo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:text="Hello World!" />

    </melo.leja.com.viewdemo.DemoRelativeLayout>
</RelativeLayout>

MainActivity:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            initView();
        }
    }

    private void initView() {
        int top = btViewDemo.getTop();
        int left = btViewDemo.getLeft();
        int bottom = btViewDemo.getBottom();
        int right = btViewDemo.getRight();
        //在 View 平移过程中,这几个参数并不改变(原始值)
        Log.e(TAG, "top:" + top + ",left:" + left + ",bottom:" + bottom + ",right:" + right);

        float x = btViewDemo.getX();
        float y = btViewDemo.getY();
        //平移过程中此参数发生变化
        Log.e(TAG, "x:" + x + ",y:" + y);


        float translationX = btViewDemo.getTranslationX();
        float translationY = btViewDemo.getTranslationY();

        Log.e(TAG, "translationX:" + translationX + ",translationY:" + translationY);

        //在 MotionEvent 之中 getRawX - getLeft = getX

        int scaledTouchSlop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
        Log.e(TAG, "scaledTouchSlop:" + scaledTouchSlop);
    }

首先有个小坑,这些 View 的属性onWindowFocusChanged方法中才能获取到,在oncreate onStart onResume 获取不到。具体原因未来再解释。

首先获取位置信息的方法分为几组:

getTop getLeft() getBottom() getRight()

它们分别可以获取 View 原始的 上边距纵坐标、左边距横坐标、下边距纵坐标、右边距横坐标。

getX() getY()
它们获取的是 View 左上角的坐标。

getTranslationX() getTranslationY()
它们获取的是 View 左上角的偏移量,默认为 0。

用图理解:


这里写图片描述

首先 Android 的坐标系是以右下为正方向的。假设 View 向右下角移动了一点。 top bottom left right 这几个属性是 View 的原始属性,并不会因为平移和发生改变,在 x 方向上平移了 translationX 和 y 方向平移了 translationY 大小之后,这个时候 x,y 各自的数值如图所示。

也就是说:
getLeft() + getTranslationX() = getX()

来看看打印的 log 吧:


log

因为 Button 父 View 为一个自定义的 RelativeLayout,当我改变这个自定义 RelativeLayout 的 margin 值时,打印出来的任何值都没有发生改变。所以我想说明的是,上述成员变量的意义都是:

相对于父布局的大小,并不是一个绝对值。

另外相信你也肯定能知道,这个 View 自身的

width = getRight - getLeft
height = getBottom - getTop

MotionEvent

关于 MotionEvent,先来看看官方文档的解释:

Object used to report movement (mouse, pen, finger, trackball) events. Motion events may hold either absolute or relative movements and other data, depending on the type of device.

其意思就是 MotionEvent 是用来报告运动事件的载体,在 Android 设备中,一般 MotionEvent 携带有事件类型、事件的坐标、事件的时间等等。

我们在未来会讨论的事件分发中,传递的就是 MotionEvent。

我们给 Button set 一个 onTouchListener,来看看 MotionEvent 里面都有什么。

        btViewDemo.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                float x = motionEvent.getX();
                float y = motionEvent.getY();
                float rawX = motionEvent.getRawX();
                float rawY = motionEvent.getRawY();
                int action = MotionEventCompat.getActionMasked(motionEvent);
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, "ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, "ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, "ACTION_UP");
                        break;
                }
                Log.e(TAG, "event.getX:" + x + ",getRawX:" + rawX + ",event.getY:" + y + ",getRawY:" + rawY);
                return false;

这里依然有两组参数,getX/getRawX,这两个又代表什么意思呢?


getX,getRawX

在 MotionEvent 携带的事件信息中,getX/getY 指的是相对于当前 View 左上角而言的,getRawX/getRawY 是相对于屏幕边距的。raw 这个单词是 生的 未加工的意思,这样一来,就好理解多了。

再来说 MotionEvent 的中事件的类型,Demo 中我只列举出来了三个,当我手指按下 --> 滑动 --> 抬起时,MotionEvent 中的事件会一次变为 一个 ACTION_DOWN 多个 ACTION_MOVE 和一个 ACTION_UP。

当然关于事件类型还有很多,大家可以去查阅官方文档,这里不多做解释,未来需要再补充吧!

TouchSlop

TouchSlop 这个值是设备认为滑动的最小距离,如果滑动的距离小于 TouchSlop 的值,则被认为没有滑动。

这个值是于设备有关的,不同的设备 TouchSlop 的值也许是不一样的。

这个值的获取方法:

        int scaledTouchSlop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
        Log.e(TAG, "scaledTouchSlop:" + scaledTouchSlop);

VelocityTracker

VelocityTracker 这个类是用来追踪手指的速度的,我们可以在重写 View 的 onTouchEvent 方法来使用它:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        boolean consume = mGestureDetector.onTouchEvent(event);
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                velocityTracker.computeCurrentVelocity(1 * 1000);
                float xVelocity = velocityTracker.getXVelocity();
                float yVelocity = velocityTracker.getYVelocity();
                Log.e(TAG, "xVelocity:" + xVelocity + ",yVelocity:" + yVelocity);
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.clear();
                velocityTracker.recycle();
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

通过 addMovement(event) 方法将 MotionEvent 传入,类的内部会进行一些运算,这里我们不用关心。然后 computeCurrentVelocity 传入一个时间间隔,单位为毫秒。这个的意思就是在这个时间间隔内,移动的像素值,就是得到的速度。注意速度是有正负的,这与我们高中学的物理概念相同。

注意在不使用的时候调用 velocityTracker.clear(); velocityTracker.recycle(); 进行资源回收。

GestureDetector

手势检测,作用就是检测用户的单击、双击、长按、滑动、快速滑动等等。使用方法和刚才的速度检测类是一样的,都需要将 MotionEvent 传入。

    public DemoRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mGestureDetector = new GestureDetector(this);
        mGestureDetector.setIsLongpressEnabled(false);
    }
public class DemoRelativeLayout extends RelativeLayout implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener

当然自定义 View 还要实现这两个接口来实现回调。

在 onTouchEvent 中调用 boolean consume = mGestureDetector.onTouchEvent(event);(代码在上方)

我把 MotionEvent 对象传给了 GestureDetector,由 GestureDetector 内部去做运算分析,我究竟做了哪些手势。说到这里你也就明白了,无论是速度还是手势识别,其实就是拿着 MotionEvent 中的事件信息去进行分析,相当于一个辅助工具类就对了。来看看这些回调方法吧。

    @Override
    public boolean onDown(MotionEvent motionEvent) {
        // 按下
        Log.e(TAG, "onDown");
        return false;
    }

    @Override
    public void onShowPress(MotionEvent motionEvent) {
        // 按住不松手
        Log.e(TAG, "onShowPress");
    }

    @Override
    public boolean onSingleTapUp(MotionEvent motionEvent) {
        // 单击
        Log.e(TAG, "onSingleTapUp");
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
        // 滑动
        Log.e(TAG, "onScroll");
        return false;
    }

    @Override
    public void onLongPress(MotionEvent motionEvent) {
        // 长按
        Log.e(TAG, "onLongPress");
    }

    @Override
    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
        // 快速滑动
        Log.e(TAG, "onFling");
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
        // 严格的单击
        Log.e(TAG, "onSingleTapConfirmed");
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent motionEvent) {
        // 双击
        Log.e(TAG, "onDoubleTap");
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent motionEvent) {
        // 双击
        Log.e(TAG, "onDoubleTapEvent");
        return false;
    }

还是挺多的,做了一下简单地解释,如果需要深入了解,那就查阅下官方文档吧!

本文到这里的内容已经完成了,下文中会研究讲解 View 的滑动,以及最后还有重头戏,也就是 View 的事件分发,敬请期待吧~

Android黑板报