自定义控件——动画进阶

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_43847987/article/details/100602551

1.1 利用PathMeasure实现路径动画

androidSDK提供了一个非常有用的API来帮助开发者实现这样一个Path路径点的坐标追踪,这个API就是PathMeasure,通过它就可以是实现复杂的动画效果

1.1.1 初始化PathMeasure方法

方法1:
PathMeasure pathMeasure=new PathMeasure();
setPath(Path path,boolean forceClosed)
使用pathMeasure.setPath()函数将Path与PathMeasure进行绑定

方法2:
通过PathMeasure的另外的构造方法直接完成初始化
PathMeasure(Path path,boolean forceClosed);
在这个构造函数与setPath函数中都有forceClosed这个参数,这个参数的含义是Path最终是否需要闭合,如果为true,则不管关联的Path是否是闭合的,都会被闭合;

1.1.2 简单函数使用

1.getLength()
public float getLength();
pathMeasure.getLength()函数就是用来计算路径的长度。getLength()函数是针对当前曲线,而不是整个Path。
我们创建一个控件,在onDraw()中重写以下内容:

canvas.translate(50,50);

        Paint paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);

        Path path=new Path();

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

        PathMeasure measure1=new PathMeasure(path,false);
        PathMeasure measure2=new PathMeasure(path,true);

        Log.e("qwe","forceClosed----->"+measure1.getLength());
        Log.e("qwe","forceClosed----->"+measure2.getLength());

        canvas.drawPath(path,paint);

运行截图如下:
在这里插入图片描述
在这里插入图片描述
很明显,当我们设置forceClosed为false时,测量的是当前Path状态的长度;如果为true,不论Path是否闭合,测试的都是Path的闭合长度。
2.isClosed()函数
public boolean isClosed();
用于判断测量Path时是否计算闭合,所以如果在关联Path的时候设置forceClosed为true,则这个函数的返回值一定为true
3.nextContour()函数
用于跳转带下一条曲线的函数,如果跳转成功返回true,失败返回false,相对于getlength();getSegment()这些函数都只会对其中第一条线段进行计算。以下是一个path路径跳转的实例:
onDraw()中代码如下所示:

    canvas.translate(250,250);

        Paint paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);

        Path path=new Path();

        path.addRect(-50,-50,50,50,Path.Direction.CW);
        canvas.drawPath(path,paint);
        path.addRect(-100,-100,100,100,Path.Direction.CW);
        canvas.drawPath(path,paint);
        path.addRect(-120,-120,120,120,Path.Direction.CW);
        canvas.drawPath(path,paint);

        PathMeasure measure1=new PathMeasure(path,false);

        do{
            float len = measure1.getLength();
            Log.d("qwe", "len="+len);
        }while (measure1.nextContour());

运行截图:
在这里插入图片描述
打印日志如下:
在这里插入图片描述
从打印日志中可以看到nextContour的跳转过程。

1.1.3 getSegment()函数

boolean getSegment (float startD; float stopD; Path dst; boolean startWithMoveTo)

这个API用于截取整个Path中的某个片段,startD和stopD用来控制长度,并将截取后的Path保存在参数dst中。最后一个参数表示起始点是否使用moveTo将路径的新起始点移到Path的起始点,通常设置为true,以保证每次截取的Path都是正常的。通常和dst一起使用;因为dst中保存的Path是被不断添加的,而不是每次被覆盖的;如果设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保证截取的Path片段是连续的。
使用实例:

  canvas.translate(250,250);

        Paint paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);

        Path path=new Path();

        path.addRect(-50,-50,50,50,Path.Direction.CW);

//        canvas.drawPath(path,paint);

        Path dst = new Path();

        PathMeasure pathMeasure = new PathMeasure(path,false);

        pathMeasure.getSegment(0,150,dst,true);

        canvas.drawPath(dst,paint);

运行截图:

