Android自定义曲线路径动画框架

原文地址声明:https://blog.csdn.net/qq_23179075/article/details/53107131

项目地址Github:https://github.com/azhengyongqin/CustomAnimationFramework/tree/master

最近在一个项目中需要一个像QQ打开个人爱好那样的动画效果如下图:
在这里插入图片描述

可以看出每个小球都是以顺时针旋转出来的,说明像这样的曲线动画用Android中自带的平移动画是很难实现的。

曲线动画怎么画?

我们先来看看Android自带的绘制曲线的方式是怎样的:
android自定义View中path常用函数

1、moveTo()

moveTo 不会进行绘制,只用于移动移动画笔,也就是确定绘制的起始坐标点。结合以下方法进行使用。

2、lineTo()

lineTo() 用于进行直线绘制。

mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

默认从坐标(0,0)开始绘制。如图:
在这里插入图片描述
moveTo()是用来移动画笔起点

mPath.moveTo(100, 100);
mPath.lineTo(300, 300);
canvas.drawPath(mPath, mPaint);

把画笔移动(100,100)处开始绘制,效果如图:在这里插入图片描述

3、quadTo()

quadTo() 用于绘制圆滑曲线,即贝塞尔曲线
在这里插入图片描述

4、cubicTo()

cubicTo() 同样是用来实现贝塞尔曲线的。mPath.cubicTo(x1, y1, x2, y2, x3, y3) (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3)为结束点。那么,cubicTo()quadTo() 有什么不一样呢?说白了,就是多了一个控制点而已。然后,我们想绘制和上一个一样的曲线,应该怎么写呢?

mPath.moveTo(100, 500);
mPath.cubicTo(100, 500, 300, 100, 600, 500);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-73Umyowb-1618822329928)(http://i.imgur.com/bWTPsoZ.png)]

一模一样!如果我们不加 moveTo
则以(0,0)为起点,(100,500)和(300,100)为控制点绘制贝塞尔曲线:
在这里插入图片描述

贝塞尔曲线:

受到上面的启发,我们也可以用同样的方法来实现一个曲线动画框架
在写框架之前我们必须要先了解一样东西:

维基百科地址:https://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A

那么在上代码之前先看看我们最后实现出来的效果图:
在这里插入图片描述

先看看项目整体结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXevbWB8-1618822329933)(http://i.imgur.com/YgRFcbR.png)]

PathPoint.java中的代码

/**
 * Created by zhengliang on 2016/10/15 0015.
 * 记录view移动动作的坐标点
 */

public class PathPoint {
    /**
     * 起始点操作
     */
    public static final int MOVE=0;
    /**
     * 直线路径操作
     */
    public static final int LINE=1;
    /**
     * 二阶贝塞尔曲线操作
     */
    public static final int SECOND_CURVE =2;
    /**
     * 三阶贝塞尔曲线操作
     */
    public static final int THIRD_CURVE=3;
    /**
     * View移动到的最终位置
     */
    public float mX,mY;
    /**
     * 控制点
     */
    public float mContorl0X,mContorl0Y;
    public float mContorl1X,mContorl1Y;
    //操作符
    public int mOperation;

    /**
     * Line/Move都通过该构造函数来创建
     */
    public PathPoint(int mOperation,float mX, float mY ) {
        this.mX = mX;
        this.mY = mY;
        this.mOperation = mOperation;
    }

    /**
     * 二阶贝塞尔曲线
     * @param mX
     * @param mY
     * @param mContorl0X
     * @param mContorl0Y
     */
    public PathPoint(float mContorl0X, float mContorl0Y,float mX, float mY) {
        this.mX = mX;
        this.mY = mY;
        this.mContorl0X = mContorl0X;
        this.mContorl0Y = mContorl0Y;
        this.mOperation = SECOND_CURVE;
    }

    /**
     * 三阶贝塞尔曲线
     * @param mContorl0x
     * @param mContorl0Y
     * @param mContorl1x
     * @param mContorl1Y
     * @param mX
     * @param mY
     */
    public PathPoint(float mContorl0x, float mContorl0Y, float mContorl1x, float mContorl1Y,float mX, float mY) {
        this.mX = mX;
        this.mY = mY;
        this.mContorl0X = mContorl0x;
        this.mContorl0Y = mContorl0Y;
        this.mContorl1X = mContorl1x;
        this.mContorl1Y = mContorl1Y;
        this.mOperation = THIRD_CURVE;
    }

    /**
     * 为了方便使用都用静态的方法来返回路径点
     */
    public static PathPoint moveTo(float x, float y){
        return new PathPoint(MOVE,x,y);
    }
    public static PathPoint lineTo(float x,float y){
        return  new PathPoint(LINE,x,y);
    }
    public static PathPoint secondBesselCurveTo(float c0X, float c0Y,float x,float y){
        return new PathPoint(c0X,c0Y,x,y);
    }
    public static PathPoint thirdBesselCurveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y){
        return new PathPoint(c0X,c0Y,c1X,c1Y,x,y);
    }
}

