View與ViewGroup
View是Android所有控件的基類
ViewGroup是View的組合,ViewGroup可以包含很多View以及ViewGroup,而包含的ViewGroup又可以包含View和ViewGroup
坐標系
Android系統(tǒng)中有兩種坐標系:Android坐標系和View坐標系。
Android坐標系
在Android中,將屏幕左上角的頂點作為Android坐標系的原點,這個原點向右是X軸正方向,向下是Y軸正方向
View坐標系
View坐標系與Android坐標系并不沖突,兩者是共同存在的
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包含一個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處理,如此反復
- 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