第四章 自定義組件、動畫

文章目錄

第四章 自定義組件、動畫

(一)View體系

(1)View簡介

View是Android所有控件的基類,同時ViewGroup也是繼承自View。我們常用的這些控件都是繼承於View。
在這裏插入圖片描述

(2)Android座標系

Android中有兩種座標系,分別爲Android座標系和視圖座標系,首先我們先來看看Android座標系。
在Android中,將屏幕的左上角的頂點作爲Android座標系的原點,這個原點向右是X軸正方向,原點向下是Y軸正方向。
MotionEvent提供的getRawX()和getRawY()獲取的座標都是Android座標系的座標。
在這裏插入圖片描述

(3)視圖座標系

在這裏插入圖片描述
View獲取自身寬高
getHeight():獲取View自身高度
getWidth():獲取View自身寬度
View自身座標
通過如下方法可以獲得View到其父控件(ViewGroup)的距離:

getTop():獲取View自身頂邊到其父佈局頂邊的距離
getLeft():獲取View自身左邊到其父佈局左邊的距離
getRight():獲取View自身右邊到其父佈局左邊的距離
getBottom():獲取View自身底邊到其父佈局頂邊的距離
MotionEvent提供的方法
我們看上圖那個深藍色的點,假設就是我們觸摸的點,我們知道無論是View還是ViewGroup,最終的點擊事件都會由onTouchEvent(MotionEvent event)方法來處理,MotionEvent也提供了各種獲取焦點座標的方法:

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

(二)自定義View

(1)onMeasure:對當前View的尺寸進行測量

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

widthMeasureSpec和heightMeasureSpec包含測量模式和尺寸大小

int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

尺寸大小:wrap_content、match_parent以及指定固定尺寸
測量模式:UNSPECIFIED,EXACTLY,AT_MOST
在這裏插入圖片描述

(2)重寫onMeasure

private int getMySize(int defaultSize, int measureSpec) {
    int mySize = defaultSize;

    int mode = MeasureSpec.getMode(measureSpec);
    int size = MeasureSpec.getSize(measureSpec);

    switch (mode) {
        case MeasureSpec.UNSPECIFIED: {//如果沒有指定大小,就設置爲默認大小
            mySize = defaultSize;
            break;
        }
        case MeasureSpec.AT_MOST: {//如果測量模式是最大取值爲size,則大小取最大值
            mySize = size;
            break;
        }
        case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改變它
            mySize = size;
            break;
        }
    }
    return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);//獲取組件寬、高信息
    int width = getMySize(defalutSize,widthMeasureSpec);
    int height = getMySize(defalutSize,heightMeasureSpec);//自定義默認寬高情況

    if(width<height)height=width;
    else width=height;

    setMeasuredDimension(width,height);//自定義組件,用來決定組件大小
}

(3)重寫onDraw:繪製當前View

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);//調用父View的onDraw函數,因爲View這個類幫我們實現了一些基本的而繪製功能,比如繪製背景顏色、背景圖片等
    int r = getMeasuredHeight() / 2;//半徑
    int centerX = getLeft() + r;//圓心的橫座標爲當前的View的左邊起始位置+半徑
    int centerY = getTop() + r; //圓心的縱座標爲當前的View的頂部起始位置+半徑
    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    //開始繪製
    canvas.drawCircle(centerX,centerY,r,paint);
}

(4)自定義佈局屬性

res/attrs.xml

<!--屬性集合名,一般與View名稱相同-->
<declare-styleable name="MyCircleView">
    <!--屬性名爲default_size,取值類型爲尺寸類型(dp,px等)-->
    <attr name="default_size" format="dimension"/>
</declare-styleable>
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.sdu.chy.chytest.myView.myViewUtils.MyCircleView
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@color/Orange"
    app:default_size="200dp"/>
public class MyCircleView extends View {
    int defalutSize = 0;
public MyCircleView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    //第二個參數就是我們在styles.xml文件中的<declare-styleable>標籤,即屬性集合的標籤,在R文件中名稱爲R.styleable+name
    TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyCircleView);
    //第一個參數爲屬性集合裏面的屬性,R文件名稱:R.styleable+屬性集合名稱+下劃線+屬性名稱
    //第二個參數爲,如果沒有設置這個屬性,則設置的默認的值
    defalutSize = a.getDimensionPixelSize(R.styleable.MyCircleView_default_size, 0);

    //最後記得將TypedArray對象回收
    a.recycle();
}

(三)自定義ViewGroup

具體實例:將子View按從上到下垂直順序一個挨着一個擺放,模仿實現LinearLayout垂直佈局

1、知道各個子View大小並根據子View大小得到ViewGroup大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //1.將所有的子View進行測量,這會觸發每個子View的onMeasure函數,與measureChild區分,measureChild是對單個view進行測量
    //調用這個函數後,能獲得後面每個子View的測量值(必加方法)
    measureChildren(widthMeasureSpec,heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int childCount = getChildCount();

    //2.根據子View的大小,及ViewGroup的大小,決定當前ViewGroup大小
    if(childCount==0){
        setMeasuredDimension(0,0);
    }else{
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            //如果高度和寬度均爲wrap_content,則寬度爲子View中最大寬度,高度爲所有子View高度和
            setMeasuredDimension(getMaxChildWidth(),getTotalHeight());
        }else if (widthMode == MeasureSpec.AT_MOST){
            //寬度是wrap_content,則設置寬度爲子View最大寬度,高度爲測量值
            setMeasuredDimension(getMaxChildWidth(),heightSize);
        }else if (heightMode == MeasureSpec.AT_MOST){
            //高度是wrap_content,則設置高度爲所有子View高度,寬度爲測量值
            setMeasuredDimension(widthSize,getTotalHeight());
        }else{
            setMeasuredDimension(widthSize,heightSize);
        }
    }
}

private int getMaxChildWidth(){
    //經過measureChildren(widthMeasureSpec,heightMeasureSpec);已經得到子View的測量值,可設置子View尺寸
    int childCount = getChildCount();
    int maxWidth = 0;
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        if(childView.getMeasuredWidth() > maxWidth){
            maxWidth = childView.getMeasuredWidth();
        }
    }
    return maxWidth;
}

private int getTotalHeight(){
    int childCount = getChildCount();
    int totalHeight = 0;
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        totalHeight += childView.getMeasuredHeight();
    }
    return totalHeight;
}

2、根據View與ViewGroup大小進行佈局

//@param changed 該參數指出當前ViewGroup的尺寸或者位置是否發生了改變
//@param left top right bottom 當前ViewGroup相對於其父控件的座標位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //對子View在ViewGroup的佈局進行管理(如何擺放?)
    int childCount = getChildCount();
    //記錄當前高度
    int currentHeight = 0;
    //將子View逐個擺放
    for(int i=0;i<childCount;i++){
        View childView = getChildAt(i);
        int height = childView.getMeasuredHeight();
        int width = childView.getMeasuredWidth();
        //擺放子View,參數分別是子View矩形區域的左、上、右、下
        childView.layout(left,currentHeight,left+width,currentHeight+height);
        currentHeight += height;
    }
}

3、佈局

<com.sdu.chy.chytest.myView.myViewUtils.MyViewGroup
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@color/Yellow">
    <Button
    android:layout_width="100dp"
    android:layout_height="200dp"
    android:background="@color/Orange"/>
    <Button
        android:layout_width="20dp"
        android:layout_height="150dp"
        android:background="@color/Orange"/>
    <Button
        android:layout_width="150dp"
        android:layout_height="20dp"
        android:background="@color/Orange"/>
</com.sdu.chy.chytest.myView.myViewUtils.MyViewGroup>

(四)Activity頁面加載流程

在這裏插入圖片描述

(1) Window

Window即窗口,這個概念在Android Framework中的實現爲android.view.Window這個抽象類,這個抽象類是對Android系統中的窗口的抽象。窗口是一個宏觀的思想,它是屏幕上用於繪製各種UI元素及響應用戶輸入事件的一個矩形區域。通常具備以下兩個特點:
(1)獨立繪製,不與其它界面相互影響;
(2)不會觸發其它界面的輸入事件;
在Android系統中,窗口是獨佔一個Surface實例的顯示區域,每個窗口的Surface由WindowManagerService分配。我們可以把Surface看作一塊畫布,應用可以通過Canvas或OpenGL在其上面作畫。畫好之後,通過SurfaceFlinger將多塊Surface按照特定的順序(即Z-order)進行混合,而後輸出到FrameBuffer中,這樣用戶界面就得以顯示。
android.view.Window這個抽象類可以看做Android中對窗口這一宏觀概念所做的約定,而PhoneWindow這個類是Framework爲我們提供的Android窗口概念的具體實現。接下來我們先來介紹一下android.view.Window這個抽象類。
這個抽象類包含了三個核心組件:

WindowManager.LayoutParams: 窗口的佈局參數;
Callback: 窗口的回調接口,通常由Activity實現;
ViewTree: 窗口所承載的控件樹。

下面我們來看一下Android中Window的具體實現(也是唯一實現)——PhoneWindow。
PhoneWindow這個類是Framework爲我們提供的Android窗口的具體實現。我們平時調用setContentView()方法設置Activity的用戶界面時,實際上就完成了對所關聯的PhoneWindow的ViewTree的設置。我們還可以通過Activity類的requestWindowFeature()方法來定製Activity關聯PhoneWindow的外觀,這個方法實際上做的是把我們所請求的窗口外觀特性存儲到了PhoneWindow的mFeatures成員中,在窗口繪製階段生成外觀模板時,會根據mFeatures的值繪製特定外觀。

(2) setContentView()

這個方法只是完成了Activity的ContentView的創建,而並沒有執行View的繪製流程。
調用的setContentView()方法是Activity類的,源碼如下:

  public void setContentView(@LayoutRes int layoutResID) {
      getWindow().setContentView(layoutResID);    
. . .
  }

getWindow()方法會返回Activity所關聯的PhoneWindow,也就是說,實際上調用到了PhoneWindow的setContentView()方法,源碼如下:

  @Override
  public void setContentView(int layoutResID) {
      if (mContentParent == null) {
          // mContentParent即爲上面提到的ContentView的父容器,若爲空則調用installDecor()生成
          installDecor();
      } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
          // 具有FEATURE_CONTENT_TRANSITIONS特性表示開啓了Transition
          // mContentParent不爲null,則移除decorView的所有子View
          mContentParent.removeAllViews();
      }
      if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
          // 開啓了Transition,做相應的處理,我們不討論這種情況
          // 感興趣的同學可以參考源碼
  . . .
      } else {
          // 一般情況會來到這裏,調用mLayoutInflater.inflate()方法來填充佈局
          // 填充佈局也就是把我們設置的ContentView加入到mContentParent中
          mLayoutInflater.inflate(layoutResID, mContentParent);
      }
. . .
      // cb即爲該Window所關聯的Activity
      final Callback cb = getCallback();
      if (cb != null && !isDestroyed()) {
          // 調用onContentChanged()回調方法通知Activity窗口內容發生了改變
          cb.onContentChanged();
      }

. . .
  }

(3) LayoutInflater.inflate()

PhoneWindow的setContentView()方法中調用了LayoutInflater的inflate()方法來填充佈局,這個方法的源碼如下:

  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
      return inflate(resource, root, root != null);
  }

  public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
      final Resources res = getContext().getResources();
. . .
      final XmlResourceParser parser = res.getLayout(resource);
      try {
          return inflate(parser, root, attachToRoot);
      } finally {
          parser.close();
      }
  }

在PhoneWindow的setContentView()方法中傳入了decorView作爲LayoutInflater.inflate()的root參數,我們可以看到,通過層層調用,最終調用的是inflate(XmlPullParser, ViewGroup, boolean)方法來填充佈局。這個方法的源碼如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
. . .
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        try {
            // Look for the root node.
            int type;
            // 一直讀取xml文件,直到遇到開始標記
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
            // 最先遇到的不是開始標記,報錯
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()+ ": No start tag found!");
            }
            final String name = parser.getName();
  . . .
// 單獨處理<merge>標籤,不熟悉的同學請參考官方文檔的說明
            if (TAG_MERGE.equals(name)) {
// 若包含<merge>標籤,父容器(即root參數)不可爲空且attachRoot須爲true,否則報錯
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                // 遞歸地填充佈局
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // temp爲xml佈局文件的根View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                if (root != null) {
      . . .
                    // 獲取父容器的佈局參數(LayoutParams)
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
// 若attachToRoot參數爲false,則我們只會將父容器的佈局參數設置給根View
                        temp.setLayoutParams(params);
                    }
                }
// 遞歸加載根View的所有子View
                rInflateChildren(parser, temp, attrs, true);
    . . .
                if (root != null && attachToRoot) {
// 若父容器不爲空且attachToRoot爲true,則將父容器作爲根View的父View包裹上來
                    root.addView(temp, params);
                }
                // 若root爲空或是attachToRoot爲false,則以根View作爲返回值
                if (root == null || !attachToRoot) {
                    result = temp;
                }
        return result;
    }
}

在上面的源碼中,首先對於佈局文件中的標籤進行單獨處理,調用rInflate()方法來遞歸填充佈局。這個方法的源碼如下:

