使用DashPathEffect绘制一条动画曲线

来源:互联网 发布:win10桌面软件不见了 编辑:程序博客网 时间:2024/06/04 19:08

引言

以前在做曲线图的时候,一直想实现曲线动态绘制的效果。类似于js图表库中的效果:http://www.highcharts.com/demo/spline-symbols

那个时候没有安卓中没有任何一个原声的图表库实现了这种效果。不管是aChartEngine,还是后来的MPAndroidChart,HelloCharts都没有在曲线图方面做到很好的展示效果。

HelloCharts以及一些其它的图表库(不太出名的)倒是实现了一种曲线的动画效果,类似于:

1441593558951518.png

但这是上下运动,我要的是线条的跟踪绘制动画效果。

是不是在安卓上要实现跟踪绘制非常难呢?

其实不难,只是那个时候大家对于api都不太熟悉。

Romain Guy的方法

直到Romain Guy发表了一篇文章http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/

里面使用path的画笔的DashPathEffect巧妙的实现了这种动画效果。

虽然我的期望是path应该有一个设置自己绘制长度的方法(这样就可以轻易的实现跟踪绘制了,遗憾的看了path的源码也没发现这样的东西存在),但是DashPathEffect这种技巧看上去也算是比较正统,可以算得上时最佳实践了吧。

ps:一会儿我会向你介绍一下我以前在不知道DashPathEffect的情况下是如何实现的。

好了回到Romain Guy用DashPathEffect实现的原理上来。

首先得介绍DashPathEffect这个类。

DashPathEffect

DashPathEffect是PathEffect类的一个子类,可以使paint画出类似虚线的样子,并且可以任意指定虚实的排列方式。

举个例子:

  1. Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
  2. p.setStyle(Style.STROKE);
  3. p.setColor(Color.WHITE);
  4. p.setStrokeWidth(1);
  5. PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 1);
  6. p.setPathEffect(effects);
  7. canvas.drawLine(0, 40, mWidth, 40, p);

代码中的float数组,必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白。

如本代码中,绘制长度1的实线,再绘制长度2的空白,再绘制长度4的实线,再绘制长度8的空白,依次重复。1是起始位置的偏移量。

效果如下:


技巧

然而这跟我们绘制跟踪效果有什么关系呢?

看看这样一个PathEffect:

  1. PathEffect effect = new DashPathEffect(new float[] { length, length }, 0);

我们可以把DashPathEffect的第一个参数(float数组)只填入两个值,都是path的总长度length,那么按照上面对DashPathEffect的解释,第一次绘制一条实线就已经完全绘制完了,间隔的空白区间得不到绘制的机会。事实上这样绘制完全不能产生虚线效果,跟不设置PathEffect是一样的。

但是我们注意第三个参数即起始位置的偏移量现在是为0的。如果我们不为0呢?

比如为100,那么第一次绘制实线就会跳过100的距离,第一次的实线就只能绘制length-100的长度,那么空白区域就可以绘制100的长度,但是你看不见空白,所以我们只会感觉到绘制了一条length-100的路径。

如果你按照我们的思路去做实验,那么很快你就会想到,把这个偏移量也设置成length,那么第一次的实线区间将完全得不到绘制,而直接进入空白区间,而我们的空白区间总长度也是length,因此它占用了全部的绘制区间,所以此时什么也看不到。如果空白区间小于length的话,是可以看到一点实线的(因为空白区间完了紧接着就是实线了)。

所以,我们可以设置一个百分比,取名叫phase,phase的增长是从0 .0-1.0,如果我们利用属性动画来改变它,然后根据它动态的构造一个这样的DashPathEffect:

  1. new DashPathEffect(new float[] { length, length },
  2.         length - phase * length);

这样就能产生跟踪绘制的效果。

获取path的长度

刚刚我们多次提到了path的总长度length,那么对于一条不规则的曲线来讲,要得到其长度是很难的。幸运的是,有相应的api:

  1. // Measure the path
  2. PathMeasure measure = new PathMeasure(path, false);
  3. float length = measure.getLength();

完整的代码

stackoverflow上有人根据Romain Guy的那篇文章写了一个演示的例子(Romain Guy自己也写了一个项目,不过里面并不单单是讲这个的,因此不太好找),在这里:http://stackoverflow.com/questions/5367950/android-drawing-an-animated-line

我把它写在改了下,做成了一个demo。代码不多我直接贴完所有代码:

MainActivity

  1. package com.jcodecraeer.pathanimate;
  2.  
  3. import android.os.Bundle;
  4. import android.support.v7.app.AppCompatActivity;
  5. import android.view.Menu;
  6. import android.view.MenuItem;
  7. import android.view.View;
  8.  
  9. public class MainActivity extends AppCompatActivity {
  10.  
  11.     @Override
  12.     protected void onCreate(Bundle savedInstanceState) {
  13.         super.onCreate(savedInstanceState);
  14.         setContentView(R.layout.activity_main);
  15.         final PathView path_view = (PathView) findViewById(R.id.path);
  16.         path_view.init();
  17.         path_view.setOnClickListener(new View.OnClickListener(){
  18.             @Override
  19.             public void onClick(View v){
  20.                 path_view.init();
  21.             }
  22.         });
  23.     }
  24.  
  25.     @Override
  26.     public boolean onCreateOptionsMenu(Menu menu) {
  27.         // Inflate the menu; this adds items to the action bar if it is present.
  28.         getMenuInflater().inflate(R.menu.menu_main, menu);
  29.         return true;
  30.     }
  31.  
  32.     @Override
  33.     public boolean onOptionsItemSelected(MenuItem item) {
  34.         // Handle action bar item clicks here. The action bar will
  35.         // automatically handle clicks on the Home/Up button, so long
  36.         // as you specify a parent activity in AndroidManifest.xml.
  37.         int id = item.getItemId();
  38.  
  39.         //noinspection SimplifiableIfStatement
  40.         if (id == R.id.action_settings) {
  41.             return true;
  42.         }
  43.  
  44.         return super.onOptionsItemSelected(item);
  45.     }
  46. }

