Android进阶之光——View体系(View事件分发机制)

View与ViewGroup

View是Android所有控件的基类
ViewGroup是View的组合,ViewGroup可以包含很多View以及ViewGroup,而包含的ViewGroup又可以包含View和ViewGroup


View树

坐标系

Android系统中有两种坐标系:Android坐标系和View坐标系。

Android坐标系

在Android中,将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向


Android坐标系

View坐标系

View坐标系与Android坐标系并不冲突,两者是共同存在的


View坐标系

View获取自身的宽高

width=getRight()-getLeft();
height=getBottom()-getTop();

这样做比较麻烦,因为系统已经向我们提供了获取View宽高的方法:getHeight()、getWidth()

View自身的坐标

  • getTop():获取View自身顶边到其父布局顶边的距离
  • getLeft():获取View自身左边到其父布局左边的距离
  • getRight():获取View自身右边到其父布局左边的距离
  • getBottom():获取View自身底边到其父布局顶边的距离

MotionEvent

  • getX() 获取点击事件距离控件左边的距离,即视图坐标
  • getY() 获取点击事件距离控件顶边的距离,视图坐标
  • getRawX() 获取点击事件距离整个屏幕左边的距离 绝对坐标
  • getRawY() 获取点击事件距离整个屏幕顶边的距离 绝对坐标

View的滑动

在处理View的滑动时,基本思路都是类似的:当点击事件传到View时,系统记下触摸点的坐标,手指移动时记下移动后触摸的坐标并计算偏移量,并通过偏移量来修改View的坐标

layout()方法

View进行绘制的时候会调用onLayout()来设置显示的位置。
我们自定义一个view

  • java代码 CustomView.java
package com.probuing.androidlight.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

public class CustomView extends View {
    private int lastX;
    private int lastY;

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法重新绘制位置
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
        }
        return true;
    }
}
  • 随后在布局文件中引用自定义View
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.ViewLSNActivity">

    <com.probuing.androidlight.view.CustomView
        android:id="@+id/customview"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="50dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

offsetLeftAndRight()和offsetTopAndBottom()

其实也可以用这两种方法来替换layout()方法

  • java代码
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                //调用layout方法重新绘制位置
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
        }
        return true;
    }

LayoutParams(改变布局参数)

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果
因为我们自定义的View的父控件是LinearLayout,所以我们使用了LinearLayout.LayoutParams。

  • java代码
  @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
/*                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);*/
                //调用layout方法重新绘制位置
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);
                break;
        }
        return true;
    }

动画

我们也可以采用View动画来移动

  • res目录创建anim目录 并创建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="1000"
        android:fromXDelta="0"
        android:toXDelta="300" />
</set>

View动画不能改变View的位置参数。但是属性动画可以解决位置问题

   @Override
    protected void onStart() {
        super.onStart();
        ObjectAnimator.ofFloat(customview,"translationX",0,300).setDuration(1000)
                .start();
    }

scrollTo与scrollBy

scrollTo(x,y)表示移动到一个具体的坐标点。而scrollBy(dx,dy)则表示移动的增量为dx、dy。

属性动画

随着Android3.0属性动画的提出,View之前的动画带来的问题,例如响应事件位置依然在动画发生前的地方,不具备交互性等也随之解决。

ObjectAnimator

ObjectAnimator是属性动画最重要的类,创建一个ObjectAnimator只需要通过其静态工厂类直接返还一个ObjectAnimator对象。参数包括一个对象和对象的属性名字,这个属性必须有get和set方法

ObjectAnimator.ofFloat(customview,"translationX",0,300)
        .setDuration(1000)
        .start();

下面就是一些常用的可以直接使用的属性动画的属性值

  • translationX和translationY:用来沿着X轴或Y轴进行平移
  • rotation、rotationX、rotationY:用来围绕View的支点进行旋转
  • PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。
  • alpha:透明度,默认是1,0是代表完全透明
  • x和y:描述View对象在其容器中的最终位置

ValueAnimator

ValueAnimator不提供任何动画效果,它是一个数值发生器,用来产生一定的有规律的数字。

动画的监听

完整的动画具有start、repeat、end、cancel这4个过程

 @Override
    protected void onStart() {
        super.onStart();
        ObjectAnimator translationX = ObjectAnimator.ofFloat(customview, "translationX", 0, 300).setDuration(1000);
        translationX.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                
            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        translationX.start();
    }

一般情况下 我们比较常用的是onAnimationEnd事件,Android也提供了AnimatorListenterAdapter来让我们选择必要的事件进行监听

 @Override
    protected void onStart() {
        super.onStart();
        ObjectAnimator translationX = ObjectAnimator.ofFloat(customview, "translationX", 0, 300).setDuration(1000);
        translationX.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                Toast.makeText(ViewLSNActivity.this, "end", Toast.LENGTH_SHORT).show();
            }
        });
        translationX.start();
    }

组合动画——AnimatorSet

AnimatorSet类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例,每次调用方法时都会返回Builder自身用于构建

  • after(Animator anim)将现有动画插入到传入的动画后执行
  • after(long delay)将现有动画延迟指定毫秒后执行
  • before(Animator anim)将现有动画插入到传入的动画之前执行
  • with(Animator anim)将现有动画和传入的动画同时执行
    private void animBuilder() {
        ObjectAnimator animator1 = ObjectAnimator.ofFloat(customview, "translationX", 0.0f, 200.0f, 0f);
        ObjectAnimator animator2 = ObjectAnimator.ofFloat(customview, "scaleX", 1.0f, 2.0f);
        ObjectAnimator animator3 = ObjectAnimator.ofFloat(customview, "rotationX", 0.0f, 90.0f, 0.0f);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(3000);
        animatorSet.play(animator1).with(animator2).after(animator3);
        animatorSet.start();
    }