void rInflate(XmlPullParser parser, View parent, Context context,
              AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 獲取當前標記的深度,根標記的深度爲0
    final int depth = parser.getDepth();
    int type;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        // 不是開始標記則繼續下一次迭代
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        final String name = parser.getName();
        // 對一些特殊標記做單獨處理
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            // 對<include>做處理
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 對一般標記的處理
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
            // 遞歸地加載子View
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

我們可以看到,上面的inflate()和rInflate()方法中都調用了rInflateChildren()方法,這個方法的源碼如下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

到這裏,setContentView()的整體執行流程我們就分析完了,至此我們已經完成了Activity的ContentView的創建與設置工作。接下來開始View的繪製。
ViewRoot
在介紹View的繪製前,首先我們需要知道是誰負責執行View繪製的整個流程。實際上,View的繪製是由ViewRoot來負責的。每個應用程序窗口的decorView都有一個與之關聯的ViewRoot對象,這種關聯關係是由WindowManager來維護的。
那麼decorView與ViewRoot的關聯關係是在什麼時候建立的呢?答案是Activity啓動時,ActivityThread.handleResumeActivity()方法中建立了它們兩者的關聯關係。這裏我們不具體分析它們建立關聯的時機與方式,感興趣的同學可以參考相關源碼。下面我們直入主題,分析一下ViewRoot是如何完成View的繪製的。

View繪製的起點

當建立好了decorView與ViewRoot的關聯後,ViewRoot類的requestLayout()方法會被調用,以完成應用程序用戶界面的初次佈局。實際被調用的是ViewRootImpl類的requestLayout()方法,這個方法的源碼如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查發起佈局請求的線程是否爲主線程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

上面的方法中調用了scheduleTraversals()方法來調度一次完成的繪製流程,該方法會向主線程發送一個“遍歷”消息,最終會導致ViewRootImpl的performTraversals()方法被調用。下面,我們以performTraversals()爲起點,來分析View的整個繪製流程。
接下來對遍歷的每個View進行三個階段的繪製:measure、draw、layout

(五)Android View 繪製流程

在這裏插入圖片描述
View繪製大致可以分成三個流程,分別是measure(測量),layout(佈局),draw(繪製),這三者的順序就是measure(測量)->layout(佈局)->draw(繪製)。

measure: 判斷是否需要重新計算View的大小,需要的話則計算;
layout: 判斷是否需要重新計算View的位置,需要的話則計算;
draw: 判斷是否需要重新繪製View,需要的話則重繪製。

在這裏插入圖片描述

1、Measure

Measure的目的就是測量View的寬和高

(1)MeasureSpec理解——父容器傳遞給子容器的佈局要求

MeasureSpec(View的內部類)
由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求(測量模式+測量參數)。對於一個ViewGroup或者View的寬高而言,都一一對應一個MeasureSpec。
測量規格爲int型,值由高2位規格模式specMode和低30位具體尺寸specSize組成,其中SpecMode只有三種值:

  • UPSPECIFIED : 父容器對於子容器沒有任何限制,子容器想要多大就多大
  • EXACTLY: 父容器已經爲子容器設置了尺寸,子容器應當服從這些邊界,不論子容器想要多大的空間。
  • AT_MOST:子容器可以是聲明大小內的任意大小(測量子View大小child.measure(width,height),但子View大小不能超過聲明大小)

組合下的子View和父View之間寬高的關係,將LayoutParams和MeasureSpec組合起來分析最終子View的寬高。LayoutParams指的是子View的寬高設置參數,而MeasureSpec是父View傳遞給子View的,因爲LayoutParams有三種情況(不討論fill _ parent,因爲已經過時),而MeasureSpec也有三種,最終會有3*3 = 9種情況:
在這裏插入圖片描述
ViewGroup.LayoutParams
我們常見的ViewGroup是各種佈局等控件,像線性佈局(LinearLayout),相對佈局(RelativeLayout),約束佈局(ConstraintLayout),網格佈局(GridLayout)等等,而LayoutParams類就是指定View寬高等佈局參數而被使用的。其實很簡單,就對應着我們在佈局文件中對應的爲View設置各種寬高,如下所示:

  • 具體值:以px或者dp爲單位
  • fill _ parent:這個已經過時,強制性使子視圖的大小擴展至與父視圖大小相等(不含 padding )
  • match _ parent:特性和fill_parent相似,Android版本大於2.3使用
  • wrap _ content:自適應大小,強制性地使視圖擴展以便顯示其全部內容(含 padding )

(2)View的Measure過程(默認)

在這裏插入圖片描述
measure():基本測量邏輯的判斷。
onMeasure():根據View寬/高的測量規格計算View的寬/高值:getDefaultSize()&存儲測量後的View寬 / 高:setMeasuredDimension()
setMeasuredDimension():存儲測量後的寬和高。
getDefaultSize():根據View寬/高的測量規格計算View的寬/高。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//設置mMeasuredWidth和mMeasuredHeight,View的測量結束
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
} 

//建議高度/寬度是android:minHeight屬性的值或者View背景圖片的大小值
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//@param size參數一般表示設置了android:minHeight屬性或者該View背景圖片的大小值(getSuggestedMinimumWidth)
//@param measureSpec參數是父View傳給自己的MeasureSpec(是由父View的measureSpec和子View的LayoutParams共同確定的)
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED://表示該View的大小視圖未定,設置爲默認值
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

對於View默認是測量很簡單,大部分情況就是拿計算出來的MeasureSpec的size 當做最終測量的大小。
而對於其他的一些View的派生類,如TextView、Button、ImageView等,它們的onMeasure方法系統了都做了重寫,一般先去測量字符或者圖片的高度等,然後拿到View本身content這個高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那麼可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做爲View的大小。

(3)ViewGroup的Measure過程

在這裏插入圖片描述
measure():基本測量邏輯的判斷。
onMeasure():遍歷所有的子View進行測量,如何遍歷子View進行測量呢,就是調用measureChildren()方法,當所有的子View測量完成後,將會合並所有子View的尺寸最終計算出ViewGroup的尺寸。
measureChildren():遍歷子View並對子View進行測量,後續會調用measureChild()方法。
measureChild():計算出單個子View的MeasureSpec,通過調用getChildMeasureSpce()方法實現,調用每個子View的measure()方法進行測量。
getChildMeasureSpec():計算出子View的MeasureSpec。
setMeasuredDimension():存儲測量後的寬和高。

    //FrameLayout 的測量
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
....
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 遍歷自己的子View,只要不是GONE的都會參與測量,measureChildWithMargins方法基本思想就是父View把自己的MeasureSpec 
// 傳給子View結合子View自己的LayoutParams 算出子View 的MeasureSpec,然後繼續往下傳,
// 傳遞葉子節點,葉子節點沒有子View,根據傳下來的這個MeasureSpec測量自己就好了。
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() +  lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);  
     .... 
            }
        }
..... 
//所有的孩子測量之後,經過一系類的計算之後通過setMeasuredDimension設置自己的寬高,/對於FrameLayout 可能用最大的子View的大小,
// 對於LinearLayout,可能是高度的累加,具體測量的原理去看看源碼。總的來說,父View是等所有的子View測量結束之後,再來測量自己。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
    }  

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最後都會封裝到這個個LayoutParams。
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

//根據父View的測量規格和父View自己的Padding,
//還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec,具體計算過程看getChildMeasureSpec方法。
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);

        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed, lp.height);

//通過父View的MeasureSpec和子View的自己LayoutParams的計算,算出子View的MeasureSpec,然後父容器傳遞給子容器的
// 然後讓子View用這個MeasureSpec(一個測量要求,比如不能超過多大)去測量自己,如果子View是ViewGroup 那還會遞歸往下測量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

// spec參數   表示父View的MeasureSpec
// padding參數    父View的Padding+子View的Margin,父View的大小減去這些邊距,才能精確算出
//               子View的MeasureSpec的size
// childDimension參數  表示該子View內部LayoutParams屬性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一個精確指(an exactly size),
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);  //獲得父View的mode
        int specSize = MeasureSpec.getSize(spec);  //獲得父View的大小

        //父View的大小-自己的Padding+子View的Margin,得到值纔是子View的大小。
        int size = Math.max(0, specSize - padding);
        //初始化值,最後通過這個兩個值生成子View的MeasureSpec
        int resultSize = 0;
        int resultMode = 0;
//對應理解MeasureSpec機制
        switch (specMode) {
            //1、父View是EXACTLY(Parent has imposed an exact size on us)
            case MeasureSpec.EXACTLY:
                //1.1、子View的width或height是個精確值 (an exactly size)
                if (childDimension >= 0) {
                    resultSize = childDimension;         //size爲精確值
                    resultMode = MeasureSpec.EXACTLY;    //mode爲 EXACTLY 。
                }
                //1.2、子View的width或height爲 MATCH_PARENT
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;                   //size爲父視圖大小
                    resultMode = MeasureSpec.EXACTLY;    //mode爲 EXACTLY 。
                }
                //1.3、子View的width或height爲 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be bigger than us.
                    resultSize = size;                   //size爲父視圖大小
                    resultMode = MeasureSpec.AT_MOST;    //mode爲AT_MOST 。
                }
                break;

            //2、父View是AT_MOST的(Parent has imposed a maximum size on us)
            case MeasureSpec.AT_MOST:
                //2.1、子View的width或height是個精確值 (an exactly size)
                if (childDimension >= 0) {
                    // Child wants a specific size.so be it
                    resultSize = childDimension;        //size爲精確值
                    resultMode = MeasureSpec.EXACTLY;   //mode爲 EXACTLY 。
                }
                //2.2、子View的width或height爲 MATCH_PARENT/FILL_PARENT
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed. Constrain child to not be bigger than us.
                    resultSize = size;                  //size爲父視圖大小
                    resultMode = MeasureSpec.AT_MOST;   //mode爲AT_MOST
                }
                //2.3、子View的width或height爲 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be bigger than us.
                    resultSize = size;                  //size爲父視圖大小
                    resultMode = MeasureSpec.AT_MOST;   //mode爲AT_MOST
                }
                break;

            //3、父View是UNSPECIFIED的(Parent asked to see how big we want to be)
            case MeasureSpec.UNSPECIFIED:
                //3.1、子View的width或height是個精確值 (an exactly size)
                if (childDimension >= 0) {
                    // Child wants a specific size.let him have it
                    resultSize = childDimension;        //size爲精確值
                    resultMode = MeasureSpec.EXACTLY;   //mode爲 EXACTLY
                }
                //3.2、子View的width或height爲 MATCH_PARENT/FILL_PARENT
                else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should be
                    resultSize = 0;                        //size爲0! ,其值未定
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode爲 UNSPECIFIED
                }
                //3.3、子View的width或height爲 WRAP_CONTENT
                else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how big it should be
                    resultSize = 0;                        //size爲0! ,其值未定
                    resultMode = MeasureSpec.UNSPECIFIED;  //mode爲 UNSPECIFIED
                }
                break;
        }
        //根據上面邏輯條件獲取的mode和size構建MeasureSpec對象。(這個值由父View的MeasureSpec和子View的lp(childDimension)決定)
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

(4)具體案例

1、佈局代碼
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="50dp"
    android:background="@android:color/holo_blue_dark"
    android:paddingBottom="70dp"
    android:orientation="vertical">
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/material_blue_grey_800"
        android:text="TextView"
        android:textColor="@android:color/white"
        android:textSize="20sp" />
    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="@android:color/holo_green_dark" />
</LinearLayout>
2、佈局結果

在這裏插入圖片描述

3、View樹

在這裏插入圖片描述

4、佈局流程

View繪製的起點
View的繪製是由ViewRoot來負責的。每個應用程序窗口的decorView都有一個與之關聯的ViewRoot對象,這種關聯關係是由WindowManager來維護的(Activity啓動時,ActivityThread.handleResumeActivity()方法中建立了它們兩者的關聯關係。)
當建立好了decorView與ViewRoot的關聯後,ViewRoot類的requestLayout()方法會被調用,以完成應用程序用戶界面的初次佈局。實際被調用的是ViewRootImpl類的requestLayout()方法,這個方法的源碼如下:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 檢查發起佈局請求的線程是否爲主線程  
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

上面的方法中調用了scheduleTraversals()方法來調度一次完成的繪製流程,該方法會向主線程發送一個“遍歷”消息,最終會導致ViewRootImpl的performTraversals()方法被調用。下面,我們以performTraversals()爲起點,來分析View的整個繪製流程。

step1.DecorView(FragmentLayout)——整個View的ROOT

繪製入口是由ViewRootImpl的perform Traversals()發起Measure,Layout,Draw等流程

    private void performTraversals() {
......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);//mWidth爲屏幕寬度,lp.width=match_parent
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);//mHeight爲屏幕高度,lp.height=match_parent
......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//mView其實就是DecorView,DecorView本質是Fragment,進入Fragment的OnMeasure()
......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
        mView.draw(canvas);
......
    }

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
                break;
   ......
        }
        return measureSpec;
    }

在這裏插入圖片描述
那麼接下來在FrameLayout 的onMeasure()方法DecorView開始for循環測量自己的子View,測量完所有的子View再來測量自己,根據View樹知接下來要測量ViewRoot的大小

step2.ViewRoot(LinearLayout)

ViewRoot 的MeasureSpec mode應該等於EXACTLY(DecorView MeasureSpec 的mode是EXACTLY,ViewRoot的layoutparams 是match_parent),size 等於DecorView的size
在這裏插入圖片描述
ViewRoot是一個LinearLayout ,ViewRoot.measure最終會執行的LinearLayout 的onMeasure 方法,LinearLayout 的onMeasure 方法又開始逐個測量它的子View(measureChildWithMargins),那麼根據View的層級圖,接下來測量的是header(ViewStub),由於header的Gone,所以直接跳過不做測量工作,所以接下來輪到ViewRoot的第二個child content(android.R.id.content)

step3.Content(LinearLayout)

