深入了解一些Android动画

一、PropertyValuesHolder

阅读本文需要上一文Android属性动画的基础,这样才可以明白接下来要讲什么。

1.理解和使用

PropertyValuesHolder 是ObjectAnimation类似的一个方法,只是少了一个target,就是要执行的控件。看看正常的使用方法:会同时执行全部的Holder

public void  doPropertyValuesHolder(){
        //定义一个旋转Holder
        PropertyValuesHolder rotationHolder=
                PropertyValuesHolder.ofFloat(
                        "rotation",
                        60f,40f,100f,-60f,40f,88f,77f);

        //定义一个透明Holder
        PropertyValuesHolder alphaHolder=
                PropertyValuesHolder.ofFloat(
                        "alpha",
                        0.01f,0.5f,1.0f,0.8f,0.2f,0.0f);
        
    	//加载进ObjectAnimator
        ObjectAnimator objectAnimator=ObjectAnimator.ofPropertyValuesHolder(ballImageView,rotationHolder,alphaHolder);
        objectAnimator.setDuration(3000);
        objectAnimator.start();
    }

2.方法和参数

可以看看这个方法的参数:

ObjectAnimator ofPropertyValuesHolder(Object target,PropertyValuesHolder... values)
PropertyValuesHolder ofFloat(String propertyName, float... values)

Object target 是要显示动画的控件

PropertyValuesHolder... values 装载多个PropertyValuesHolder

String propertyName 代表要反射的参数,跟ObjectAnimation的参数是一样的

float... values 代表是可变长参数
这样的方法还有以下图片这些:

图片.png

其中ofObject()方法 ,也是跟ObjectAnimation的相似,也是要自定义TypeEvaluator。
图片.png

二、Keyframe

1.理解和使用

看名字,就是理解为关键帧的意思,在动画中,在某帧做一些操作,从而实现对比效果比较明显的效果。
关键帧表示是某个物体在哪个时间点应该在哪个位置上。
具体使用:

 public void  doPropertyValuesHolderKeyFrame(){

        //头keyframe1,从进度0.6开始,在进度60%的时候,数值是0.1f
        Keyframe keyframe1=Keyframe.ofFloat(0.6f,0.1f);

        //中间keyframe2
        Keyframe keyframe2=Keyframe.ofFloat(0.1f,0.8f);

        //尾部keyframe3,以50%进度作为结束,这时候的数值为0.2f
        Keyframe keyframe3=Keyframe.ofFloat(0.5f,0.2f);

        //装载到Holder中,并设置要反射的方法,这是反射的是setAlpha()方法,控制透明度
        PropertyValuesHolder alphaHolder=PropertyValuesHolder.ofKeyframe("alpha",keyframe1,keyframe2,keyframe3);

        //把装载到Holder中装载到ObjectAnimator或者ValueAnimation
        ObjectAnimator objectAnimator=ObjectAnimator.ofPropertyValuesHolder(ballImageView,alphaHolder);
        objectAnimator.setDuration(3000);
        objectAnimator.start();
    }

2.方法和参数

Keyframe ofFloat(float fraction, float value)

float fraction 表示进度

float value  表示在这个进度下的数值

 PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values)

String propertyName 要反射的set方法

Keyframe... values 传入Keyframe

Keyframe的方法,也是和其他的类似的。
图片.png

Keyframe的set方法,设置进度,插值器,数值。
没有设置插值器的时候,默认是线性插值器

 keyframe1.setInterpolator(new LinearInterpolator()); //默认线性插值器

图片.png

3.帧的操作

直接写结论:

  • 如果去掉0帧,则以第一个关键帧为起始位置
  • 如果去掉结束帧(进度为1),则以最后一个关键帧为结束位置
  • 使用keyframe来构建动画,至少需要2帧

三、ViewPropertyAnimator

1.理解和使用

可以通过串行的形式,快速定义动画,省去一些定义,在每次界面绘制的时候,启动动画,比其他的更节省消耗。
比如:

 ballImageView.animate().alpha(0.5f).rotation(360f).scaleX(1.5f).translationX(100f);

2.参数和方法

可以看到这些方法的返回值,基本都是ViewPropertyAnimator
图片.png
图片.png
再引用一张表格:

函数 含义
alpha(float value) 设置透明度
scaleY(float value) 设置 Y轴方向的缩放大小
scaleX(float value) 设置X轴方向的缩放大小
translationY(float value) 设置Y轴方向的移动值
translationX(float value) 设置X轴方向的移动值
rotation(float value) 设置绕Z轴旋转度数
rotationX(float value) 设置绕x轴旋转度数
rotationY(float value) 设置绕 Y 轴旋转度数
x(float value) 相对于父容器的左上角坐标在 X轴方向的最终位置
y(float value) 相对于父容器的左上角坐标在Y轴方向的最终位置
alphaBy(float value) 设置透明度增量
rotationBy(float value) 设置绕Z轴旋转增量
rotationXBy(float value) 设置绕 X 油旋转增量
rotationYBy(float value) 设置统Y轴旋转增量
translationXBy(float value) 设置X轴方向的移动值增量
translationYBy(float value) 设置Y轴方向的移动值增量
scaleXBy(float value) 设置X轴方向的缩放大小增量
scaleYBy(float value) 设置 Y轴方向的缩放大小增量
xBy(float value) 相对于父容器的左上角坐标在 X轴方向的位置增量
yBy(float value) 相对于父容器的左上角坐标在 Y轴方向的位置增量
setlnterpolator(Timelnterpolator interpolator) 设置插值器
setStartDelay(long startDelay) 设置开始延时
setDuration(long duration) 设置动画时长

四、animateLayoutChanges

 android:animateLayoutChanges="true"
在Layout加入控件,或者移除控件的时候,添加动画,但是只能使用默认动画。

 <LinearLayout
            android:animateLayoutChanges="true"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"/>

五、LayoutTransition

LayoutTransition可以控制ViewGroup的动画,可以使用自定义的动画。
具体使用:

 public void doLayoutTransition(){

        LinearLayout linearLayout=new LinearLayout(this);

        //1.创建实例
        LayoutTransition transition=new LayoutTransition();

        //2.创建动画
        ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(null,"rotation",0f,90f,0f);

        //3.动画出现形式进行设置
        transition.setAnimator(LayoutTransition.DISAPPEARING,objectAnimator);

        //4.将LayoutTransition设置到ViewGroup中
        linearLayout.setLayoutTransition(transition);
     
      	//5.开源动画库 NineOldAndroids
     
    }

setAnimator(int transitionType, Animator animator)

这个方法中,transitionType有五个选项

image.png

CHANGE_APPEARING  由于容器中要显示一个新的元素,其他需要变化的元素所应用的动画(问题多,不常用)

_CHANGE_DISAPPEARING_  当个容器中某个元素要消失时,其他需要变化的元素所应用的动画(问题多,不常用)

_CHANGING_  容器中正在更改的元素的动画变化

_APPEARING_  元素在容器中出现时所定义的动画

_DISAPPEARING_  元素在容器中消失时所定义的动画

六、PathMeasure

PathMeasure类似一个计算器,可以计算出目标path的坐标,长度等

1.初始化

  public void doPathMeasure(){
        Path path=new Path();

        //初始化方法1
        PathMeasure pathMeasure1=new PathMeasure();
        pathMeasure1.setPath(path,true);

        //初始化方法2
        PathMeasure pathMeasure2=new PathMeasure(path,false);
    }

setPath(Path path, boolean forceClosed)
path    就是代表要计算的目标Path。
forceClosed    是否闭合,true会计算闭合状态下的Path,false会按照Path原来情况来计算。

2.函数调用

自定义一个view

public class PathView extends View {
    Path mPath;
    Paint mPaint;
    PathMeasure mPathMeasure;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPath=new Path();
        mPaint=new Paint();
        mPathMeasure=new PathMeasure();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.translate(250,250); //画布移动
        mPaint.setColor(Color.BLUE); //画笔颜色
        mPaint.setStrokeWidth(5); //画笔粗细
        mPaint.setStyle(Paint.Style.STROKE); //画笔风格

        mPath.moveTo(0,0);
        mPath.lineTo(0,100);
        mPath.lineTo(100,100);
        mPath.lineTo(100,0);

        mPathMeasure.setPath(mPath,true);
        Log.v("showLog",
                "getLength()=="+mPathMeasure.getLength()
                        +"  isClosed()=="+ mPathMeasure.isClosed()); //结果400.0  true

        mPathMeasure.setPath(mPath,false);
        Log.v("showLog",
                "getLength()=="+mPathMeasure.getLength()
                        +"  isClosed()=="+ mPathMeasure.isClosed()); //结果300.0  false

        canvas.drawPath(mPath,mPaint); //绘制路径
    }
}

绘制效果:

image.png

2.1 PathMeasure.getLength()

PathMeasure.getLength() 函数用于测量路径的长度

