第四章 自定义组件、动画

文章目录

第四章 自定义组件、动画

(一)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()都可以在主线程中调用而刷新视图。

发布了74 篇原创文章 · 获赞 15 · 访问量 6255

猜你喜欢

转载自blog.csdn.net/qq_29966203/article/details/90416199