这个类主要是用来记录View移动动作的坐标点,通过不同的构造函数传入不同的参数来区分不同的移动轨迹,注释写的很清楚的…

为了让不同类型的移动方式都能在使用时一次性使用我写了一个 AnimatorPath类

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Created by zhengliang on 2016/10/15 0015.
 * 客户端使用类,记录一系列的不同移动轨迹
 */

public class AnimatorPath {
    //一系列的轨迹记录动作
    private List<PathPoint> mPoints = new ArrayList<>();

    /**
     * 移动位置到:
     * @param x
     * @param y
     */
    public void moveTo(float x,float y){
        mPoints.add(PathPoint.moveTo(x,y));
    }

    /**
     * 直线移动
     * @param x
     * @param y
     */
    public void lineTo(float x,float y){
        mPoints.add(PathPoint.lineTo(x,y));
    }

    /**
     * 二阶贝塞尔曲线移动
     * @param c0X
     * @param c0Y
     * @param x
     * @param y
     */
    public void secondBesselCurveTo(float c0X, float c0Y,float x,float y){
        mPoints.add(PathPoint.secondBesselCurveTo(c0X,c0Y,x,y));
    }

    /**
     * 三阶贝塞尔曲线移动
     * @param c0X
     * @param c0Y
     * @param c1X
     * @param c1Y
     * @param x
     * @param y
     */
    public void thirdBesselCurveTo(float c0X, float c0Y, float c1X, float c1Y, float x, float y){
        mPoints.add(PathPoint.thirdBesselCurveTo(c0X,c0Y,c1X,c1Y,x,y));
    }
    /**
     *
     * @return  返回移动动作集合
     */
    public Collection<PathPoint> getPoints(){
        return mPoints;
    }
}

该类是最终在客户端使用的,记录一系列的不同移动轨迹,使用时调用里面的方法就可以添加不同的移动轨迹最后通过getPoints()来得到所有的移动轨迹集合

在Android自带的绘制曲线的方法中都是只是通过moveTo()方法设置起始点,在其它的方法中只是传入了终点或控制点坐标。实际上我们要画连续的曲线或连续的移动时,都需要知道起点到终点的之间所有的坐标,哪么怎么来的到这些点的坐标?

Android中为我们提供了一个泛型的接口:TypeEvaluator<T>可以很简单的实现这个难题。这里我就把它叫做"估值器".我们只要创建一个类来实现这个接口,然后通过自己计算公式(就是我们上面的贝塞尔曲线公式)

下面来看看我项目中的估值器类:PathEvaluator


import android.animation.TypeEvaluator;

/**
 * Created by zhengliang on 2016/10/15 0015.
 * 估值器类,实现坐标点的计算
 */

public class PathEvaluator implements TypeEvaluator<PathPoint> {