由於ViewRoot 的mPaddingBottom=100px(id/statusBarBackground的View的高度剛好等於100px,所以計算出來Content(android.R.id.content) 的MeasureSpec 的高度少了100px )它的寬高的mode 根據算出來也是EXACTLY(ViewRoot 是EXACTLY和android.R.id.content 是match_parent)。
在這裏插入圖片描述
Content(android.R.id.content) 是FrameLayout,遞歸調用開始準備計算id/linear的MeasureSpec

step4.linear(LinearLayout)

id/linear的heightMeasureSpec 的mode=AT_MOST,因爲id/linear 的LayoutParams 的layout_height=“wrap_content”,由於id/linear 的 android:layout_marginTop=“50dp” 使得lp.topMargin=200px (本設備的density=4,px=4*pd),在計算後id/linear的heightMeasureSpec 的size 少了200px。
在這裏插入圖片描述

step5.text(TextView)

算出id/text 的MeasureSpec 後TextView 拿着剛纔計算出來的MeasureSpec(mode=AT_MOST,size=1980),這個就是對TextView的高度和寬度的約束,進到TextView 的onMeasure(widthMeasureSpec,heightMeasureSpec) 方法
在這裏插入圖片描述
TextView字符的高度(也就是TextView的content高度[wrap_content])測出來=107px,107px 並沒有超過1980px(允許的最大高度),所以實際測量出來TextView的高度是107px。
最終算出id/text 的mMeasureWidth=1440px,mMeasureHeight=107px。

step6 view(View)

在這裏插入圖片描述
id/linear 的子View的高度都計算完畢了,接下來id/linear就通過所有子View的測量結果計算自己的高寬,id/linear是LinearLayout,所有它的高度計算簡單理解就是子View的高度的累積+自己的Padding.
在這裏插入圖片描述
最終算出id/linear出來後,id/content 就要根據它唯一的子View id/linear 的測量結果和自己的之前算出的MeasureSpec一起來測量自己的結果,具體計算的邏輯去看FrameLayout onMeasure 函數的計算過程。以此類推,接下來測量ViewRoot,然後再測量id/statusBarBackground,最後測量DecorView 的高寬,最終整個測量過程結束。

2、Layout

layout的主要作用 :根據子視圖的大小以及佈局參數將View樹放到合適的位置上。確認View&ViewGroup的四個頂點的位置(從而確定位置),left,top,right,bottom

1.Android屏幕座標系

View&ViewGroup位置與Android屏幕座標系相關。

2.入口DecorView

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 

3.ViewGroup的layout函數

在這裏插入圖片描述
layout():調用layout()方法計算ViewGroup自身的位置,在此方法調用路徑中有一個方法特別重要,這個方法就是setFrame(),它的作用就是根據傳入的4個位置值,設置View本身的四個頂點位置,也就是用來確定最終View的位置的。接下來就是回調onLayout()方法。
onLayout():對於ViewGroup而言,它不僅僅要確認自身的位置,它還要計算它的子View的位置,因此onLayout的作用就是遍歷並計算每個子View的位置。

public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        //LayoutTransition是用於處理ViewGroup增加和刪除子視圖的動畫效果
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

int childCount = getChildCount() ;
//安排其children在父視圖的具體位置
for(int i=0 ;i<childCount ;i++){
    View child = getChildAt(i) ;
    //整個layout()過程就是個遞歸過程
    child.layout(l, t, r, b) ;
}

遍歷自己的孩子,然後調用child.layout(l, t, r, b) ,給子view 通過setFrame(l, t, r, b) 確定位置,
而重點是(l, t, r, b) 怎麼計算出來的呢。是通過之前measure測量出來的MeasuredWidth和MeasuredHeight、在xml 設置的Gravity、RelativeLayout 的其他參數等等一起來確定子View在父視圖的具體位置的。
具體的計算過程不同的ViewGroup 的實現都不一樣(FragmentLayout\RelativeLayout\LinearLayout)

4.View的layout函數

在這裏插入圖片描述
layout():調用layout()方法主要爲了計算View自身的位置,在此方法調用路徑中有一個方法特別重要,這個方法就是setFrame(),它的作用就是根據傳入的4個位置值,設置View本身的四個頂點位置,也就是用來確定最終View的位置的。接下來就是回調onLayout()方法。
onLayout():對於View的onLayout()方法來說,它是一個空實現。爲什麼View的onLayout()方法是空實現呢?因爲onLayout()方法作用是計算此VIew的子View的位置,對於單一的View而言,它並不存在子View,因此它肯定是空實現啦!

public final void layout(int l, int t, int r, int b) {
   .....
    //設置View位於父視圖的座標軸
    //setFrame(l, t, r, b) 可以理解爲給mLeft 、mTop、mRight、mBottom賦值,確定該View在父View的相對位置
    boolean changed = setFrame(l, t, r, b);
    //判斷View的位置是否發生過變化,看有必要進行重新layout嗎
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        //調用onLayout(changed, l, t, r, b); 函數
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
   .....
}

3、Draw

Draw過程的目的繪製View&ViewGroup的視圖。

1、背景繪製

2、對ViewGroup繪製

在這裏插入圖片描述
draw():繪製ViewGroup自身。
drawBackground():繪製ViewGroup自身的背景。
onDraw():繪製View自身的內容。
dispatchDraw():對於ViewGroup而言,它是存在子View的,因此此方法就是用來遍歷子View,然後讓每個子View進入Draw過程從而完成繪製過程。
onDrawScrollBars():ViewGroup的裝飾繪製。

3、對View繪製

在這裏插入圖片描述
draw():繪製View自身。
drawBackground():繪製View自身的背景。
onDraw():繪製View自身的內容。
dispatchDraw():對於View而言,它是空實現,因爲它的作用是繪製子View的,因爲單一的View沒有子View,因此它是空實現。
onDrawScrollBars():從名字可以看出,它是繪製滑動條等裝飾的,比如ListView的滑動條。

onDraw(canvas) 方法是view用來draw 自己的,具體如何繪製,顏色線條什麼樣式就需要子View自己去實現,View.java 的onDraw(canvas) 是空實現,ViewGroup 也沒有實現,每個View的內容是各不相同的,所以需要由子類去實現具體邏輯。

4、對當前父View的所有子View繪製

就是遍歷子View然後drawChild(),drawChild()方法實際調用的是子View.draw()方法,ViewGroup類已經爲我們實現繪製子View的默認過程

5、對View滾動條繪製

在這裏插入圖片描述

(六)Android動畫

(1)View動畫(視圖動畫)

視圖動畫的作用對象是視圖(View),分爲補間動畫和逐幀動畫

1.1 幀動畫(AnimationDrawable)

在這裏插入圖片描述
原理就是將一張張單獨的圖片連貫的進行播放,從而在視覺上產生一種動畫的效果,圖片資源決定了這種方式可以實現怎樣的動畫
(1)作用對象
視圖控件(View)

1、如Android的TextView、Button等等
2、不可作用於View組件的屬性,如:顏色、背景、長度等等

(2)原理
將動畫拆分爲 幀 的形式,且定義每一幀 = 每一張圖片,並按序播放一組預先定義好的圖片。
(3)特點
優點:使用簡單、方便
缺點:容易引起OOM,因爲會使用大量&尺寸較大的圖片資源(應該避免使用尺寸較大的圖片)
(4)應用場景
適用於複雜、個性化的動畫效果。
(5)具體使用
步驟1:將動畫資源(每張圖片資源)放到drawable文件夾裏
步驟2:在XML中實現動畫(文件路徑:res/anim文件夾創建動畫效果的.xml文件)
res/anim/frame_anim1.xml

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
 						android:oneshot="true">//設置是否只播放一次,默認爲false
//item = 動畫圖片資源;duration = 設置一幀持續時間(ms)
<item android:drawable="@drawable/a_0" android:duration="100" />
<item android:drawable="@drawable/a_1"  android:duration="100" />
<item android:drawable="@drawable/a_2" android:duration="100" />
</animation-list>

步驟3:在Java代碼中載入&啓動&停止動畫

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_frame_animation);
    ImageView animationImg1 = (ImageView) findViewById(R.id.animation1);
    animationImg1.setImageResource(R.drawable.frame_anim1);//設置動畫
    AnimationDrawable animationDrawable1 = (AnimationDrawable) animationImg1.getDrawable();//獲取動畫對象
    animationDrawable1.start();//啓動動畫
}

protected void onStop() {
    super.onStop();
    AnimationDrawable animationDrawable1 = (AnimationDrawable) animationImg1.getDrawable();//獲取動畫對象
    animationDrawable1.stop();//停止動畫
}

1.2補間動畫(Animation)

在這裏插入圖片描述
(1)作用對象
視圖對象(View)
(2)原理
通過確定開始的視圖樣式 & 結束的視圖樣式、中間動畫變化過程由系統補全來確定一個動畫。即只需要開發者設置動畫的起始值和結束值,中間的動畫由系統自動幫我們完成。
(3)分類
補間動畫包含四種動畫類型:透明度(AlphaAnimation),縮放(ScaleAnimation),旋轉(RotateAnimation),位移(TranslateAnimation)繼承自 Animation 類。不同的動畫對應不同的子類。
在這裏插入圖片描述
(4)具體使用
既可以在Java代碼中動態的指定這四種動畫效果,也可在XML代碼指定。
(4.1)XML代碼中設置
xml文件中屬性動畫的目錄是res/anim/file_name.xml(不推薦)xml 文件中視圖動畫代碼如下,透明度動畫對應標籤 ,縮放動畫對應標籤 ,旋轉動畫對應標籤 ,位移動畫對應標籤 ,根標籤 就表示一個動畫集合 AnimationSet;

<set xmlns:android="http://schemas.android.com/apk/res/android"
//組合動畫獨特的屬性,表示祝賀動畫是否 和集合共享一個插值器
//如果集合不指定插值器,那麼自動化需要單獨設置
android:shareInterpolator="true" >
<!--透明度-->
<alpha
    android:fromAlpha="透明度起始值,0表示完全透明"
    android:toAlpha="透明度最終值,1表示不透明"
    android:duration="動畫持續時間"
    android:fillAfter="true表示保持動畫結束時的狀態,false表示不保持"/>
<!--縮放-->
<scale
    android:fromXScale="水平方向縮放的起始值,比如0"
    android:fromYScale="豎直方向縮放的起始值,比如0"
    android:toXScale="水平方向縮放的結束值,比如2"
    android:toYScale="豎直方向縮放的結束值,比如2"
    android:pivotX="縮放支點的x座標"
    android:pivotY="縮放支點的y座標(支點可以理解爲縮放的中心點,縮放過程中這點的座標是不變的;支點默認在中心位置)" />
<!--位移-->
<translate
    android:fromXDelta="x起始值"
    android:toXDelta="x結束值"
    android:fromYDelta="y起始值"
    android:toYDelta="y結束值" />
<!--旋轉-->
<rotate
    android:fromDegrees="旋轉起始角度"
    android:toDegrees="旋轉結束角度"
    android:pivotX="縮放支點的x座標"
    android:pivotY="縮放支點的y座標" />
</set>

(1)單個動畫實現

ImageView ivAni = (ImageView) findViewById(R.id.iv_ani);//創建設置動畫的視圖View
Animation ani = AnimationUtils.loadAnimation(this, R.anim.ani_view);//創建動畫對象 並傳入設置的動畫效果xml文件
ivAni.startAnimation(ani);//播放動畫

(4.2)Java代碼實現
每種補間動畫擁有自己的子類。

llGroup = (LinearLayout) findViewById(R.id.ll_group);
// 創建動畫集合
AnimationSet aniSet = new AnimationSet(false);
// 透明度動畫
AlphaAnimation alpha = new AlphaAnimation(0, 1);
alpha.setDuration(4000);
aniSet.addAnimation(alpha);

// 旋轉動畫
RotateAnimation rotate = new RotateAnimation(0, 360,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
rotate.setDuration(4000);
aniSet.addAnimation(rotate);

// 縮放動畫
ScaleAnimation scale = new ScaleAnimation(1.5f, 0.5f, 1.5f, 0.5f);
scale.setDuration(4000);
aniSet.addAnimation(scale);

// 位移動畫
TranslateAnimation translate = new TranslateAnimation(0, 160, 0, 240);
translate.setDuration(4000);
aniSet.addAnimation(translate);
// 把動畫設置給llGroup
llGroup.startAnimation(aniSet);

(5)監聽動畫
Animation類通過監聽動畫開始 / 結束 / 重複時刻來進行一系列操作,如跳轉頁面等等。可採用動畫適配器AnimatorListenerAdapter,解決實現接口繁瑣 的問題。

anim.addListener(new AnimatorListenerAdapter() {  
// 向addListener()方法中傳入適配器對象AnimatorListenerAdapter()
// 由於AnimatorListenerAdapter中已經實現好每個接口
// 所以這裏不實現全部方法也不會報錯
    @Override  
    public void onAnimationStart(Animator animation) {  
    // 如想只想監聽動畫開始時刻,就只需要單獨重寫該方法就可以
    }  
    @Override
            public void onAnimationEnd(Animation animation) {
                // 動畫結束時回調
    }
            @Override
            public void onAnimationRepeat(Animation animation) {
                //動畫重複執行的時候回調
     }
});  

(6)自定義View動畫
所有的自定義動畫都需要繼承 android.view.animation.Animation 抽象類,然後重寫 initialize() 和 applyTransformation() 這兩個方法
(1)initialize() 方法中對一些變量進行初始化
(2)applyTransformation() 方法中通過矩陣(Matrix)修改動畫數值,從而控制動畫的實現過程,這也是自定義動畫的核心。
applyTransformation(float interpolatedTime, Transformation t) 方法在動畫的執行過程中會不斷地調用
float interpolatedTime 表示當前動畫進行的時間與動畫總時間(一般在 setDuration() 方法中設置)的比值,從0逐漸增大到1;
Transformation t 傳遞當前動畫對象,一般可以通過代碼 android.graphics.Matrix matrix = t.getMatrix() 獲得 Matrix 矩陣對象,再設置 Matrix 對象,一般要用到 interpolatedTime 參數,以此達到控制動畫實現的結果(隨時間變換)
具體案例:實現QQ抖動效果

public class QQTrembleAnimation extends Animation{

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        setDuration(1000);//設置默認時長1秒
        setFillAfter(true);//保持動畫結束狀態
        setInterpolator(new LinearInterpolator());//設置線性插值器
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        //自定義動畫的核心,在動畫的執行過程中會不斷回調此方法,並且每次回調interpolatedTime值都在不斷變化(0----1)
        //Matrix.setTranslate(dx,dy)中dx、dy表示移動距離,是根據interpolatedTime計算出正弦值,實現了抖動
        t.getMatrix().setTranslate(
                (float)Math.sin(interpolatedTime * 50)*8,
                (float)Math.sin(interpolatedTime * 50)*8
        );
        super.applyTransformation(interpolatedTime, t);
    }
}