在这里插入图片描述
该图是移动坐标和后,绘制的矩形图案。
在这里插入图片描述
这幅图是使用getSegment截取的路径,从这个街区结果可以看出:
路径截取是以路径的左上角为起始点开始的。
还有截取路径的方向是由Path.Direction.CW(顺时针方向)决定的,若设置为Path.Direction.CCW则是逆时针截取。

实例二当dst中不为空,存储有路径时:

 Path path=new Path();

        path.addRect(-50,-50,50,50,Path.Direction.CW);
        

        Path dst = new Path();
        dst.lineTo(20,100);

        PathMeasure pathMeasure = new PathMeasure(path,false);

        pathMeasure.getSegment(0,150,dst,true);

        canvas.drawPath(dst,paint);

运行截图如下:
在这里插入图片描述
这个实例中dst出事并不是空的是一条线段,从(0,0)到(20,100);从结果中看,dst中原有的线段被保留下来了,所以的得出结论:dst是将截取的路径添加到dst中,而不是替换原有内容。

实例三:当startWithMoveTo为false
还是上面的代码,将参数改为false:

pathMeasure.getSegment(0,150,dst,false);

运行截图:
在这里插入图片描述
这个看起来是不是很迷糊其实它的实现是这样的:
在这里插入图片描述
得出结论:若startWithMoveTo为True,则被截取出来的是Path片段保持原状,为false则将Path片段的起始点移动到dst的最后一个点,以保证dst路径的连续性。

2.示例加载动画
以下是该动画的代码实现:

 public Weigetm(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);
        path=new Path();
        Path circlePath= new Path();
        circlePath.addCircle(100,100,50,Path.Direction.CW);
        pathMeasure = new PathMeasure(circlePath,true);
        final ValueAnimator animator = ValueAnimator.ofFloat(0,1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currentValue = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();
    }

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        float stop = pathMeasure.getLength()*currentValue;
        path.reset();
        pathMeasure.getSegment(0,stop,path,true);
        canvas.drawPath(path,paint);
    }

运行截图如下:
在这里插入图片描述
在这里插入图片描述
这个动画就是实现一条圆形路径从长度为0增加到整个圆,如此反复。

1.1.4 getPosTan 函数

//getPosTan函数用于得到路径上某一长度的位置以及该位置的正切值
boolean getPosTan(float distance ,float[] pos;float[] tan);

参数:

  • float distance :距离Path起始点的长度,取值范围0<=distance<=getLength
  • float [] pos:该店的坐标值
  • float [] tan: 该点的正切值

箭头加载动画实例:图中的箭头就需要实时的改变角度

private Path mCirclePath,mDstPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Float mCurrAnimValue;
    private Bitmap mArrawBmp;
    //tan pos 在使用时必须先使用new关键词分配存储空间
    private float [] pos = new float[2];
    private float [] tan = new float[2];
    
    public Weigetm(Context context, AttributeSet attrs) {
        super(context, attrs);
        mArrawBmp= BitmapFactory.decodeResource(getResources(),R.drawable.arraw);
        mPaint=new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mDstPath=new Path();
        mCirclePath= new Path();
        mCirclePath.addCircle(100,100,300,Path.Direction.CW);
        mPathMeasure = new PathMeasure(mCirclePath,true);
        final ValueAnimator animator = ValueAnimator.ofFloat(0,1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCurrAnimValue = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(2000);
        animator.start();
    }

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(300,300);
        canvas.drawColor(Color.WHITE);
        float stop = mPathMeasure.getLength()*mCurrAnimValue;
        mDstPath.reset();
        mPathMeasure.getSegment(0,stop,mDstPath,true);
        canvas.drawPath(mDstPath,mPaint);

       //旋转箭头图片,并绘制
        mPathMeasure.getPosTan(stop,pos,tan);//向数组中的元素赋值
        float degrees=(float) (Math.atan2(tan[1],tan[0])*180.0/Math.PI);//将得到的弧度值转化为角度值
        Matrix matrix=new Matrix();
        matrix.postRotate(degrees,mArrawBmp.getWidth()/2,mArrawBmp.getHeight()/2);//将图片围绕中心点旋转指定的角度,以便和切线重合
        matrix.postTranslate(pos[0]-mArrawBmp.getWidth()/2,pos[1]-mArrawBmp.getHeight()/2);//将图片从默认的(0,0)点移动到当前路劲的最前端
        canvas.drawBitmap(mArrawBmp,matrix,mPaint);

    }

