Android自定义曲线路径动画框架
sxcr8001
8年前
<h3><strong>最近在一个项目中需要一个像QQ打开个人爱好那样的动画效果如下图:</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/14a24f83cc571f3390c5d720a0d3e52e.gif"></p> <p>可以看出每个小球都是以顺时针旋转出来的,说明像这样的曲线动画用Android中自带的平移动画是很难实现的。</p> <h2><strong>曲线动画怎么画???</strong></h2> <p>我们先来看看Android自带的绘制曲线的方式是怎样的:</p> <p>android自定义View中画图经常用到这几个什么什么To</p> <h2><strong>1、moveTo</strong></h2> <p>moveTo 不会进行绘制,只用于移动移动画笔,也就是确定绘制的起始坐标点。结合以下方法进行使用。</p> <h2><strong>2、lineTo</strong></h2> <p>lineTo 用于进行直线绘制。</p> <pre> <code class="language-java">mPath.lineTo(300, 300); canvas.drawPath(mPath, mPaint);</code></pre> <h3><strong>默认从坐标(0,0)开始绘制。如图:</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/df5188acbbc34079a10bdfbd92ffddd8.png"></p> <p>刚才我们不是说了moveTo是用来移动画笔的吗?</p> <pre> <code class="language-java">mPath.moveTo(100, 100); mPath.lineTo(300, 300); canvas.drawPath(mPath, mPaint);</code></pre> <h3><strong>把画笔移动(100,100)处开始绘制,效果如图:</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/61c7b024446887cc2c0b7f1a619fd3c9.png"></p> <h2><strong>3、quadTo</strong></h2> <p>quadTo 用于绘制圆滑曲线,即贝塞尔曲线。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/196383fce98ea3d26257710474920378.png"></p> <h2><strong>4、cubicTo</strong></h2> <p>cubicTo 同样是用来实现贝塞尔曲线的。mPath.cubicTo(x1, y1, x2, y2, x3, y3) (x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点。那么,cubicTo 和 quadTo 有什么不一样呢?说白了,就是多了一个控制点而已。然后,我们想绘制和上一个一样的曲线,应该怎么写呢?</p> <pre> <code class="language-java">mPath.moveTo(100, 500); mPath.cubicTo(100, 500, 300, 100, 600, 500);</code></pre> <h3><strong>看看效果:</strong></h3> <p style="text-align:center"><img src="https://simg.open-open.com/show/884f509687fadeaef984d11f9c16ce01.png"></p> <p>一模一样!如果我们不加 moveTo 呢?</p> <p>则以(0,0)为起点,(100,500)和(300,100)为控制点绘制贝塞尔曲线:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/635091cfd920b5ca0c8d287289b0567c.png"></p> <p>受到上面的启发,我们也可以用同样的方法来实现一个曲线动画框架</p> <p>在写框架之前我们必须要先了解一样东西:</p> <h2><strong>贝塞尔曲线:</strong></h2> <h3><strong>维基百科中这样说到:</strong></h3> <p>在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。</p> <p>贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。</p> <h2><strong>1、线性贝塞尔曲线</strong></h2> <p>给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:</p> <p><img src="https://simg.open-open.com/show/a23cfd3b674392ab23d31ce4136a5437.png"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/654980a01157642c89cebf8d0f7bb1ec.gif"></p> <h3><strong>二次方贝塞尔曲线</strong></h3> <p>二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:</p> <p><img src="https://simg.open-open.com/show/eed78ec69a22986c6b4b03269f0ec6c7.png"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/514eda91b17146cb992a23d0ebf262ee.gif"></p> <h3><strong>三次方贝塞尔曲线</strong></h3> <p>P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。</p> <h3><strong>曲线的参数形式为:</strong></h3> <p><img src="https://simg.open-open.com/show/f9613ad29b5ab2d88e07af5ece42c4cb.png"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/043dbd133e4d7d6485c5d3b80a573fd5.gif"></p> <p>以上都是维基百科给出的定义,以及不同曲线的公式和效果图; 如果不清楚可以自己百度搜索或者维基百科搜索,么么哒!</p> <h2><strong>那么在上代码之前先看看我们最后实现出来的效果图:</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/4f2f123882d9831485edee0461c2325d.gif"></p> <p>运动路径自己想怎么设置就怎么设置,是不是感觉很装逼,好了下面正式开撸...</p> <h2><strong>先看看项目整体结构:</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/b0bc66064775d4257515ed487922358c.png"></p> <h2><strong>下面是代码时间</strong></h2> <h2><strong>PathPoint.java中的代码:</strong></h2> <pre> <code class="language-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); } }</code></pre> <p>这个类主要是用来记录View移动动作的坐标点,通过不同的构造函数传入不同的参数来区分不同的移动轨迹,注释写的很清楚的...</p> <h2><strong>为了让不同类型的移动方式都能在使用时一次性使用我写了一个AnimatorPath类</strong></h2> <pre> <code class="language-java">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; } }</code></pre> <p>该类是最终在客户端使用的,记录一系列的不同移动轨迹,使用时调用里面的方法就可以添加不同的移动轨迹最后通过 getPoints() 来得到所有的移动轨迹集合</p> <p>在Android自带的绘制曲线的方法中都是只是通过 moveTo() 方法设置起始点,在其它的方法中只是传入了终点或控制点坐标。实际上我们要画连续的曲线或连续的移动时,都需要知道起点到终点的之间所有的坐标,哪么怎么来的到这些点的坐标?</p> <p>Android中为我们提供了一个泛型的接口: TypeEvaluator<T> 可以很简单的实现这个难题。这里我就把它叫做"估值器".我们只要创建一个类来实现这个接口,然后通过自己计算公式(就是我们上面的贝塞尔曲线公式)</p> <h2><strong>下面来看看我项目中的估值器类:PathEvaluator</strong></h2> <pre> <code class="language-java">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); } }</code></pre> <p>泛型中传入我们自己的定义的 PathPoint 类;其实这些复杂的计算代码很简单,就是上面贝塞尔曲线的公式,将需要的点直接带入公式即可,我相信仔细看看会明白的!</p> <h2><strong>核心代码到这里就没有了,下面看看MainActivity中的代码:</strong></h2> <pre> <code class="language-java">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; } } }</code></pre> <p>上面代码中的: setPath() 方法根据你自己项目的需要来设置不同的坐标 注意:("这里的坐标是View以当前位置的偏移坐标,不是绝对坐标")</p> <p>上面代码中的: startAnimatorPath() 参数就不介绍了注释中写的很清楚;这里直接看看 ObjectAnimator.ofObject() 方法的使用把:</p> <p>ObjectAnimator.ofObject(this, propertyName, new PathEvaluator(), path.getPoints().toArray())</p> <p>参数:this:View</p> <p>参数:propertyName:属性名字 :起始这个名字是一个反射机制的调用,这样说不明白,看看这条代码:</p> <pre> <code class="language-java">ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f).setDuration(500).start();</code></pre> <p>相信这句代码都能看懂,其中"scaleX"就相当于参数:propertyName</p> <p>项目代码中我们传入的参数是:</p> <pre> <code class="language-java">startAnimatorPath(fab, "fab", path);</code></pre> <p>"fab"参数其实对应的就是 setFab(PathPoint newLoc) 方法,当我们在当前类中定义了该方法,就会自动通过反射的机制来调用该方法! ,如果还不懂,可以看看其它大神写的博客!</p> <h2><strong>看看Xml中的代码:</strong></h2> <pre> <code class="language-java"><?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></code></pre> <h2><strong>为了可以清晰的看见小球的移动轨迹,自定义了以个View来显示小球的运动轨迹:</strong></h2> <pre> <code class="language-java">/** * 时 间: 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); } }</code></pre> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/f64c3cd25f67</p> <p> </p>