组合动画——PropertyValuesHolder

除了使用AnimatorSet类之外,还可以使用PropertyValuesHolder类来实现组合动画。使用PropertyValuesHolder类只能是多个动画一起执行。使用PropertyValuesHolder只能是多个动画一起执行。得结合ObjectAnimator.ofPropertyValuesHolder()

   private void propertyValuesHolder() {
        PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.5f);
        PropertyValuesHolder valueHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 90.0f, 0.0f);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(customview, valuesHolder1, valueHolder2);
        objectAnimator.setDuration(2000).start();
    }

在XML中使用属性动画

在res中新建animator目录(属性动画必须放在animator目录下),新建scale.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000"
    android:propertyName="scaleX"
    android:valueFrom="1.0"
    android:valueTo="2.0"
    android:valueType="floatType"
    >
</objectAnimator>

在程序中引用XML定义得属性动画

  private void startXMLAnimator() {
        Animator animator = AnimatorInflater.loadAnimator(this, R.animator.scale);
        animator.setTarget(customview);
        animator.start();
    }

View的事件分发机制

先来看看Activity组成

Activity的构成

一个Activity包含一个Window对象,这个对象是由PhoneWindow实现的。PhoneWindow将DecorView作为整个应用窗口的根View。而这个DecorView又将屏幕划分为两个区域:一个是TitleView另一个是ContentView。我们平常做应用所写的布局就是展示在ContentView中的

解析View的事件分发机制

当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent,而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级。MotionEvent在View中的层级传递过程就是点击事件的分发
点击事件有3个重要的方法

  • dispatchTouchEvent(MotionEvent ev):用于事件的分发
  • onInterceptTouchEvent(MotionEvent ev):用于事件的拦截,在dispatchTouchEvent中调用
  • onTouchEvent(MotionEvent ev):用来处理点击事件,在dispatchTouchEvent()方法中进行调用

View的事件分发机制

当点击事件产生后,事件首先会传递给当前的Activity,这会调用Activity的dispatchTouchEvent()方法(也就是交由Activity中的PhoneWindow来完成,然后PhoneWindow再把事件处理工作交给DecorView,然后再由DecorView将事件处理工作交给根ViewGroup)

注意:一个完整的事件的序列是以DOWN开始以UP结束

  • 如果ViewGroup要拦截事件的时候,那么后续的事件序列都会交给它处理,而不用再调用onInterceptTouchEvent()方法了。

点击事件分发的传递规则

伪代码表示

public boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if(onInterceptTouchEvent(ev)){
           result=super.onTouchEvent(ev)
  }else{
          result=child.dispatchTouchEvent(ev)
}
return result;
}

事件自上而下传递过程

当点击事件产生后会由Activity来处理,传递给PhoneWindow,再传递给DecorView,最后传递给顶层的ViewGroup。
对于根ViewGroup,点击事件首先传递给它的dispatchTouchEvent(),该ViewGroup的onInterceptTouchEvent()

  • 如果返回true,则表示要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理
  • 如果返回false,则表示不拦截这个事件,这个事件会交给子元素的dispatchTouchEvent()处理
    如此反复下去,如果传递给底层的View,View是没有子View的,就会调用这个View的dispathTouchEvent()方法,一般最终会调用View的onTouchEvent()

事件自下而上传递过程

当点击事件传递给底层的View时,如果底层的View的onTouchEvent()方法返回true,则表示事件由底层的View消耗并处理。
如果返回false则表示该View不做处理,事件会传递给父View的onTouchEvent()处理,如果父View的onTouchEvent()返回false表示父View也不处理,则继续传递给该父View的父View处理,如此反复


事件分发机制
  • Activity
    • 没有onInterceptTouchEvent方法
    • 只有dispatchTouchEvent、onTouchEvent方法
  • ViewGroup
    • 有 onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent方法
  • View
    • 没有 onInterceptTouchEvent方法

事件分发流程

Activity

dispatchTouchEvent:

  • 返回值true/false:事件由自己消费
  • 返回值super:交由子ViewGroup的dispatchTouchEvent处理

ViewGroup

dispatchTouchEvent:
  • 返回值true:事件由自己消费
  • 返回值false:交由父View的onTouchEvent()处理
  • 返回值super:传递给自己的onInterceptTouchEvent()进行分发
onInterceptTouchEvent:
  • 返回值true:表示拦截事件,交由自己的onTouchEvent处理
  • 返回值false/super:表示不拦截事件,交由子View的dispatchTouchEvent()处理
onTouchEvent:
  • 返回值true:表示事件自己处理
  • 返回值false/super:将事件交由父onTouchEvent处理

View

dispatchTouchEvent:
  • 返回值为true:事件由自己消费
  • 返回值为false:事件交由父View的onTouchEvent处理
  • 返回值为super:交由自己的onTouchEvent处理
onTouchEvent
  • 返回值true:事件自己消费
  • 返回值false、super:事件交由父view的onTouchEvent处理,直至传递到Activity的onTouchEvent()

OnTouchListener和onClickListener执行顺序

当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener中的OnTouch会被回调。

  • 如果onTouch返回false,则当前View的onTouchEvent方法会被调用
  • 如果onTouch返回true,那么onTouchEvent方法将不会调用
    由此可看出onTouchListener要比onTouchEvent优先级高
    在onTouchEvent方法中,如果当前设置的有onClickListener那么onClick就会被调用,由此可以看出onClick的优先级最低,处于事件传递的尾端

onTouch->onTouchListener->onTouchEvent->onClick->onClickListener

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

推荐阅读更多精彩内容