运行截图如下:
在这里插入图片描述
在这里插入图片描述
图中的箭头是我们准备的图片。

1.1.5 getMatrix 函数

//这个函数用于得到路径上某一长度以及该位置的正切值的矩阵
boolean getMatrix(float distance, Matrix matrix, int flags)
  • distance 距离path起始点的长度
  • matrix 根据flags 封装好的matrix会根据flags的值设置而存入不同的内容
  • flags 用于指定哪些内容会存入matrix中,flags 的值有两个:PathMeasure.POSITION_MATRIX_FLAG表示获取位置信息;pathMeasure.TANGENT_MATRIX_FLAG表示获取切边的信息,使得图片按Path旋转。可以同时指定。

该函数很明显和getPosTan的实现效果是一样的,实现上面的例子只需要修改如下代码:
在这里插入图片描述
1.1.6 实例:支付宝支付成功动画
以下时是实现动画的代码:

private  float mRadius=200;
    private  float mCentY=300;
    private  float mCentX=300;
    private Path mCirclePath,mDstPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Float mCurrAnimValue;

public Weigetm(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint=new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
		//dst容器
        mDstPath=new Path();
        //Path路径
        mCirclePath= new Path();

        mCirclePath.addCircle(mCentX,mCentY,mRadius,Path.Direction.CW);

        mCirclePath.moveTo(mCentX-mRadius/2,mCentY);
        mCirclePath.lineTo(mCentX,mCentY+mRadius/2);
        mCirclePath.lineTo(mCentX+mRadius/2,mCentY-mRadius/3);

        mPathMeasure = new PathMeasure(mCirclePath,false);

        final ValueAnimator animator = ValueAnimator.ofFloat(0,2);


        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCurrAnimValue = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(4000);
        animator.start();
    }


  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
		//画出外圈的圆
        if(mCurrAnimValue<1){
            float stop=mPathMeasure.getLength()*mCurrAnimValue;
            mPathMeasure.getSegment(0,stop,mDstPath,true);
        }else {
        //跳转到圈内,使用nextContour函数
            mPathMeasure.getSegment(0,mPathMeasure.getLength(),mDstPath,true);
            mPathMeasure.nextContour();
            float stop = mPathMeasure.getLength()*(mCurrAnimValue);
            mPathMeasure.getSegment(0,stop,mDstPath,true);
        }

        canvas.drawPath(mDstPath,mPaint);

    }

结果如图:

在这里插入图片描述在这里插入图片描述

1.2 SVG动画

SVG是矢量图,专门用于网络的适量图形标准。与矢量图对应的是位图,Bitmap就是位图,它是由一个个像素点组成。
SVG与Bitmap相比的好处

  • SVG使用XML格式定义图形,可被非常多的工具读取和修改
  • SVG由点来存储,由计算机根据点信息绘图,不会失真,无须根据分辨率适配多套图标
  • SVG的占用空间明显比Bitmap小
  • SVG可以转换为Path路径,与Path动画相结合,可以形成更丰富的动画

SVG在HTML中使用的非常多,如下面一段代码:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect x="25" y="25" width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
<circle cx="125" cy="125" r="75" fill="orange" />
<polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
<line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</svg>

运行截图:
在这里插入图片描述
SVG语法中支持很多标签:

  • rect 标签——绘制矩形
  • circle标签——绘制圆形
  • line标签——绘制线段
  • polyline标签——绘制折线
  • ellipse标签——绘制椭圆
  • polygon标签——绘制多边形
  • path标签——绘制路径