myAnimationView = (ImageView) findViewById(R.id.my_animation_view);
//實現抖動動畫
private void TrembleAnimation() {
    QQTrembleAnimation tremble = new QQTrembleAnimation();
    tremble.setRepeatCount(2);//重複次數2次(不包含第一次)
    myAnimationView.startAnimation(tremble);
}

(7)應用場景
Activity的切換效果
Activity啓動/退出時的動畫效果。自定義淡入淡出效果&左右滑動效果。淡入淡出採用透明度動畫(Alpha)左右滑動採用平移動畫(Translate)
(7.1)進入動畫xml文件
enter_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    >
    //從右邊滑動到中間
    <translate
        android:duration="500"
        android:fromXDelta="100%p"
        android:toXDelta="0%p"
        />
    //淡入
    <alpha  
        android:duration="1500"  
        android:fromAlpha="0.0"  
        android:toAlpha="1.0" />  
</set>

(7.2)退出動畫xml文件
exit_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    >
    //從中間滑動到左邊
    <translate
        android:duration="500"
        android:fromXDelta="0%p"
        android:toXDelta="-100%p"
         />
    //淡出
    <alpha  
        android:duration="1500"  
        android:fromAlpha="1.0"  
        android:toAlpha="0.0" />  
</set>

(7.3)設置動畫

Intent intent = new Intent(MainActivity.this, SecActivity.class);
                startActivity(intent);
                // 自定義的Activity切換動畫效果      
                overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
                }

注意: overridePendingTransition()必須要在startActivity(intent)和finish()後被調用才能生效。
Fragment的切換效果
(1)系統自帶

FragmentTransaction fragmentTransaction = mFragmentManager
                .beginTransaction();

fragmentTransaction.setTransition(int transit);
// 通過setTransition(int transit)進行設置
// transit參數說明
// 1. FragmentTransaction.TRANSIT_NONE:無動畫
// 2. FragmentTransaction.TRANSIT_FRAGMENT_OPEN:標準的打開動畫效果
// 3. FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:標準的關閉動畫效果

// 標準動畫設置好後,在Fragment添加和移除的時候都會有。

(2)自定義動畫

// 採用`FragmentTransavtion`的 `setCustomAnimations()`進行設置

FragmentTransaction fragmentTransaction = mFragmentManager
                .beginTransaction();

fragmentTransaction.setCustomAnimations(
                R.anim.in_from_right,
                R.anim.out_to_left);

視圖組中子元素出場動畫
視圖組(ViewGroup)中子元素可以具備出場時的補間動畫效果,常用於爲ListView的item設置出場動畫
步驟1:設置子元素的出場動畫
res/anim/view_animation.xml

<?xml version="1.0" encoding="utf-8"?>
// 此處採用了組合動畫
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    android:duration="3000"

    <alpha
        android:duration="1500"
        android:fromAlpha="1.0"
        android:toAlpha="0.0" />
        
    <translate
        android:fromXDelta="500"
        android:toXDelta="0"
         />
</set>

步驟2:設置 視圖組(ViewGroup)的動畫文件
res/ anim /anim_layout.xml

<?xml version="1.0" encoding="utf-8"?>
// 採用LayoutAnimation標籤
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    // 子元素開始動畫的時間延遲
    // 如子元素入場動畫的時間總長設置爲300ms
    // 那麼 delay = "0.5" 表示每個子元素都會延遲150ms纔會播放動畫效果
    // 第一個子元素延遲150ms播放入場效果;第二個延遲300ms,以此類推

    android:animationOrder="normal"
    // 表示子元素動畫的順序
    // 可設置屬性爲:
    // 1. normal :順序顯示,即排在前面的子元素先播放入場動畫
    // 2. reverse:倒序顯示,即排在後面的子元素先播放入場動畫
    // 3. random:隨機播放入場動畫

    android:animation="@anim/view_animation"
    // 設置入場的具體動畫效果
    // 將步驟1的子元素出場動畫設置到這裏
    />

步驟3:爲視圖組(ViewGroup)指定andorid:layoutAnimation屬性
指定的方式有兩種: XML / Java代碼設置
方式1:在 XML 中指定

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:orientation="vertical" >
    <ListView
        android:id="@+id/listView1"
        android:layoutAnimation="@anim/anim_layout"
        // 指定layoutAnimation屬性用以指定子元素的入場動畫
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

方式2:在Java代碼中指定
這樣就不用額外設置res/ anim /anim_layout.xml該xml文件了

        ListView lv = (ListView) findViewById(R.id.listView1);

        Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_item);
         // 加載子元素的出場動畫

        LayoutAnimationController controller = new LayoutAnimationController(animation);
        controller.setDelay(0.5f);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
       // 設置LayoutAnimation的屬性

        lv.setLayoutAnimation(controller);
        // 爲ListView設置LayoutAnimation的屬性

(2)屬性動畫(Animator)

1、簡介

作用對象:任意Java對象(不再侷限視圖View對象)
實現的動畫效果:可自定義各種動畫效果(不再侷限於4種基本變換)
作用領域:API11(Android3.0)後引入

2、核心原理

在一定時間間隔內,通過不斷對值進行改變,並不斷將該值賦給對象的屬性,從而實現該對象在該屬性上的動畫效果。這裏的可以是任意對象的任意屬性。
可理解爲一種按照一定變化率對屬性值進行操作的機制,變化率就是依賴Interpolator控制,而值操作則是TypeEvaluator控制。
在這裏插入圖片描述
從工作原理可以看出屬性動畫有2個重要的類——ValueAnimator,ObjectAnimator。屬性動畫的使用基本依靠這兩個類:

3、具體使用

3.1 ValueAnimator類

(1)簡介
屬性動畫機制中最核心的一個類。通過不斷控制值的變化,再不斷手動賦給對象的屬性,從而實現動畫效果。
在這裏插入圖片描述
從上面原理可以看出:ValueAnimator類中有3個重要方法:
1、ValueAnimator.ofInt(int values)
2、ValueAnimator.ofFloat(float values)
3、ValueAnimator.ofObject(int values)
(2)ValueAnimator.ofInt(int values)
作用:將初始值 以整型數值的形式 過渡到結束值,即估值器是整型估值器 - IntEvaluator。ValueAnimator本質是一種值的操作機制,值從一個int的初始值平滑過渡到一個int結束值,開發者通過手動將這些值賦給對象的屬性值。從而實現動畫。

// 步驟1:設置動畫屬性的初始值 & 結束值
ValueAnimator anim = ValueAnimator.ofInt(0, 3);
        // ofInt()作用有兩個
        // 1. 創建動畫實例
        // 2. 將傳入的多個Int參數進行平滑過渡:此處傳入0和1,表示將值從0平滑過渡到1
        // 如果傳入了3個Int參數 a,b,c ,則是先從a平滑過渡到b,再從b平滑過渡到C,以此類推
        // ValueAnimator.ofInt()內置了整型估值器,直接採用默認的.不需要設置,即默認設置瞭如何從初始值 過渡到 結束值
        // 關於自定義插值器我將在下節進行講解
        // 下面看看ofInt()的源碼分析 ->>關注1
        
// 步驟2:設置動畫的播放各種屬性
        anim.setDuration(500);
        // 設置動畫運行的時長
        
        anim.setStartDelay(500);
        // 設置動畫延遲播放時間

        anim.setRepeatCount(0);
        // 設置動畫重複播放次數 = 重放次數+1
        // 動畫播放次數 = infinite時,動畫無限重複
        
        anim.setRepeatMode(ValueAnimator.RESTART);
        // 設置重複播放動畫模式
        // ValueAnimator.RESTART(默認):正序重放
        // ValueAnimator.REVERSE:倒序回放
     
// 步驟3:將改變的值手動賦值給對象的屬性值:通過動畫的更新監聽器
        // 設置 值的更新監聽器
        // 即:值每次改變、變化一次,該方法就會被調用一次
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                int currentValue = (Integer) animation.getAnimatedValue();
                // 獲得改變後的值
                
                System.out.println(currentValue);
                // 輸出改變後的值

        // 步驟4:將改變後的值賦給對象的屬性值,下面會詳細說明
                View.setproperty(currentValue);

       // 步驟5:刷新視圖,即重新繪製,從而實現動畫效果
                View.requestLayout();
                
                
            }
        });

        anim.start();
        // 啓動動畫
    }

// 關注1:ofInt()源碼分析
    public static ValueAnimator ofInt(int... values) {
        // 允許傳入一個或多個Int參數
        // 1. 輸入一個的情況(如a):從0過渡到a;
        // 2. 輸入多個的情況(如a,b,c):先從a平滑過渡到b,再從b平滑過渡到C
        
        ValueAnimator anim = new ValueAnimator();
        // 創建動畫對象
        anim.setIntValues(values);
        // 將傳入的值賦值給動畫對象
        return anim;
    }

值從初始值過渡到結束值的過程如下:
在這裏插入圖片描述
實例:通過動畫的更新監聽器,將改變的值手動賦值給對象的屬性值。實現效果:將按鈕的寬度從150px放大到500px

Button mButton = (Button) findViewById(R.id.Button);
        // 創建動畫作用對象:此處以Button爲例

// 步驟1:設置屬性數值的初始值 & 結束值
        ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
        // 初始值 = 當前按鈕的寬度,此處在xml文件中設置爲150
        // 結束值 = 500
        // ValueAnimator.ofInt()內置了整型估值器,直接採用默認的.不需要設置
        // 即默認設置瞭如何從初始值150 過渡到 結束值500

// 步驟2:設置動畫的播放各種屬性
        valueAnimator.setDuration(2000);
        // 設置動畫運行時長:1s

// 步驟3:將屬性數值手動賦值給對象的屬性:此處是將 值 賦給 按鈕的寬度
        // 設置更新監聽器:即數值每次變化更新都會調用該方法
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {

                int currentValue = (Integer) animator.getAnimatedValue();
                // 獲得每次變化後的屬性值
                System.out.println(currentValue);
                // 輸出每次變化後的屬性值進行查看

                mButton.getLayoutParams().width = currentValue;
                // 每次值變化時,將值手動賦值給對象的屬性
                // 即將每次變化後的值 賦 給按鈕的寬度,這樣就實現了按鈕寬度屬性的動態變化

// 步驟4:刷新視圖,即重新繪製,從而實現動畫效果
                mButton.requestLayout();
                
            }
        });

        valueAnimator.start();
        // 啓動動畫

    }

效果:
在這裏插入圖片描述
(3)ValueAnimator.ofFloat(float values)
作用:將初始值 以整型數值的形式 過渡到結束值,即估值器是浮點型估值器 - FloatEvaluator。

ValueAnimator anim = ValueAnimator.ofFloat(0, 3);  

效果圖
在這裏插入圖片描述
從上面可以看出,ValueAnimator.ofInt()與ValueAnimator.oFloat()僅僅只是在估值器上的區別:(即如何從初始值 過渡 到結束值)
ValueAnimator.oFloat()採用默認的浮點型估值器 (FloatEvaluator)
ValueAnimator.ofInt()採用默認的整型估值器(IntEvaluator)
(4)ValueAnimator.ofObject(int values)
作用:
將初始值以對象的形式過渡到結束值,通過操作實現動畫效果。
使用模板:

// 創建初始動畫時的對象  & 結束動畫時的對象
myObject object1 = new myObject();  
myObject object2 = new myObject();  

ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);  
// 創建動畫對象 & 設置參數
// 參數說明
// 參數1:自定義的估值器對象(TypeEvaluator 類型參數) - 下面會詳細介紹
// 參數2:初始動畫的對象
// 參數3:結束動畫的對象
anim.setDuration(5000);  
anim.start(); 

實例說明:
步驟1:定義對象類
因爲ValueAnimator.ofObject()是面向對象操作的,所以需要自定義對象類。本例需要操作的對象是 圓的點座標
Point.java

public class Point {

    // 設置兩個變量用於記錄座標的位置
    private float x;
    private float y;

    // 構造方法用於設置座標
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    // get方法用於獲取座標
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}

步驟2:根據需求實現TypeEvaluator接口
估值器(TypeEvaluator)介紹
作用:設置動畫 如何從初始值 過渡到 結束值 的邏輯

插值器(Interpolator)決定 值 的變化模式(勻速、加速blabla)
估值器(TypeEvaluator)決定 值的具體變化數值

