View体系之View的位置与事件

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 的事件分发,敬请期待吧~

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

推荐阅读更多精彩内容