在Android中并没有对原生的SVG图像语法进行支持,而是以一种简化的方式对SVG进行兼容,也就是通过使用它的path标签,几乎可以实现其他所有的标签。

1.2.2 vector标签与图像显示
SVG矢量图是使用标签定义的,放在res/drawable下
如下SVG代码:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="200dp"
    android:height="100dp"
    android:viewportWidth="100"
    android:viewportHeight="50">
    <path
        android:name="bar"
        android:fillColor="@color/colorPrimary"
        android:pathData="M0,0 L100 0 L100,50 L0,50 L0,0 M50,25 L100,25"
        android:strokeWidth="2"
        android:strokeColor="@android:color/darker_gray"/>
</vector>

显示图像为:
在这里插入图片描述整个蓝色区域是我们设置的SVG图形大大小区域,我们使用了白色边框将其显示出来。
vector 参数属性:

  • width与height属性:表示该SVG图形的具体大小
  • viewportWidth与viewportHeight属性:表示SVG图像划分的比例;即将图形均分为多少份。
    像上面的例子我们将宽度分为100个点,高度分为50个点,而M0,0 L100 ,0分别代表将点(0,0)移动到(100,0)。这里的坐标就是以viewportWidth和viewportHeight为单位的,即一个点有2dp。
    vector是指定标签大小;path标签是指定路径的内容。

1.path标签
常用属性:

  • android:name :声明一个标记 ,类似于ID,便于对其做动画的时候顺利地找到该节点。
  • android:pathData :对SVG矢量图的描述。
    • M=moveto(M X,Y):将画笔移动到指定的位置
    • L=lineto(L X,Y):画直线到指定的坐标位置
    • H=horizontal lineto(H X):画水平线到指定的X坐标位置
    • V=vertical lineto(V Y):画垂直线到指定的Y坐标位置
    • C=curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三阶贝济埃曲线
    • S=smooth curveto(S X2,Y2,ENDX,ENDY):三阶贝济埃曲线。将上一条指令终点作为起点
    • Q=quadratic Belzier curve(Q X1,Y1,ENDX,ENDY):二阶贝济埃曲线
    • T=smooth quardratic Belzier curve(T,ENDX,ENDY):映射前面枯井后的终点
    • A=elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线
    • Z=closepath():关闭路径
  • android:strokeWidth :画笔的宽度
  • android:fillColor :填充颜色
  • android:fillAlpha:填充颜色的透明度
  • android:strokeWidth:描边宽度
  • android:strokeHeight:描边透明度
  • android:strokeLineJoin:用于指定折线拐角形状。去只有miter(锐角),round(圆弧),bevel(直线)
  • android:strokeLineCap:画出线条的终点形状
  • android:strokeMiterLimit:设置斜角的上限
  • android:trimPathEnd:指定路径结束的位置,取值为(0,1)
  • android:trimPathStart:指定路径开始的位置,取值为(0,1)
  • android:trimPathOffset:用于指定结果路径的位移距离,取值为(0,1)

2.group标签
用于定义一系列路径或者将Path标签分组。在动画中,我们可以指定每个path 路径做特定的
动画,通过 group 标签则可以将原本由一个 path 路径实现的内容分为多个 path 路径来实现,
每个 path 路径可以指定特定的动画,这样一来,效果显示就丰富多彩了。
在这里插入图片描述

group 签具有以下常用属性。
• ndroid:name :组的名字,用于与动画相关联
• android:rotation :指定该组图像的旋转度数。
• android:pivotX :定义缩放和旋转该组时的X参考点。该值是相对于 vector的viewport
值来指定的。
• android :pivotY :定义缩放和旋转该组时的Y参考点。该值是相对于 vector的viewport
值来指定的。
• android:scaleX :指定该组X轴缩放大小
• android: scaleY:指定该组Y轴缩放大小。
• android:translateX :指定该组沿X轴平移的距离。
• android:translateY:指定 组沿Y轴平移的距离