2.2 PathMeasure.isClosed()

PathMeasure.isClosed() 函数用于返回是否测量闭合状态

2.3 PathMeasure.nextContour()

   		mPath.addRect(-50, -50, 50, 50, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPath.addRect(-100, -100, 100, 100, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPath.addRect(-120, -120, 120, 120, Path.Direction.CW);
        canvas.drawPath(mPath, mPaint);

        mPathMeasure.setPath(mPath, false);

        do {
            float len = mPathMeasure.getLength();
            Log.v("showLog", "len=" + len);
        } while (mPathMeasure.nextContour());

效果:

image.png

打印结果:

len=400.0
len=800.0
len=960.0

PathMeasure.nextContour()得到的顺序与添加的Path的顺序相同

PathMeasure.getLength()只是得到当前path的长度,不是全部的长度

2.3 getSegment()

使用getSegment函数需要禁用硬件加速 在构造方法中加入
setLayerType(LAYER_TYPE_SOFTWARE,null);

 		mPath.addRect(-50, -50, 50, 50, Path.Direction.CW);
        mPathMeasure.setPath(mPath,false); //计算的path
        mPathMeasure.getSegment(0,150,mDstPath,true); //截取并添加到mDstPath,是添加,不是其他
        canvas.drawPath(mPath, mPaint); //绘制原来的path

        canvas.translate(200,0); //画布移动
        mPaint.setColor(Color.RED);
        canvas.drawPath(mDstPath, mPaint); //绘制添加后的mDstPath
 boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

startD path开始截取的点,截取的起始点,是以左上角的点开始的

stopD 截取停止的点

dst 截取后添加到的path

startWithMoveTo  是否保存原状,true保存原样,false则会连接初始点和终点,和原来的不一定相同形状
以上代码的效果: 截图的方向,与原来的path的生成方向有关

image.png

2.4 动态画圆的例子

代码:


public class PathView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();

        mPaint.setColor(Color.BLUE); //画笔颜色
        mPaint.setStrokeWidth(5); //画笔粗细
        mPaint.setStyle(Paint.Style.STROKE); //画笔风格

        mPath.addCircle(100, 100, 50, Path.Direction.CW); //一个完整的圆
        mPathMeasure.setPath(mPath, true); //要计算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1); //进度 0~1
        animator.setRepeatCount(ValueAnimator.INFINITE); //无限循环
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到当前的进度
                invalidate();//重绘,重新执行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(100, 100); //画布移动

        float stop=mPathMeasure.getLength()*mCurAnimValue; //一个进度确定一个截取点
        mDstPath.reset();
        mPathMeasure.getSegment(0,stop,mDstPath,true); //一点点添加

        canvas.drawPath(mDstPath,mPaint); //每次有进度更新,就绘制一小段截取
    }
}

效果:
动态画圆.gif

2.5 getPosTan()

先看看函数的定义:

boolean getPosTan(float distance, float pos[], float tan[]) 

float distance 距离path的其实长度

float pos[]  该点的坐标值。x和y pos[0]=x,pos[1]=y

float tan[] 该点的正切值。x和y pos[0]=x,pos[1]=y  tan<a=y/x

2.6 箭头画圆的例子

代码:


public class PathView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;
    Bitmap mArrowBmp;
    float[] mPos;
    float[] mTan;
    int mCenterX,mCenterY;
    float mRadius;

    public PathView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();
        mPos=new float[2];
        mTan=new float[2];

        //加载箭头图片
        mArrowBmp= BitmapFactory.decodeResource(getResources(), R.drawable.arrow);

        mPaint.setColor(Color.BLUE); //画笔颜色
        mPaint.setStrokeWidth(5); //画笔粗细
        mPaint.setStyle(Paint.Style.STROKE); //画笔风格

        mPath.addCircle(540, 972, 486, Path.Direction.CW); //一个完整的圆
        mPathMeasure.setPath(mPath, true); //要计算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1); //进度 0~1
        animator.setRepeatCount(ValueAnimator.INFINITE); //无限循环
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到当前的进度
                invalidate();//重绘,重新执行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        /*
         * 得到h,w的最小的那个值;
         * >> 1 移位 跟 /2 相同;
         * 乘以0.9f,表示占布局的90%
         * */
        mRadius = (Math.min(h, w) >> 1) * 0.9f;

        // 中心坐标
        mCenterX = w / 2;
        mCenterY = h / 2;
        Log.v("showLog",mCenterX+"  "+mCenterY+"  "+mRadius);
        postInvalidate();
        super.onSizeChanged(w, h, oldw, oldh);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float stop=mPathMeasure.getLength()*mCurAnimValue; //一个进度确定一个截取点

        mDstPath.reset();
        mPathMeasure.getSegment(0,stop,mDstPath,true); //一点点添加

        canvas.drawPath(mDstPath,mPaint); //每次有进度更新,就绘制一小段截取

        mPathMeasure.getPosTan(stop,mPos,mTan); //获得每点的正切值和坐标

        /**
         *    Math.atan2(mTan[1],mTan[0])获得tan的弧度值
         *    *180.0/Math.PI将转化为角度值
         * */
        float degrees=(float)(Math.atan2(mTan[1],mTan[0])*180.0/Math.PI);

        Matrix matrix=new Matrix();

        /**
         * 将图片围绕中心点旋转指定角度
         * postRotate(float degrees, float px, float py)
         * degrees是角度  (px,py)是图片中心点
         * */
        matrix.postRotate(degrees,mArrowBmp.getWidth()/2,mArrowBmp.getHeight()/2);

        /**
         * 将图片从默认的(0,0)点移动到路径的最前端
         * */
        matrix.postTranslate(mPos[0]-mArrowBmp.getWidth()/2,mPos[1]-mArrowBmp.getHeight()/2);

        //绘制图片
        canvas.drawBitmap(mArrowBmp,matrix,mPaint);

    }
}

效果:

箭头动态画圆.gif

2.7 getMatrix()

参数类型:

boolean getMatrix(float distance, Matrix matrix, int flags)

使用方法:

  		//计算方位角
        Matrix matrix = new Matrix();

		//获取位置信息
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.POSITION_MATRIX_FLAG);

		//获取切边信息
        mPathMeasure.getMatrix(stop,matrix,PathMeasure.TANGENT_MATRIX_FLAG); 

2.8 支付成功例子

public class TickView extends View {
    Path mPath, mDstPath;
    Paint mPaint;
    PathMeasure mPathMeasure;
    float mCurAnimValue;
    int mCenterX, mCenterY;
    float mRadius;

    public TickView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPath = new Path();
        mDstPath = new Path();
        mPaint = new Paint();
        mPathMeasure = new PathMeasure();

        mPaint.setColor(Color.BLUE); //画笔颜色
        mPaint.setStrokeWidth(5); //画笔粗细
        mPaint.setStyle(Paint.Style.STROKE); //画笔风格

        mCenterX = 540;
        mCenterY = 972;
        mRadius = 486 / 2;

        /**
         * 圆
         * */
        mPath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);

        /**
         * 对勾
         * */
        mPath.moveTo(mCenterX - mRadius / 2, mCenterY);
        mPath.lineTo(mCenterX, mCenterY + mRadius / 2);
        mPath.lineTo(mCenterX + mRadius / 2, mCenterY - mRadius / 3);

        mPathMeasure.setPath(mPath, false); //要计算的path

        ValueAnimator animator = ValueAnimator.ofFloat(0, 2); //进度 0~1 是圆,1~2是对勾
        animator.setRepeatCount(ValueAnimator.RESTART);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurAnimValue = (Float) animation.getAnimatedValue(); //得到当前的进度
                invalidate();//重绘,重新执行onDraw()方法
            }
        });
        animator.setDuration(5000);
        animator.start();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        /*
         * 得到h,w的最小的那个值;
         * >> 1 移位 跟 /2 相同;
         * 乘以0.9f,表示占布局的90%
         * */
        mRadius = (Math.min(h, w) >> 1) * 0.9f;

        // 中心坐标
        mCenterX = w / 2;
        mCenterY = h / 2;
        Log.v("showLog", mCenterX + "  " + mCenterY + "  " + mRadius);
        postInvalidate();
        super.onSizeChanged(w, h, oldw, oldh);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mCurAnimValue < 1) {
            float stop = mPathMeasure.getLength() * mCurAnimValue;
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        } else if (mCurAnimValue == 1) {
            mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
            mPathMeasure.nextContour();
        } else {
            float stop = mPathMeasure.getLength() * (mCurAnimValue - 1);
            mPathMeasure.getSegment(0, stop, mDstPath, true);
        }

        canvas.drawPath(mDstPath, mPaint);
    }
}

效果:
对勾动画.gif

编程中我们会遇到多少挫折?表放弃,沙漠尽头必是绿洲。

posted @ 2020-05-07 17:02  蓝家彬  阅读(418)  评论(0编辑  收藏  举报