activity_main.xml

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2.     xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  3.     android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
  4.     android:paddingRight="@dimen/activity_horizontal_margin"
  5.     android:paddingTop="@dimen/activity_vertical_margin"
  6.     android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
  7.  
  8.     <com.jcodecraeer.pathanimate.PathView
  9.         android:id="@+id/path"
  10.         android:layout_width="match_parent"
  11.         android:layout_height="match_parent"
  12.         />
  13.  
  14. </RelativeLayout>

PathView

  1. package com.jcodecraeer.pathanimate;
  2.  
  3. import android.animation.ObjectAnimator;
  4. import android.content.Context;
  5. import android.graphics.Canvas;
  6. import android.graphics.Color;
  7. import android.graphics.DashPathEffect;
  8. import android.graphics.Paint;
  9. import android.graphics.Path;
  10. import android.graphics.PathEffect;
  11. import android.graphics.PathMeasure;
  12. import android.util.AttributeSet;
  13. import android.util.Log;
  14. import android.view.View;
  15.  
  16. /**
  17.  * Created by jianghejie on 15/9/7.
  18.  */
  19.  
  20. public class PathView extends View
  21. {
  22.     Path path;
  23.     Paint paint;
  24.     float length;
  25.  
  26.     public PathView(Context context)
  27.     {
  28.         super(context);
  29.     }
  30.  
  31.     public PathView(Context context, AttributeSet attrs)
  32.     {
  33.         super(context, attrs);
  34.     }
  35.  
  36.     public PathView(Context context, AttributeSet attrs, int defStyleAttr)
  37.     {
  38.         super(context, attrs, defStyleAttr);
  39.     }
  40.  
  41.     public void init()
  42.     {
  43.         paint = new Paint();
  44.         paint.setColor(Color.DKGRAY);
  45.         paint.setStrokeWidth(10);
  46.         paint.setStyle(Paint.Style.STROKE);
  47.  
  48.         path = new Path();
  49.         path.moveTo(50, 50);
  50.         path.lineTo(50, 500);
  51.         path.lineTo(200, 500);
  52.         path.lineTo(200, 300);
  53.         path.lineTo(350, 300);
  54.         path.quadTo(360,100,400,310);
  55.  
  56.         // Measure the path
  57.         PathMeasure measure = new PathMeasure(path, false);
  58.         length = measure.getLength();
  59.  
  60.         float[] intervals = new float[]{length, length};
  61.  
  62.         ObjectAnimator animator = ObjectAnimator.ofFloat(PathView.this, "phase", 0.0f, 1.0f);
  63.         animator.setDuration(3000);
  64.         animator.start();
  65.     }
  66.  
  67.     //is called by animtor object
  68.     public void setPhase(float phase)
  69.     {
  70.         Log.d("pathview", "setPhase called with:" + String.valueOf(phase));
  71.         paint.setPathEffect(createPathEffect(length, phase, 0.0f));
  72.         invalidate();//will calll onDraw
  73.     }
  74.  
  75.     private static PathEffect createPathEffect(float pathLength, float phase, float offset)
  76.     {
  77.         return new DashPathEffect(new float[] { pathLength, pathLength },
  78.                 pathLength - phase * pathLength);
  79. //        return new DashPathEffect(new float[] { phase*pathLength, pathLength },
  80. //               0);
  81.     }
  82.  
  83.     @Override
  84.     public void onDraw(Canvas c)
  85.     {
  86.         super.onDraw(c);
  87.         c.drawPath(path, paint);
  88.     }
  89. }

在createPathEffect方法中,细心的人会注意到被注释掉了的return:

  1. return new DashPathEffect(new float[] { phase * pathLength, pathLength },
  2.        0);

其实用注释掉了的替换得到的效果是相同的。前不久我介绍的AndroidFillableLoaders库https://github.com/JorgeCastilloPrz/AndroidFillableLoaders就是用的这种,应该说这个其实比Romain Guy用改变起始偏移量来实现更好理解些。不过我还是喜欢Romain Guy的方式。

好了如果你运行上面的代码的话,得到的效果如下:

未命名.gif

最勉强的实现方法

刚刚我们讲到,以前我不知道PathEffect这种东西,于是我想了个方法。那就是在绘制线条之前,先用canvas裁减一个矩形区域,该矩形区域的高度可以覆盖完曲线(当然是需要计算得出的),而起始位置和曲线相同,然后不断的加长矩形区域的宽度。在裁减矩形范围内的曲线可以显示出来,而在这之外的部分则不能显示,因此就会得到一种曲线不断延长的效果。

大致的代码如下:

  1. canvas.clipRect(0, 0, (int)((float)maxCanvasWidth * percent), maxCanvasHeight);
  2. .....
  3. canvas.drawPath(path, paint);

对于Y值总是随着x轴增加的曲线,这种方式问题不大。但是对于比如c形的曲线,这种方式一眼就能看出问题来。自行脑补吧!

因为这个方式本身已经失去意义,我也就不多讲了。

0 0
原创粉丝点击