    /**
     * @param t          :执行的百分比
     * @param startValue : 起点
     * @param endValue   : 终点
     * @return
     */
    @Override
    public PathPoint evaluate(float t, PathPoint startValue, PathPoint endValue) {
        float x, y;
        float oneMiunsT = 1 - t;
        //三阶贝塞尔曲线
        if (endValue.mOperation == PathPoint.THIRD_CURVE) {
            x = startValue.mX*oneMiunsT*oneMiunsT*oneMiunsT+3*endValue.mContorl0X*t*oneMiunsT*oneMiunsT+3*endValue.mContorl1X*t*t*oneMiunsT+endValue.mX*t*t*t;
            y = startValue.mY*oneMiunsT*oneMiunsT*oneMiunsT+3*endValue.mContorl0Y*t*oneMiunsT*oneMiunsT+3*endValue.mContorl1Y*t*t*oneMiunsT+endValue.mY*t*t*t;
        //二阶贝塞尔曲线
        }else if(endValue.mOperation == PathPoint.SECOND_CURVE){
            x = oneMiunsT*oneMiunsT*startValue.mX+2*t*oneMiunsT*endValue.mContorl0X+t*t*endValue.mX;
            y = oneMiunsT*oneMiunsT*startValue.mY+2*t*oneMiunsT*endValue.mContorl0Y+t*t*endValue.mY;
        //直线
        }else if (endValue.mOperation == PathPoint.LINE) {
            //x起始点+t*起始点和终点的距离
            x = startValue.mX + t * (endValue.mX - startValue.mX);
            y = startValue.mY + t * (endValue.mY - startValue.mY);
        } else {
            x = endValue.mX;
            y = endValue.mY;
        }
        return PathPoint.moveTo(x,y);
    }
}

泛型中传入我们自己的定义的PathPoint类;其实这些复杂的计算代码很简单,就是上面贝塞尔曲线的公式,将需要的点直接带入公式即可,我相信仔细看看会明白的!

MainActivity中的代码

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private FloatingActionButton fab;
    private AnimatorPath path;//声明动画集合
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.fab = (FloatingActionButton) findViewById(R.id.fab);

        setPath();

        fab.setOnClickListener(this);
    }
    /*设置动画路径*/
    public void setPath(){
        path = new AnimatorPath();
        path.moveTo(0,0);
        path.lineTo(400,400);
        path.secondBesselCurveTo(600, 200, 800, 400); //订单
        path.thirdBesselCurveTo(100,600,900,1000,200,1200);
    }

    /**
     * 设置动画
     * @param view
     * @param propertyName
     * @param path
     */
    private void startAnimatorPath(View view, String propertyName, AnimatorPath path) {
        ObjectAnimator anim = ObjectAnimator.ofObject(this, propertyName, new PathEvaluator(), path.getPoints().toArray());
        anim.setInterpolator(new DecelerateInterpolator());//动画插值器
        anim.setDuration(3000);
        anim.start();
    }

    /**
     * 设置View的属性通过ObjectAnimator.ofObject()的反射机制来调用
     * @param newLoc
     */
    public void setFab(PathPoint newLoc) {
        fab.setTranslationX(newLoc.mX);
        fab.setTranslationY(newLoc.mY);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.fab:
                startAnimatorPath(fab, "fab", path);
                break;
        }
    }
}

上面代码中的:setPath()方法根据你自己项目的需要来设置不同的坐标 注意:(“这里的坐标是View以当前位置的偏移坐标,不是绝对坐标”)

上面代码中的:startAnimatorPath()参数就不介绍了注释中写的很清楚

ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f).setDuration(500).start();

相信这句代码都能看懂,其中"scaleX"就相当于参数:propertyName

项目代码中我们传入的参数是:

startAnimatorPath(fab, "fab", path);

"fab"参数其实对应的就是setFab(PathPoint newLoc)方法,当我们在当前类中定义了该方法,就会自动通过反射的机制来调用该方法!

看看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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    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="zhengliang.com.customanimationframework.MainActivity">

    <zhengliang.com.customanimationframework.CustomView.PathView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:targetApi="lollipop" />
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="40dp"
        android:layout_height="40dp"
        />
</RelativeLayout>

为了可以清晰的看见小球的移动轨迹,自定义了以个View来显示小球的运动轨迹:

/**
 * 时 间: 2016/11/8 0008
 * 作 者: 郑亮
 * Q  Q : 1023007219
 */

public class PathView extends View {

    private Paint paint;


    public PathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        //抗锯齿
        paint.setAntiAlias(true);
        //防抖动
        paint.setDither(true);
        //设置画笔未实心
        paint.setStyle(Paint.Style.STROKE);
        //设置颜色
        paint.setColor(Color.GREEN);
        //设置画笔宽度
        paint.setStrokeWidth(3);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Path path = new Path();
        path.moveTo(60,60);
        path.lineTo(460,460);
        path.quadTo(660, 260, 860, 460); //订单
        path.cubicTo(160,660,960,1060,260,1260);
        canvas.drawPath(path,paint);
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值