3.制作SVG图像
方法一:设计软件
如果有绘图基础,则可以直接使用Illustrator或在线SVG工具制作SVG图像,或者通过SVG源文件下载网站后进行编辑。
方法二:Iconfont
它原理是把你想要矢量图标打包成.ttf 文件,在 Android 中应用这个ttf 文件来方便地加载和指定各 种图标。由于是 SVG图像,所以也不存在屏幕适配问题,可以减少各种图标的占用空间。

4.实例
在android种使用ImageView,显示SVG图像
(1)引入兼容包
添加对Appcompat的支持

implementation 'androidx.appcompat:appcompat:1.1.0'

在Android 5.0之前使用Vector,需要aapt来对资源进行一些处理,这一过程可以在aapt的配置中进行设置,如果没有启用这样一个flag,那么在5.0以下的设备上运行就会发生android.content.res.Resources$NotFoundException。

首先,你需要在项目的build.gradle脚本中,增加对Vector兼容性的支持,代码如下所示:

使用Gradle Plugin 2.0以上:

android {
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}

使用Gradle Plugin 2.0以下,Gradle Plugin 1.5以上:

android {
  defaultConfig {
    // Stops the Gradle plugin’s automatic rasterization of vectors
    generatedDensities = []
  }
  // Flag to tell aapt to keep the attribute ids around
  aaptOptions {
    additionalParameters "--no-version-vectors"
  }

(2)生成Vector图像
使用之前的Vector图像:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="200dp"
    android:height="100dp"
    android:viewportWidth="100"
    android:viewportHeight="50">
    <path
        android:name="bar"
        android:pathData="M50,25 L100,25"
        android:strokeWidth="2"
        android:strokeColor="@android:color/darker_gray"/>
</vector>

(3)在ImageView和ImageButton中使用
代码如下:

<androidx.appcompat.widget.AppCompatImageView
		android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        app:srcCompat="@drawable/svg"/>

在java代码中设置

ImageView iv=findViewByid(R.id.iv);
iv.setImageResource(R.drawable.svg);

在这里插入图片描述
(4)在Button和RadioButton中使用
在这两种控件中使用时需要通过selector标签来使用

综合实例

Java代码:


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final ImageView imageView=findViewById(R.id.anim_img);

        //将焦点放在ImageView上
        imageView.setFocusable(true);
        imageView.setFocusableInTouchMode(true);
        imageView.requestFocus();
        imageView.requestFocusFromTouch();
        EditText editText=findViewById(R.id.edit);
        editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View view, boolean b) {
                if(b){
                    AnimatedVectorDrawableCompat animatedVectorDrawableCompat=AnimatedVectorDrawableCompat.create(
                            MainActivity.this,R.drawable.animated_vector_search);
                    imageView.setImageDrawable(animatedVectorDrawableCompat);
                    ((Animatable)imageView.getDrawable()).start();
                }
            }
        });
    }
}

xml文件目录:
在这里插入图片描述
anim_nar_trim_start.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="trimPathStart"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType"
    android:duration="500">
</objectAnimator>

anim_search_trim_end.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:propertyName="trimPathEnd"
    android:valueFrom="0"
    android:valueType="floatType"
    android:valueTo="1">
</objectAnimator>```

animated_vector_search.xml

```java
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/svg">

    <target
        android:animation="@animator/anim_search_trim_end"
        android:name="search"/>

    <target
        android:animation="@animator/anim_nar_trim_start"
        android:name="bar"/>

</animated-vector>

svg.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="24">


    <path
        android:name="search"
        android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
        android:strokeWidth="2"
        android:strokeColor="@color/colorPrimary"/>
    <path
        android:name="bar"
        android:trimPathStart="1"
        android:pathData="M50,25 L100,25"
        android:strokeWidth="2"
        android:strokeColor="@color/colorPrimary"/>

</vector>

运行截图:
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43847987/article/details/100602551
今日推荐