Android進階之光——View體系(View事件分發(fā)機制)

View與ViewGroup

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


View樹

坐標系

Android系統(tǒng)中有兩種坐標系:Android坐標系和View坐標系。

Android坐標系

在Android中,將屏幕左上角的頂點作為Android坐標系的原點,這個原點向右是X軸正方向,向下是Y軸正方向


Android坐標系

View坐標系

View坐標系與Android坐標系并不沖突,兩者是共同存在的


View坐標系

View獲取自身的寬高

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

這樣做比較麻煩,因為系統(tǒng)已經向我們提供了獲取View寬高的方法:getHeight()、getWidth()

View自身的坐標

  • getTop():獲取View自身頂邊到其父布局頂邊的距離
  • getLeft():獲取View自身左邊到其父布局左邊的距離
  • getRight():獲取View自身右邊到其父布局左邊的距離
  • getBottom():獲取View自身底邊到其父布局頂邊的距離

MotionEvent

  • getX() 獲取點擊事件距離控件左邊的距離,即視圖坐標
  • getY() 獲取點擊事件距離控件頂邊的距離,視圖坐標
  • getRawX() 獲取點擊事件距離整個屏幕左邊的距離 絕對坐標
  • getRawY() 獲取點擊事件距離整個屏幕頂邊的距離 絕對坐標

View的滑動

在處理View的滑動時,基本思路都是類似的:當點擊事件傳到View時,系統(tǒng)記下觸摸點的坐標,手指移動時記下移動后觸摸的坐標并計算偏移量,并通過偏移量來修改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目錄創(chuàng)建anim目錄 并創(chuàng)建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之前的動畫帶來的問題,例如響應事件位置依然在動畫發(fā)生前的地方,不具備交互性等也隨之解決。

ObjectAnimator

ObjectAnimator是屬性動畫最重要的類,創(chuàng)建一個ObjectAnimator只需要通過其靜態(tài)工廠類直接返還一個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不提供任何動畫效果,它是一個數值發(fā)生器,用來產生一定的有規(guī)律的數字。

動畫的監(jiān)聽

完整的動畫具有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來讓我們選擇必要的事件進行監(jiān)聽

 @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)將現有動畫插入到傳入的動畫后執(zhí)行
  • after(long delay)將現有動畫延遲指定毫秒后執(zhí)行
  • before(Animator anim)將現有動畫插入到傳入的動畫之前執(zhí)行
  • with(Animator anim)將現有動畫和傳入的動畫同時執(zhí)行
    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類只能是多個動畫一起執(zhí)行。使用PropertyValuesHolder只能是多個動畫一起執(zhí)行。得結合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的事件分發(fā)機制

先來看看Activity組成

Activity的構成

一個Activity包含一個Window對象,這個對象是由PhoneWindow實現的。PhoneWindow將DecorView作為整個應用窗口的根View。而這個DecorView又將屏幕劃分為兩個區(qū)域:一個是TitleView另一個是ContentView。我們平常做應用所寫的布局就是展示在ContentView中的

解析View的事件分發(fā)機制

當我們點擊屏幕時,就產生了點擊事件,這個事件被封裝成了一個類:MotionEvent,而當這個MotionEvent產生后,那么系統(tǒng)就會將這個MotionEvent傳遞給View的層級。MotionEvent在View中的層級傳遞過程就是點擊事件的分發(fā)
點擊事件有3個重要的方法

  • dispatchTouchEvent(MotionEvent ev):用于事件的分發(fā)
  • onInterceptTouchEvent(MotionEvent ev):用于事件的攔截,在dispatchTouchEvent中調用
  • onTouchEvent(MotionEvent ev):用來處理點擊事件,在dispatchTouchEvent()方法中進行調用

View的事件分發(fā)機制

當點擊事件產生后,事件首先會傳遞給當前的Activity,這會調用Activity的dispatchTouchEvent()方法(也就是交由Activity中的PhoneWindow來完成,然后PhoneWindow再把事件處理工作交給DecorView,然后再由DecorView將事件處理工作交給根ViewGroup)

注意:一個完整的事件的序列是以DOWN開始以UP結束

  • 如果ViewGroup要攔截事件的時候,那么后續(xù)的事件序列都會交給它處理,而不用再調用onInterceptTouchEvent()方法了。

點擊事件分發(fā)的傳遞規(guī)則

偽代碼表示

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也不處理,則繼續(xù)傳遞給該父View的父View處理,如此反復


事件分發(fā)機制
  • Activity
    • 沒有onInterceptTouchEvent方法
    • 只有dispatchTouchEvent、onTouchEvent方法
  • ViewGroup
    • 有 onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent方法
  • View
    • 沒有 onInterceptTouchEvent方法

事件分發(fā)流程

Activity

dispatchTouchEvent:

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

ViewGroup

dispatchTouchEvent:
  • 返回值true:事件由自己消費
  • 返回值false:交由父View的onTouchEvent()處理
  • 返回值super:傳遞給自己的onInterceptTouchEvent()進行分發(fā)
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執(zhí)行順序

當一個View需要處理事件時,如果設置了OnTouchListener,那么OnTouchListener中的OnTouch會被回調。

  • 如果onTouch返回false,則當前View的onTouchEvent方法會被調用
  • 如果onTouch返回true,那么onTouchEvent方法將不會調用
    由此可看出onTouchListener要比onTouchEvent優(yōu)先級高
    在onTouchEvent方法中,如果當前設置的有onClickListener那么onClick就會被調用,由此可以看出onClick的優(yōu)先級最低,處于事件傳遞的尾端

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容