ValueAnimator.ofFloat()實現了 **將初始值 以浮點型的形式 過渡到結束值 ** 的邏輯,那麼這個過渡邏輯具體是怎麼樣的呢?
其實是系統內置了一個 FloatEvaluator估值器,內部實現了初始值與結束值 以浮點型的過渡邏輯,我們來看一下 FloatEvaluator的代碼實現:

public class FloatEvaluator implements TypeEvaluator {  
// FloatEvaluator實現了TypeEvaluator接口

// 重寫evaluate()
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 參數說明
// fraction:表示動畫完成度(根據它來計算當前動畫的值)
// startValue、endValue:動畫的初始值和結束值
        float startFloat = ((Number) startValue).floatValue();  
        
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
        // 初始值 過渡 到結束值 的算法是:
        // 1. 用結束值減去初始值,算出它們之間的差值
        // 2. 用上述差值乘以fraction係數
        // 3. 再加上初始值,就得到當前動畫的值
    }  
}  

ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具備系統內置的估值器,即FloatEvaluator & IntEvaluator,即系統已經默認實現了 如何從初始值 過渡到 結束值 的邏輯;但對於ValueAnimator.ofObject(),從上面的工作原理可以看出並沒有系統默認實現,因爲對對象的動畫操作複雜 & 多樣,系統無法知道如何從初始對象過度到結束對象。因此,對於ValueAnimator.ofObject(),我們需自定義估值器(TypeEvaluator)來告知系統如何進行從 初始對象 過渡到 結束對象的邏輯。
自定義實現的邏輯如下

// 實現TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 複寫evaluate()
// 在evaluate()裏寫入對象動畫過渡的邏輯
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 參數說明
        // fraction:表示動畫完成度(根據它來計算當前動畫的值)
        // startValue、endValue:動畫的初始值和結束值

        ... // 寫入對象動畫過渡的邏輯
        
        return value;  
        // 返回對象動畫過渡的邏輯計算後的值
    }

實現TypeEvaluator接口的目的是自定義如何 從初始點座標 過渡 到結束點座標;本例實現的是一個從左上角到右下角的座標過渡邏輯。
PointEvaluator.java

// 實現TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {

    // 複寫evaluate()
    // 在evaluate()裏寫入對象動畫過渡的邏輯
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 將動畫初始值startValue 和 動畫結束值endValue 強制類型轉換成Point對象
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        // 根據fraction來計算當前動畫的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        
        // 將計算後的座標封裝到一個新的Point對象中並返回
        Point point = new Point(x, y);
        return point;
    }

}

步驟3:將屬性動畫作用到自定義View當中
MyView.java

public class MyView extends View {
    // 設置需要用到的變量
    public static final float RADIUS = 70f;// 圓的半徑 = 70
    private Point currentPoint;// 當前點座標
    private Paint mPaint;// 繪圖畫筆
    

    // 構造方法(初始化畫筆)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 複寫onDraw()從而實現繪製邏輯
    // 繪製邏輯:先在初始點畫圓,通過監聽當前座標值(currentPoint)的變化,每次變化都調用onDraw()重新繪製圓,從而實現圓的平移動畫效果
    @Override
    protected void onDraw(Canvas canvas) {
        // 如果當前點座標爲空(即第一次)
        if (currentPoint == null) {
            currentPoint = new Point(RADIUS, RADIUS);
            // 創建一個點對象(座標是(70,70))

            // 在該點畫一個圓:圓心 = (70,70),半徑 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);


 // (重點關注)將屬性動畫作用到View中
            // 步驟1:創建初始動畫時的對象點  & 結束動畫時的對象點
            Point startPoint = new Point(RADIUS, RADIUS);// 初始點爲圓心(70,70)
            Point endPoint = new Point(700, 1000);// 結束點爲(700,1000)

            // 步驟2:創建動畫對象 & 設置初始值 和 結束值
            ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
            // 參數說明
            // 參數1:TypeEvaluator 類型參數 - 使用自定義的PointEvaluator(實現了TypeEvaluator接口)
            // 參數2:初始動畫的對象點
            // 參數3:結束動畫的對象點

            // 步驟3:設置動畫參數
            anim.setDuration(5000);
            // 設置動畫時長

// 步驟3:通過 值 的更新監聽器,將改變的對象手動賦值給當前對象
// 此處是將 改變後的座標值對象 賦給 當前的座標值對象
            // 設置 值的更新監聽器
            // 即每當座標值(Point對象)更新一次,該方法就會被調用一次
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    currentPoint = (Point) animation.getAnimatedValue();
                    // 將每次變化後的座標值(估值器PointEvaluator中evaluate()返回的Piont對象值)到當前座標值對象(currentPoint)
                    // 從而更新當前座標值(currentPoint)

// 步驟4:每次賦值後就重新繪製,從而實現動畫效果
                    invalidate();
                    // 調用invalidate()後,就會刷新View,即才能看到重新繪製的界面,即onDraw()會被重新調用一次
                    // 所以座標值每改變一次,就會調用onDraw()一次
                }
            });

            anim.start();
            // 啓動動畫


        } else {
            // 如果座標值不爲0,則畫圓
            // 所以座標值每改變一次,就會調用onDraw()一次,就會畫一次圓,從而實現動畫效果

            // 在該點畫一個圓:圓心 = (30,30),半徑 = 30
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }
    }
}

步驟4:在佈局文件加入自定義View空間
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />
</RelativeLayout>

步驟5:在主代碼文件設置顯示視圖
MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

效果圖:
在這裏插入圖片描述
從上面可以看出,其實ValueAnimator.ofObject()的本質還是操作 ** 值 **,只是是採用將 多個值 封裝到一個對象裏的方式 同時對多個值一起操作而已

就像上面的例子,本質還是操作座標中的x,y兩個值,只是將其封裝到Point對象裏,方便同時操作x,y兩個值而已

3.2 ObjectAnimator類

(1)實現動畫原理
直接對對象的屬性值進行改變操作,從而實現動畫效果

  • 如直接改變 View的 alpha 屬性 從而實現透明度的動畫效果
  • 繼承自ValueAnimator類,即底層的動畫實現機制是基於ValueAnimator類

本質原理: 通過不斷控制 值 的變化,再不斷 自動 賦給對象的屬性,從而實現動畫效果。如下圖:
在這裏插入圖片描述
從上面的工作原理可以看出:ObjectAnimator與 ValueAnimator類的區別:
ValueAnimator 類是先改變值,然後 手動賦值 給對象的屬性從而實現動畫;是 間接 對對象屬性進行操作;
ObjectAnimator 類是先改變值,然後 自動賦值 給對象的屬性從而實現動畫;是 直接 對對象屬性進行操作;
(2)具體使用
使用格式

ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  

// ofFloat()作用有兩個
// 1. 創建動畫實例
// 2. 參數設置:參數說明如下
// Object object:需要操作的對象
// String property:需要操作的對象的屬性
// float ....values:動畫初始值 & 結束值(不固定長度)
// 若是兩個參數a,b,則動畫效果則是從屬性的a值到b值
// 若是三個參數a,b,c,則則動畫效果則是從屬性的a值到b值再到c值
// 以此類推
// 至於如何從初始值 過渡到 結束值,同樣是由估值器決定,此處ObjectAnimator.ofFloat()是有系統內置的浮點型估值器FloatEvaluator,同ValueAnimator講解
anim.setDuration(500);// 設置動畫運行的時長
anim.setStartDelay(500);// 設置動畫延遲播放時間
anim.setRepeatCount(0);// 設置動畫重複播放次數 = 重放次數+1,動畫播放次數 = infinite時,動畫無限重複
anim.setRepeatMode(ValueAnimator.RESTART);// 設置重複播放動畫模式:ValueAnimator.RESTART(默認):正序重放,ValueAnimator.REVERSE:倒序回放
animator.start();  // 啓動動畫

使用實例:四種基本變換:平移、旋轉、縮放&透明度。通過在ObjectAnimator.ofFloat()的第二個參數String property傳入alpha、rotation、translationX 和 scaleY設置改變的屬性值。

//動畫作用對象是mButton,屬性是透明度alpha,效果是常規-全透明-常規
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);
//動畫作用對象是mButton,屬性是旋轉rotation,效果是0-360
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
//動畫作用對象是mButton,屬性是X軸平移,效果是從當前位置平移到x=300再平移到初始位置
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
//動畫作用對象是mButton,屬性是X軸縮放,效果是放大到3倍再縮小到初始大小
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "scaleX", 1f, 3f, 1f);

(3)自定義對象屬性實現動畫效果
ofFloat()的第二個參數可以傳入任意屬性值。
ObjectAnimator 類 對 對象屬性值 進行改變從而實現動畫效果的本質是:通過不斷控制 值 的變化,再不斷 自動 賦給對象的屬性,從而實現動畫效果。
而 自動賦給對象的屬性的本質是調用該對象屬性的set() & get()方法進行賦值
所以,ObjectAnimator.ofFloat(Object object, String property, float …values)的第二個參數傳入值的作用是:讓ObjectAnimator類根據傳入的屬性名 去尋找 該對象對應屬性名的 set() & get()方法,從而進行對象屬性值的賦值。而 自動賦給對象的屬性的本質是調用該對象屬性的set() & get()方法進行賦值
所以,ObjectAnimator.ofFloat(Object object, String property, float …values)的第二個參數傳入值的作用是:讓ObjectAnimator類根據傳入的屬性名 去尋找 該對象對應屬性名的 set() & get()方法,從而進行對象屬性值的賦值。
源碼分析

ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
// 其實Button對象中並沒有rotation這個屬性值
// ObjectAnimator並不是直接對我們傳入的屬性名進行操作
// 而是根據傳入的屬性值"rotation" 去尋找對象對應屬性名對應的get和set方法,從而通過set() &  get()對屬性進行賦值

// 因爲Button對象中有rotation屬性所對應的get & set方法
// 所以傳入的rotation屬性是有效的
// 所以才能對rotation這個屬性進行操作賦值
public void setRotation(float value);  
public float getRotation();  

// 實際上,這兩個方法是由View對象提供的,所以任何繼承自View的對象都具備這個屬性

自動賦值

// 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  
anim.setDuration(500);
animator.start();  
// 啓動動畫,源碼分析就直接從start()開始

<--  start()  -->
@Override  
public void start() {  
    AnimationHandler handler = sAnimationHandler.get();  

    if (handler != null) {  
        // 判斷等待動畫(Pending)中是否有和當前動畫相同的動畫,如果有就把相同的動畫給取消掉 
        numAnims = handler.mPendingAnimations.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
      // 判斷延遲動畫(Delay)中是否有和當前動畫相同的動畫,如果有就把相同的動畫給取消掉 
        numAnims = handler.mDelayedAnims.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
    }  
    
    super.start();  
   // 調用父類的start()
   // 因爲ObjectAnimator類繼承ValueAnimator類,所以調用的是ValueAnimator的star()
   // 經過層層調用,最終會調用到 自動賦值給對象屬性值的方法
   // 下面就直接看該部分的方法
}  
<-- 自動賦值給對象屬性值的邏輯方法 ->>
// 步驟1:初始化動畫值
private void setupValue(Object target, Keyframe kf) {  
    if (mProperty != null) {  
        kf.setValue(mProperty.get(target));  
        // 初始化時,如果屬性的初始值沒有提供,則調用屬性的get()進行取值
    }  
        kf.setValue(mGetter.invoke(target));   
    }  
}  

// 步驟2:更新動畫值
// 當動畫下一幀來時(即動畫更新的時候),setAnimatedValue()都會被調用
void setAnimatedValue(Object target) {  
    if (mProperty != null) {  
        mProperty.set(target, getAnimatedValue());  
        // 內部調用對象該屬性的set()方法,從而從而將新的屬性值設置給對象屬性
    }  
    
}  

自動賦值的邏輯:
初始化時,如果屬性的初始值沒有提供,則調用屬性的 get()進行取值;
當 值 變化時,用對象該屬性的 set()方法,從而從而將新的屬性值設置給對象屬性。

  • ObjectAnimator 類針對的是任意對象 & 任意屬性值,並不是單單針對於View對象 如果需要採用ObjectAnimator類實現動畫效果,那麼需要操作的對象就必須有該屬性的set() & get()
  • 同理,針對上述另外的三種基本動畫效果,View也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()等set()& get() 。
    實例:一個圓顏色漸變
    對於屬性動畫,其拓展性在於:不侷限於系統限定的動畫,可以自定義動畫,即自定義對象的屬性,並通過操作自定義的屬性從而實現動畫。
    那麼,該如何自定義屬性呢?本質上,就是:
  • 爲對象設置需要操作屬性的set() & get()方法
  • 通過實現TypeEvaluator類從而定義屬性變化的邏輯
    步驟1:設置對象類屬性的set() & get()方法
    設置對象類屬性的set() & get()有兩種方法:
  • 通過繼承原始類,直接給類加上該屬性的 get()& set(),從而實現給對象加上該屬性的 get()& set()
  • 通過包裝原始動畫對象,間接給對象加上該屬性的 get()& set()。即 用一個類來包裝原始對象
    這裏示範第一種方法:
    MyView2.java
public class MyView2 extends View {
    // 設置需要用到的變量
    public static final float RADIUS = 100f;// 圓的半徑 = 100
    private Paint mPaint;// 繪圖畫筆

    private String color;
    // 設置背景顏色屬性

    // 設置背景顏色的get() & set()方法
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
        mPaint.setColor(Color.parseColor(color));
        // 將畫筆的顏色設置成方法參數傳入的顏色
        invalidate();
        // 調用了invalidate()方法,即畫筆顏色每次改變都會刷新視圖,然後調用onDraw()方法重新繪製圓
        // 而因爲每次調用onDraw()方法時畫筆的顏色都會改變,所以圓的顏色也會改變
    }


    // 構造方法(初始化畫筆)
    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 複寫onDraw()從而實現繪製邏輯
    // 繪製邏輯:先在初始點畫圓,通過監聽當前座標值(currentPoint)的變化,每次變化都調用onDraw()重新繪製圓,從而實現圓的平移動畫效果
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(500, 500, RADIUS, mPaint);
    }
}

