读书笔记——《Android进阶之光》

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

2021-08-20  本文已影响0人  So_ProbuING

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自身的坐标

MotionEvent

View的滑动

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

layout()方法

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

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;
    }
}
<?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()方法

 @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。

  @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动画来移动

<?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();

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

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自身用于构建

    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个重要的方法

View的事件分发机制

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

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

点击事件分发的传递规则

伪代码表示

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()

事件自下而上传递过程

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


事件分发机制

事件分发流程

Activity

dispatchTouchEvent:

ViewGroup

dispatchTouchEvent:
onInterceptTouchEvent:
onTouchEvent:

View

dispatchTouchEvent:
onTouchEvent

OnTouchListener和onClickListener执行顺序

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

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

上一篇下一篇

猜你喜欢

热点阅读