步驟2:在佈局文件加入自定義View控件
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView2
        android:id="@+id/MyView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

步驟3:根據需求實現TypeEvaluator接口
此處實現估值器的本質是:實現 顏色過渡的邏輯。
ColorEvaluator.java

public class ColorEvaluator implements TypeEvaluator {
    // 實現TypeEvaluator接口

    private int mCurrentRed;

    private int mCurrentGreen ;

    private int mCurrentBlue ;

    // 複寫evaluate()
    // 在evaluate()裏寫入對象動畫過渡的邏輯:此處是寫顏色過渡的邏輯
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 獲取到顏色的初始值和結束值
        String startColor = (String) startValue;
        String endColor = (String) endValue;

        // 通過字符串截取的方式將初始化顏色分爲RGB三個部分,並將RGB的值轉換成十進制數字
        // 那麼每個顏色的取值範圍就是0-255
        int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
        int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
        int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);

        int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
        int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
        int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);

        // 將初始化顏色的值定義爲當前需要操作的顏色值
            mCurrentRed = startRed;
            mCurrentGreen = startGreen;
            mCurrentBlue = startBlue;


        // 計算初始顏色和結束顏色之間的差值
        // 該差值決定着顏色變化的快慢:初始顏色值和結束顏色值很相近,那麼顏色變化就會比較緩慢;否則,變化則很快
        // 具體如何根據差值來決定顏色變化快慢的邏輯寫在getCurrentColor()裏.
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        int colorDiff = redDiff + greenDiff + blueDiff;
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
                    fraction);
                    // getCurrentColor()決定如何根據差值來決定顏色變化的快慢 ->>關注1
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
                    redDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
                    redDiff + greenDiff, fraction);
        }
        // 將計算出的當前顏色的值組裝返回
        String currentColor = "#" + getHexString(mCurrentRed)
                + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);

        // 由於我們計算出的顏色是十進制數字,所以需要轉換成十六進制字符串:調用getHexString()->>關注2
        // 最終將RGB顏色拼裝起來,並作爲最終的結果返回
        return currentColor;
    }


    // 關注1:getCurrentColor()
    // 具體是根據fraction值來計算當前的顏色。

    private int getCurrentColor(int startColor, int endColor, int colorDiff,
                                int offset, float fraction) {
        int currentColor;
        if (startColor > endColor) {
            currentColor = (int) (startColor - (fraction * colorDiff - offset));
            if (currentColor < endColor) {
                currentColor = endColor;
            }
        } else {
            currentColor = (int) (startColor + (fraction * colorDiff - offset));
            if (currentColor > endColor) {
                currentColor = endColor;
            }
        }
        return currentColor;
    }

    // 關注2:將10進制顏色值轉換成16進制。
    private String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }

}

步驟4:調用ObjectAnimator.ofObject()方法
MainActivity.java

public class MainActivity extends AppCompatActivity {

    MyView2 myView2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myView2 = (MyView2) findViewById(R.id.MyView2);
        ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "color", new ColorEvaluator(),
                "#0000FF", "#FF0000");
        // 設置自定義View對象、背景顏色屬性值 & 顏色估值器
        // 本質邏輯:
        // 步驟1:根據顏色估值器不斷 改變 值 
        // 步驟2:調用set()設置背景顏色的屬性值(實際上是通過畫筆進行顏色設置)
        // 步驟3:調用invalidate()刷新視圖,即調用onDraw()重新繪製,從而實現動畫效果

        anim.setDuration(8000);
        anim.start();
    }
}

效果圖
在這裏插入圖片描述
注:如何手動設置對象類屬性的set()&get()
ObjectAnimator 類 自動賦給對象的屬性 的本質是調用該對象屬性的set() & get()方法進行賦值。所以,ObjectAnimator.ofFloat(Object object, String property, float …values)的第二個參數傳入值的作用是:讓ObjectAnimator類根據傳入的屬性名 去尋找 該對象對應屬性名的 set() & get()方法,從而進行對象屬性值的賦值。
從上面的原理可知,如果想讓對象的屬性a的動畫生效,屬性a需要同時滿足下面兩個條件:
1、對象必須要提供屬性a的set()方法

a. 如果沒傳遞初始值,那麼需要提供get()方法,因爲系統要去拿屬性a的初始值
b. 若該條件不滿足,程序直接Crash

2、對象提供的 屬性a的set()方法 對 屬性a的改變 必須通過某種方法反映出來

a. 如帶來ui上的變化
b. 若這條不滿足,動畫無效,但不會Crash)

比如說:由於View的setWidth()並不是設置View的寬度,而是設置View的最大寬度和最小寬度的;所以通過setWidth()無法改變控件的寬度;所以對View視圖的width做屬性動畫沒有效果。具體請看下面Button按鈕的例子

       Button  mButton = (Button) findViewById(R.id.Button);
        // 創建動畫作用對象:此處以Button爲例
        // 此Button的寬高設置具體爲具體寬度200px

               ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
                 // 設置動畫的對象

針對上述對象屬性的set()不是設置屬性 或 根本沒有set() / get ()的情況應該如何處理?手動設置對象類屬性的set() & get()。共有兩種方法:
(1)通過繼承原始類,直接給類加上該屬性的 get()& set(),從而實現給對象加上該屬性的 get()& set()
(2)通過包裝原始動畫對象,間接給對象加上該屬性的 get()&
set()。即 用一個類來包裝原始對象
對於第一種方法,在上面的例子已經說明;下面主要講解第二種方法:通過包裝原始動畫對象,間接給對象加上該屬性的get()& set()
本質上是採用了設計模式中的裝飾模式,即通過包裝類從而擴展對象的功能
還是採用上述 Button 按鈕的例子

public class MainActivity extends AppCompatActivity {
    Button mButton;
    ViewWrapper wrapper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.Button);
        // 創建動畫作用對象:此處以Button爲例

        wrapper = new ViewWrapper(mButton);
        // 創建包裝類,並傳入動畫作用的對象
        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
                // 設置動畫的對象是包裝類的對象
            }
        });

    }
    // 提供ViewWrapper類,用於包裝View對象
    // 本例:包裝Button對象
    private static class ViewWrapper {
        private View mTarget;

        // 構造方法:傳入需要包裝的對象
        public ViewWrapper(View target) {
            mTarget = target;
        }

        // 爲寬度設置get() & set()
        public int getWidth() {
            return mTarget.getLayoutParams().width;
        }

        public void setWidth(int width) {
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }

    }

}

效果圖:
在這裏插入圖片描述

3.3 ValueAnimator & ObjectAnimator總結

對比ValueAnimator類 & ObjectAnimator 類,其實二者都屬於屬性動畫,本質上都是一致的:先改變值,然後 賦值 給對象的屬性從而實現動畫效果。
但二者的區別在於:

  • ValueAnimator 類是先改變值,然後 手動賦值 給對象的屬性從而實現動畫;是 間接 對對象屬性進行操作;本質上是一種 改變 值的操作機制
  • ObjectAnimator類是先改變值,然後 自動賦值 給對象的屬性從而實現動畫;是 直接 對對象屬性進行操作;

可以理解爲:ObjectAnimator更加智能、自動化程度更高

4、動畫監聽

動畫適配器AnimatorListenerAdapter進行監聽動畫的部分時刻,解決實現接口繁瑣問題

anim.addListener(new AnimatorListenerAdapter() {  
// 向addListener()方法中傳入適配器對象AnimatorListenerAdapter()
// 由於AnimatorListenerAdapter中已經實現好每個接口
// 所以這裏不實現全部方法也不會報錯
    @Override  
    public void onAnimationStart(Animator animation) {  
    // 如想只想監聽動畫開始時刻,就只需要單獨重寫該方法就可以
    }  
});  

5、應用實例

(1)實現影子特效(安卓自帶屬性)

常用的屬性動畫屬性值:

translationX、translationY----控制view對象相對其左上角座標在X、Y軸上偏移的距離
rotation、rotationX、rotationY----控制view對象繞支點進行2D和3D旋轉
scaleX、scaleY----控制view對象繞支點進行2D縮放
pivotX、pivotY----控制view對象的支點位置,這個位置一般就是view對象的中心點。圍繞這個支點可以進行旋轉和縮放處理
x、y----描述view對象在容器中的最終位置,是最初的左上角座標和translationX、translationY值的累計和
alpha----表示view對象的透明度。默認值是1(完全透明)、0(不透明)

myAnimationView1 = (ImageView) findViewById(R.id.my_animation_view1);
private void ShadowAnimator() {
    //創建ObjectAnimator屬性對象,參數爲動畫要設置的View對象、動畫屬性、屬性值
ObjectAnimator animator1 = ObjectAnimator.ofFloat(myAnimationView1,"alpha",0,1);//漸變
ObjectAnimator animator2 = ObjectAnimator.ofFloat(myAnimationView2,"translationY",0,200F);//上移
ObjectAnimator animator3 = ObjectAnimator.ofFloat(myAnimationView3,"translationY",0,-200F);//下移
ObjectAnimator animator4 = ObjectAnimator.ofFloat(myAnimationView4,"translationX",0,200F);//右移
ObjectAnimator animator5 = ObjectAnimator.ofFloat(myAnimationView5,"translationX",0,-200F);//左移
    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.setDuration(3000);
    animatorSet.setInterpolator(new BounceInterpolator());//彈跳效果插值器
    animatorSet.playTogether(animator1,animator2,animator3,animator4,animator5);//組合動畫共同播放
    animatorSet.start();
}
(2)實現顏色漸變特效(自定義屬性實現)
方法1:ValueAnimator和屬性動畫的監聽

爲ValueAnimator對象設置動畫監聽,代碼如下所示:valueAnimator.addUpdateListener(),需要傳入一個AnimatorUpdateListener對象,一般我們傳入的是AnimatorUpdateListener的匿名對象,即:valueAnimator.addUpdateListener(new AnimatorUpdateListener(){…}),需要重寫它的onAnimationUpdate()方法,那麼上述值的計算邏輯就放在onAnimationUpdate()方法體內;

//6秒內把一個view控件的背景顏色從從紅色漸變到藍色
private void ColorChangeAnimator(final String start,final String end) {
    final ValueAnimator animator = ValueAnimator.ofFloat(0,100f);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            //獲取當前動畫的進度值,1~100
            float currentValue = (float) valueAnimator.getAnimatedValue();
            //獲取當前動畫的百分比,0~1
            float fraction = valueAnimator.getAnimatedFraction();
            //調用evaluateForColor,根據百分比計算出對應的顏色
            String colorResult = evaluateForColor(fraction,start,end);
            //通過Color.parseColor解析字符串顏色值,傳給ColorDrawable
            ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
            myAnimationView1.setBackground(colorDrawable);
            //Android視圖機制中在主線程中調用它,用於觸發視圖的繪製刷新
            myAnimationView1.invalidate();
        }
    });
    animator.setDuration(6*1000);
    animator.start();
}
方法2:重寫TypeEvaluator估值器,用來計算屬性動畫某個時刻的屬性值的具體值

(1)自定義Interpolator
Interpolator直譯過來就是插補器,也譯作插值器,直接控制動畫的變化速率,這涉及到變化率概念,形象點說就是加速度,可以簡單理解爲變化的快慢。從上面的繼承關係可以清晰的看出來,Interpolator是一個接口,並未提供插值邏輯的具體實現,它的非直接子類有很多,比較常用的有下面四個:

加減速插值器AccelerateDecelerateInterpolator;
線性插值器LinearInterpolator;
加速插值器AccelerateInterpolator;
減速插值器DecelerateInterpolator;

當你沒有爲動畫設置插值器時,系統默認會幫你設置加減速插值器AccelerateDecelerateInterpolator
(2)自定義TypeEvaluator

public class PositionEvaluator implements TypeEvaluator {
    // 創建PositionView對象,用來調用createPoint()方法創建當前PositionPoint對象
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        // 將startValue,endValue強轉成PositionPoint對象
        PositionPoint point_1 = (PositionPoint) startValue;
        // 獲取起始點Y座標
        float currentY = point_1.getY();
        // 調用forCurrentX()方法計算X座標
        float x = forCurrentX(fraction);
        // 調用forCurrentY()方法計算Y座標
        float y = forCurrentY(fraction, currentY);
        return new PositionPoint(x,y);
    }
    /**
     * 計算Y座標
     */
    private float forCurrentY(float fraction, float currentY) {
        float resultY = currentY;
        if (fraction != 0f) {
            resultY = fraction * 400f + 20f;
        }
        return resultY;
    }
    /**
     * 計算X座標
     */
    private float forCurrentX(float fraction) {
        float range = 120f;// 振幅
        float resultX = 160f + (float) Math.sin((6 * fraction) * Math.PI) * range;// 週期爲3,故爲6fraction
        return resultX;
    }
}

ValueAnimator animator1 = ValueAnimator.ofObject(
        new PositionEvaluator(),
        new PositionPoint(RADIUS, RADIUS),
        new PositionPoint(getWidth() - RADIUS, getHeight() - RADIUS));
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        currentPoint = (PositionPoint) animation.getAnimatedValue();
        invalidate();
    }
});

6、屬性動畫主要使用類

在這裏插入圖片描述

(3)View動畫與屬性動畫對比

視圖動畫缺點:

  • 作用對象侷限:View
    即補間動畫 只能夠作用在視圖View上,即只可以對一個Button、TextView、甚至是LinearLayout、或者其它繼承自View的組件進行動畫操作,但無法對非View的對象進行動畫操作。
    有些情況下的動畫效果只是視圖的某個屬性 & 對象而不是整個視圖;如,現需要實現視圖的顏色動態變化,那麼就需要操作視圖的顏色屬性從而實現動畫效果,而不是針對整個視圖進行動畫操作
  • 沒有改變View的屬性,只改變視覺效果
    補間動畫只是改變了View的視覺效果,而不會真正去改變View的屬性。如,將屏幕左上角的按鈕 通過補間動畫 移動到屏幕的右下角,點擊當前按鈕位置(屏幕右下角)是沒有效果的,因爲實際上按鈕還是停留在屏幕左上角,補間動畫只是將這個按鈕繪製到屏幕右下角,改變了視覺效果而已。
  • 動畫效果單一
    補間動畫只能實現平移、旋轉、縮放 & 透明度這些簡單的動畫需求一旦遇到相對複雜的動畫效果,即超出了上述4種動畫效果,那麼補間動畫則無法實現。功能和可擴展性具有侷限性。

屬性動畫優點:
屬性動畫是在Android3.0(API 11)後才提供的一種全新動畫模式。

  • 作用對象是任何一個Object對象
    也就是說我們完全可以給任意Object對象設置屬性動畫,而這個對象可以不是一個View組件,也不管這個對象是否是可見的,而視圖動畫的作用對象只能是一個View對象,這是最大的不同;
  • 實際改變了View對象的屬性
    視圖動畫的一個致命缺陷就是,通過視圖動畫將一個View對象(比如一個TextView,Button)位置改編後,該對象的觸摸事件的焦點依然在原位置,屬性動畫就很好的解決了這一缺陷;
  • 功能與可擴展性強
  • 可以控制動畫執行過程中的任意時刻的任意屬性值
    視圖動畫從本質上來說是一種補間動畫,他只對動畫的起始值和結束值進行賦值,屬性動畫就提供了很好地解決方案,就是自定義估值器控制動畫執行過程中的屬性值

視圖動畫優點:

  • 當我們把動畫的repeatCount設置爲無限循環時,如果在Activity退出時沒有及時將動畫停止,屬性動畫會導致Activity無法釋放而導致內存泄漏,而補間動畫卻沒有問題。因此,使用屬性動畫時切記在Activity執行 onStop 方法時順便將動畫停止。
  • xml 文件實現的補間動畫,複用率極高。在Activity切換,窗口彈出時等情景中有着很好的效果。

(4)插值器與估值器

4.1插值器

(1)簡介

定義:一個接口。
作用:設置 屬性值 從初始值過渡到結束值 的變化規律,如勻速、加速 & 減速 等等,即確定了 動畫效果變化的模式,如勻速變化、加速變化 等等。

(2)應用場景

實現非線性運動的動畫效果。

(3)系統內置插值器類型
作用 資源ID 對應Java類
動畫加速進行 @android:anim/accelerate_interpolator AccelerateInterpolator
先加速再減速 @android:anim/accelerate_decelerate_interpolator AccelerateDecelerateInterpolator
彈球效果 @android:anim/bounce_interpolator BounceInterpolator
週期運動 @android:anim/cycle_interpolator CycleInterpolator
減速 @android:anim/decelerate_interpolator DecelerateInterpolator
勻速 @android:anim/linear_interpolator LinearInterpolator

系統默認的插值器是AccelerateDecelerateInterpolator,即先加速後減速

(4)自定義插值器

本質:根據動畫的進度(0%-100%)計算出當前屬性值改變的百分比
具體使用:自定義插值器需要實現 Interpolator / TimeInterpolator接口 & 複寫getInterpolation()
補間動畫 實現 Interpolator接口;屬性動畫實現TimeInterpolator接口;TimeInterpolator接口是屬性動畫中新增的,用於兼容Interpolator接口,這使得所有過去的Interpolator實現類都可以直接在屬性動畫使用。

// Interpolator接口
public interface Interpolator {  

    // 內部只有一個方法
     float getInterpolation(float input) {  
         // 參數說明
         // input值值變化範圍是0-1,且隨着動畫進度(0% - 100% )均勻變化
        // 即動畫開始時,input值 = 0;動畫結束時input = 1
        // 而中間的值則是隨着動畫的進度(0% - 100%)在0到1之間均勻增加
        
      ...// 插值器的計算邏輯

      return xxx;
      // 返回的值就是用於估值器繼續計算的fraction值,下面會詳細說明
    }  

// TimeInterpolator接口
// 同上
public interface TimeInterpolator {  
  
    float getInterpolation(float input);  
   
}  

系統內置插值器源碼:勻速插值器LinearInterpolator、先加速再減速插值器AccelerateDecelerateInterpolator

// 勻速差值器:LinearInterpolator
@HasNativeInterpolator  
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {  
   // 僅貼出關鍵代碼
  ...
    public float getInterpolation(float input) {  
        return input;  
        // 沒有對input值進行任何邏輯處理,直接返回
        // 即input值 = fraction值
        // 因爲input值是勻速增加的,因此fraction值也是勻速增加的,所以動畫的運動情況也是勻速的,所以是勻速插值器
    }  


// 先加速再減速 差值器:AccelerateDecelerateInterpolator
@HasNativeInterpolator  
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {  
      // 僅貼出關鍵代碼
  ...
    public float getInterpolation(float input) {  
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
        // input的運算邏輯如下:
        // 使用了餘弦函數,因input的取值範圍是0到1,那麼cos函數中的取值範圍就是π到2π。
        // 而cos(π)的結果是-1,cos(2π)的結果是1
        // 所以該值除以2加上0.5後,getInterpolation()方法最終返回的結果值還是在0到1之間。只不過經過了餘弦運算之後,最終的結果不再是勻速增加的了,而是經歷了一個先加速後減速的過程
        // 所以最終,fraction值 = 運算後的值 = 先加速後減速
        // 所以該差值器是先加速再減速的
    }  
    }

自定義插值器的關鍵在於:對input值 根據動畫的進度(0%-100%)通過邏輯計算 計算出當前屬性值改變的百分比。(input取值從0~1)

(5)實例

效果:寫一個自定義Interpolator:先減速後加速
步驟1:根據需求實現Interpolator接口
DecelerateAccelerateInterpolator.java

public class DecelerateAccelerateInterpolator implements TimeInterpolator {

    @Override
    public float getInterpolation(float input) {
        float result;
        if (input <= 0.5) {
            result = (float) (Math.sin(Math.PI * input)) / 2;
            // 使用正弦函數來實現先減速後加速的功能,邏輯如下:
            // 因爲正弦函數初始弧度變化值非常大,剛好和餘弦函數是相反的
            // 隨着弧度的增加,正弦函數的變化值也會逐漸變小,這樣也就實現了減速的效果。
            // 當弧度大於π/2之後,整個過程相反了過來,現在正弦函數的弧度變化值非常小,漸漸隨着弧度繼續增加,變化值越來越大,弧度到π時結束,這樣從0過度到π,也就實現了先減速後加速的效果
        } else {
            result = (float) (2 - Math.sin(Math.PI * input)) / 2;
        }
        return result;
        // 返回的result值 = 隨着動畫進度呈先減速後加速的變化趨勢
    }
}

步驟2:設置插值器
MainActivity.java

 mButton = (Button) findViewById(R.id.Button);
        // 創建動畫作用對象:此處以Button爲例

        float curTranslationX = mButton.getTranslationX();
        // 獲得當前按鈕的位置

        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
        // 創建動畫對象 & 設置動畫效果
        // 表示的是:
        // 動畫作用對象是mButton
        // 動畫作用的對象的屬性是X軸平移
        // 動畫效果是:從當前位置平移到 x=1500 再平移到初始位置
        animator.setDuration(5000);
        animator.setInterpolator(new DecelerateAccelerateInterpolator());
        // 設置插值器
        animator.start();
        // 啓動動畫

4.2估值器

(1)簡介

設置屬性值從初始值過渡到結束值的變化具體數值的一個接口,用於決定值的變化規律(如勻速、加速等),是屬性動畫特有的屬性。

(2)應用場景

協助插值器實現非線性運動的動畫效果。

(3)系統內置估值器類型
插值器 說明
IntEvaluator 以整型的形式從初始值 - 結束值 進行過渡
FloatEvaluator 以浮點型的形式從初始值 - 結束值 進行過渡
ArgbEvaluator 以Argb類型的形式從初始值 - 結束值 進行過渡
(4)自定義估值器

本質:根據 插值器計算出當前屬性值改變的百分比 & 初始值 & 結束值 來計算 當前屬性具體的數值

如:動畫進行了50%(初始值=100,結束值=200),那麼勻速插值器計算出了當前屬性值改變的百分比是50%,那麼估值器則負責計算當前屬性值 = 100 + (200-100)x50% =150.

具體使用:自定義估值器需要實現 TypeEvaluator接口 & 複寫evaluate()

public interface TypeEvaluator {  

    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 參數說明
// fraction:插值器getInterpolation()的返回值
// startValue:動畫的初始值
// endValue:動畫的結束值

        ....// 估值器的計算邏輯

        return xxx;
        // 賦給動畫屬性的具體數值
        // 使用反射機制改變屬性變化

// 特別注意
// 那麼插值器的input值 和 估值器fraction有什麼關係呢?
// 答:input的值決定了fraction的值:input值經過計算後傳入到插值器的getInterpolation(),然後通過實現getInterpolation()中的邏輯算法,根據input值來計算出一個返回值,而這個返回值就是fraction了
    }  
}  

在學習自定義插值器前,我們先來看一個已經實現好的系統內置差值器:浮點型插值器:FloatEvaluator

public class FloatEvaluator implements TypeEvaluator {  
// FloatEvaluator實現了TypeEvaluator接口

// 重寫evaluate()
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 參數說明
// fraction:表示動畫完成度(根據它來計算當前動畫的值)
// startValue、endValue:動畫的初始值和結束值
        float startFloat = ((Number) startValue).floatValue();  
        
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
        // 初始值 過渡 到結束值 的算法是:
        // 1. 用結束值減去初始值,算出它們之間的差值
        // 2. 用上述差值乘以fraction係數
        // 3. 再加上初始值,就得到當前動畫的值
    }  
}  

屬性動畫中的ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具備系統內置的估值器,即FloatEvaluator & IntEvaluator
即系統已經默認實現了 如何從初始值 過渡到 結束值 的邏輯
但對於ValueAnimator.ofObject(),從上面的工作原理可以看出並沒有系統默認實現,因爲對對象的動畫操作複雜 & 多樣,系統無法知道如何從初始對象過度到結束對象
因此,對於ValueAnimator.ofObject(),我們需自定義估值器(TypeEvaluator)來告知系統如何進行從 初始對象 過渡到 結束對象的邏輯
自定義實現的邏輯如下

// 實現TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 複寫evaluate()
// 在evaluate()裏寫入對象動畫過渡的邏輯
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 參數說明
        // fraction:表示動畫完成度(根據它來計算當前動畫的值)
        // startValue、endValue:動畫的初始值和結束值

        ... // 寫入對象動畫過渡的邏輯
        
        return value;  
        // 返回對象動畫過渡的邏輯計算後的值
    }
(5)實例

自定義TypeEvaluator接口並通過ValueAnimator.ofObject()實現動畫效果。實現的動畫效果:一個圓從一個點 移動到 另外一個點
實現TypeEvaluator接口的目的是自定義如何 從初始點座標 過渡 到結束點座標;本例實現的是一個從左上角到右下角的座標過渡邏輯。

PointEvaluator.java
// 實現TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {

    // 複寫evaluate()
    // 在evaluate()裏寫入對象動畫過渡的邏輯
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 將動畫初始值startValue 和 動畫結束值endValue 強制類型轉換成Point對象
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        // 根據fraction來計算當前動畫的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        
        // 將計算後的座標封裝到一個新的Point對象中並返回
        Point point = new Point(x, y);
        return point;
    }

}

(七)Android事件分發機制

1、什麼叫事件分發機制

Android上面的View是樹形結構,View可能會重疊在一起,當我們點擊的地方有多個View都可以響應的時候,這個點擊事件應該給誰呢?爲了解決這個問題,就有了事件分發機制。
事件分發是:當發生了一個事件時,在屏幕上找到一個合適的控件來處理這個事件的過程。
其實事件分發的本質將點擊屏幕產生的MotionEvent對象傳遞到某個具體的View然後處理消耗這個事件的整個過程。

2、常見事件

當用戶點擊屏幕裏View或者ViewGroup的時候,將會產生一個事件對象,這個事件對象就是MotionEvent對象,這個對象記錄了事件的類型,觸摸的位置,以及觸摸的時間等。MotionEvent裏面定義了事件的類型,其實很容易理解,因爲用戶可以在屏幕觸摸,滑動,離開屏幕動作,分別對應MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP;

ACTION_DOWN:手指 初次接觸到屏幕 時觸發。
ACTION_MOVE:手指 在屏幕上滑動 時觸發,會會多次觸發。
ACTION_UP:手指 離開屏幕 時觸發。
ACTION_CANCEL:事件 被上層攔截 時觸發。

因此用戶在觸摸屏幕到離開屏幕會產生一系列事件,ACTION _ DOWN->ACTION _ MOVE(0個或者多個)->ACTION _ UP,那麼ACTION _ CANCEL事件是怎麼回事呢?請看下面的圖你就懂的更徹底了:
在這裏插入圖片描述
cancel的理解:
當控件收到前驅事件(什麼叫前驅事件?一個從DOWN一直到UP的所有事件組合稱爲完整的手勢,中間的任意一次事件對於下一個事件而言就是它的前驅事件)之後,後面的事件如果被父控件攔截,那麼當前控件就會收到一個CANCEL事件,並且把這個事件會傳遞給它的子事件。(注意:這裏如果在控件的onInterceptTouchEvent中攔截掉CANCEL事件是無效的,它仍然會把這個事件傳給它的子控件)之後這個手勢所有的事件將全部攔截,也就是說這個事件對於當前控件和它的子控件而言已經結束了。
  簡單的理解產生Cancel事件的條件就是:
父View收到ACTION_DOWN,如果沒有攔截事件,則ACTION_DOWN前驅事件被子視圖接收,父視圖後續事件會發送到子View。
此時如果在父View中攔截ACTION_UP或ACTION_MOVE,在第一次父視圖攔截消息的瞬間,父視圖指定子視圖不接受後續消息了,同時子視圖會收到ACTION_CANCEL事件。
  來個例子,我們知道ViewPager如何用戶在A頁滑動到B頁,滑動到不及一半的位置,那麼ViewPager就會給用戶回退到A頁,這是ViewPager的Cancel事件處理的。
ViewPager的onTouchEvent對ACTION_CANCEL的處理:

case MotionEvent.ACTION_CANCEL:
      if (mIsBeingDragged) {
          scrollToItem(mCurItem, true, 0, false);
          mActivePointerId = INVALID_POINTER;
          endDrag();
          needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
      }
      break;

拿ViewPager來說,在ScrollView包含ViewPager的情況下,對ViewPager做左右滑動,滑到一頁的一半時往上下滑,ViewPager收到MotionEvent.ACTION_CANCEL後就能夠回到先前那一頁,而不是停在中間。

3、Android事件分發方法

(1)dispatchTouchEvent
用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級的dispatchTouchEvent方法影響,表示是否消耗此事件。
(2)onInterceptTouchEvent
在上述方法dispatchTouchEvent內部調用,用來判斷是否攔截某個事件,返回結果表示是否攔截當前事件。如果當前View攔截了某個事件,則交給onTouchEvent繼續處理。並且同一個事件序列當中,此方法不會被再次調用。
(3)onTouchEvent
同樣也會在dispatchTouchEvent內部調用,用來處理Touch事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

Android事件分發由三個方法完成:

// 父View調用dispatchTouchEvent()開始分發事件
public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    // 父View決定是否攔截事件
    if(onInterceptTouchEvent(event)){
        // 父View調用onTouchEvent(event)消費事件,如果該方法返回true,表示
        // 該View消費了該事件,後續該事件序列的事件(Down、Move、Up)將不會在傳遞
        // 該其他View。
        consume = onTouchEvent(event);
    }else{
        // 調用子View的dispatchTouchEvent(event)方法繼續分發事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

通過上述僞代碼,我們可以得知點擊事件的傳遞規則:對於一個根ViewGroup而言,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouch就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前的事件,接着事件就會交給這個ViewGroup處理,即它的onTouch方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此直到事件被最終處理。

當一個View需要處理事件時,如果它設置了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調。這時事件處理還要看onTouch的返回值,如果返回false,則當前View的onTouchEvent方法會被調用;如果返回true,那麼當前View的onTouchEvent方法不會被調用。由此可見,給View設置的onTouchListener的優先級比onTouchEvent要高。在onTouchEvent方法中,如果當前設置的有onClickListener,那麼它的onClick方法會被調用。可以看出,平時我們常用的OnClickListener,其優先級最低,即處於事件傳遞的尾端。

當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity–>Window–>View,即事件總數先傳遞給Activity,Activity再傳遞給Window,最後Window再傳遞給頂級View,頂級View接收到事件後,就會按照事件分發機制去分發事件。考慮一種情況,如果一個View的onTouchEvent返回false,那麼它的父容器的onTouchEvent將會被調用,依次類推。如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理, 即Activity的onTouchEvent方法會被調用。這個過程其實很好理解,我們可以換一種思路,假設點擊事件是一個難題,這個難題最終被上級領導分給了一個程序員去處理(這是事件分發過程),結果這個程序員搞不定(onTouchEvent返回了false),現在該怎麼辦呢?難題必須要解決,那就只能交給水平更高的上級解決(上級的onTouchEvent被調用),如果上級再搞不定,那就只能交給上級的上級去解決,就這樣難題一層層地向上拋,這是公司內部一種常見的處理問題的過程。

4、屏幕控件

(1)佈局結構

<com.xyl.touchevent.test.RootView>
<com.xyl.touchevent.test.ViewGroupA>
<com.xyl.touchevent.test.View1/>
</com.xyl.touchevent.test.ViewGroupA>
<com.xyl.touchevent.test.View2/>
</com.xyl.touchevent.test.RootView>

(2)View樹
在這裏插入圖片描述

5、事件分發流程

在如上圖View的樹形結構中,事件發生時,最先由Activity接收,然後再一層層的向下層傳遞,直到找到合適的處理控件。大致如下:
ACTIVITY -> PHONEWINDOW -> DECORVIEW -> VIEWGROUP -> … -> VIEW
但是如果事件傳遞到最後的View還是沒有找到合適的View消費事件,那麼事件就會向相反的方向傳遞,最終傳遞給Activity,如果最後 Activity 也沒有處理,本次事件纔會被拋棄:
ACTIVITY <- PHONEWINDOW <- DECORVIEW <- VIEWGROUP <- … <- VIEW

6、Android中事件具體分發流程

在這裏插入圖片描述

7、總結

(1)對於 dispatchTouchEvent,onTouchEvent,return true是終結事件傳遞。return false 是回溯到父View的onTouchEvent方法。
(2)ViewGroup 想把自己分發給自己的onTouchEvent,需要攔截器onInterceptTouchEvent方法return true 把事件攔截下來。ViewGroup 的攔截器onInterceptTouchEvent 默認是不攔截的,所以return super.onInterceptTouchEvent()=return false;
(3)View 沒有攔截器,爲了讓View可以把事件分發給自己的onTouchEvent,View的dispatchTouchEvent默認實現(super)就是把事件分發給自己的onTouchEvent。
(4)View和ViewGroup相關事件分發回調方法:

  • 【dispatchTouchEvent】(View&&ViewGroup)
    事件分發,那麼這個事件可能分發出去的四個目標(注:------> 後面代表事件目標需要怎麼做。)
    1、 自己消費,終結傳遞。------->return true ;
    2、 給自己的onTouchEvent處理-------> 調用super.dispatchTouchEvent()系統默認會去調用 onInterceptTouchEvent,在onInterceptTouchEvent return true就會去把事件分給自己的onTouchEvent處理。
    3、 傳給子View------>調用super.dispatchTouchEvent()默認實現會去調用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就會把事件傳給子類。
    4、 不傳給子View,事件終止往下傳遞,事件開始回溯,從父View的onTouchEvent開始事件從下到上回歸執行每個控件的onTouchEvent------->return false;
    注: 由於View沒有子View所以不需要onInterceptTouchEvent 來控件是否把事件傳遞給子View還是攔截,所以View的事件分發調用super.dispatchTouchEvent()的時候默認把事件傳給自己的onTouchEvent處理(相當於攔截),對比ViewGroup的dispatchTouchEvent 事件分發,View的事件分發沒有上面提到的4個目標的第3點。
  • 【onTouchEvent】(View&&ViewGroup)
    事件處理的,那麼這個事件只能有兩個處理方式:
    1、自己消費掉,事件終結,不再傳給誰----->return true;
    2、繼續從下往上傳,不消費事件,讓父View也能收到到這個事件----->return false;View的默認實現是不消費的。所以super==false。
  • 【onInterceptTouchEvent】(ViewGroup)
    對於事件有兩種情況:
    1、攔截下來,給自己的onTouchEvent處理—>return true;
    2、不攔截,把事件往下傳給子View---->return false,ViewGroup默認是不攔截的,所以super==false;

8、幾點注意

(1)同一見事件序列是從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件的序列以down開始,中間含有數量不定的move事件,最終以up事件結束。
(2)正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因爲一旦一個元素攔截了某個事件,那麼同一個事件序列的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊手段可以做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
(3)某個View一旦決定攔截,那麼這個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不會被調用。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再調用這個View的onInterceptTouchEvent去詢問它是否攔截了。
(4)某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一件序列中的其他事件都不會再交給它處理,並且事件 將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。意思就是事件一旦交給一個View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它處理了,這就好比上級交給程序員一件事,如果這件事沒有處理好,短時間內上級就不敢再把事件交給這個程序員做了,二者是類似的道理。
(5)如果View不消耗ACTION_DOWN以外的事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會調用,並且當前View可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false。
(7)View沒有onInterceptTouchEvent方法,一旦點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。
(8)View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時爲false)。View的longClickable屬性默認爲false,clickable屬性要分情況,比如Button的clickable屬性默認爲true,而TextView的clickable屬性默認爲false。
(9)View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true。
(10)onClick會發生的前提是當前View是可點擊的,並且它接收到了down和up事件。
(11)事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外。

9、事件分發中的onTouch 和onTouchEvent 有什麼區別,又該如何使用?

(1)重寫onTouchEvent重寫onTouchEvent()處理ACTION_MOVE/DOWN/UP事件

public class TestButton extends Button {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean value = super.onTouchEvent(event);
        System.out.println("super.onTouchEvent: " + value+ " event: " + event.getAction());
        return value;
    }

(2)實現onTouchListener接口重寫onTouch()處理ACTION_MOVE/DOWN/UP事件

class OnTouchListenerTest implements View.OnTouchListener{
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return false;
    }
}

TestButton b = (TestButton)findViewById(R.id.button);
OnTouchListenerTest listener = new OnTouchListenerTest();
b.setOnTouchListener(listener);

(3)onTouchEvent與onTouch監聽區別
1、源碼對於View中dispatchTouchEvent實現

  public boolean dispatchTouchEvent(MotionEvent event){
... ...
      if(onFilterTouchEventForSecurity(event)){
          ListenerInfo li = mListenerInfo;
          if(li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                  && li.mOnTouchListener.onTouch(this, event)) {
              return true;
          }
          if(onTouchEvent(event)){
              return true;
          }
      }
... ...
      return false;
  }

2、總結
(1)onTouchListener的onTouch方法優先級比onTouchEvent高,會先觸發。
(2)假如onTouch方法返回false會接着觸發onTouchEvent,反之onTouchEvent方法不會被調用。
(3)控件內置諸如click事件的實現等等都基於onTouchEvent,假如onTouch返回true,這些事件將不會被觸發。

(八)View的刷新機制

1、invalidate()流程圖

在這裏插入圖片描述
invalidate主要給需要重繪的視圖添加DIRTY標記,並通過不斷回溯父視圖做矩形運算求得真正需要繪製的區域,並最終保存在ViewRoot中的mDirty變量中,最後調用scheduleTraversals發起重繪請求,scheduleTraversals會發送一個異步消息,最終調用performTraversals()執行重繪(performTraversals()遍歷所有相關聯的 View ,觸發它們的 onDraw 方法進行繪製)

2、源碼分析

1、子View需要刷新時,調用invalidate,通知父View完成——首先找到自己父View(View的成員變量mParent記錄自己的父View),然後將AttachInfo中保存的信息告訴父View刷新自己。

void invalidate(boolean invalidateCache) {
    final AttachInfo ai = mAttachInfo;//AttachInfo是在View第一次attach到Window時,ViewRoot傳給自己的子View的
    final ViewParent p = mParent;//父View
    //對於開啓硬件加速的應用程序,則調用父視圖的invalidateChild函數繪製整個區域,
    // 否則只繪製dirty區域(r變量所指的區域),這是一個向上回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集。
    if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
        if (p != null && ai != null && ai.mHardwareAccelerated) {
            p.invalidateChild(this, null);
            return;
        }
    }
    if (p != null && ai != null) {
        final Rect r = ai.mTmpInvalRect;
        r.set(0, 0, mRight - mLeft, mBottom - mTop);
        p.invalidateChild(this, r);
    }
}

2、在invalidate中,調用父View的invalidateChild,這是一個從第向上回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集。

   public final void invalidateChild(View child, final Rect dirty) {
        ViewParent parent = this;

        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            final int[] location = attachInfo.mInvalidateChildLocation;
            // 需要刷新的子View的位置
            location[CHILD_LEFT_INDEX] = child.mLeft;
            location[CHILD_TOP_INDEX] = child.mTop;

            do {
                //不斷的將子視圖的dirty區域與父視圖做運算來確定最終要重繪的dirty區域,
                // 最終循環到ViewRoot(ViewRoot的parent爲null)爲止,並將dirty區域保存到ViewRoot的mDirty變量中
...
                parent = parent.invalidateChildInParent(location, dirty);
            } while (parent != null);
        }
    }
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    //運算,計算最終需要重繪的dirty區域
        ...
    }

3、向上回溯的過程直到ViewRoot那裏結束,由ViewRoot對這個最終的刷新區域重新繪製performTraversals。

public void invalidateChild(View child, Rect dirty) {
    scheduleTraversals();
}

3、invalidate和postInvalidate

1、Invalidate()方法不能放在線程中,所以需要把Invalidate()方法放在Handler中。在MyThread中只需要在規定時間內發送一個Message給handler,當Handler接收到消息就調用Invalidate()方法。postInvalidate()方法就可以放在線程中做處理,就不需要Handler(postInvalidate 最終通過 Handler 切換到主線程,調用 invalidate)
2、Invalidate()方法和postInvalidate()都可以在主線程中調用而刷新視圖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章