Android自定义View绘图实现拖影动画

前几天在“Android绘图之渐隐动画”一文中通过画线实现了渐隐动画,但里面有个问题,画笔较粗(大于1)时线段之间会有裂隙,我又改进了一下。这次效果好多了。

先看效果吧:

然后我们来说说基本的做法:
 •根据画笔宽度,计算每一条线段两个顶点对应的四个点,四点连线,包围线段,形成一个路径。
 •后一条线段的路径的前两个点,取(等于)前一条线段的后两点,这样就衔接起来了。

把Path的Style修改为FILL,效果是这样的:

可以看到一个个四边形,连成了路径。

好啦,现在说说怎样根据两点计算出包围它们连线的路径所需的四个点。

先看一张图:

在这张图里,黑色细线是我们拿到的两个触摸点相连得到的。当画笔宽度大于1(比如为10)时,其实经过这条黑线的两个端点并且与这条黑线垂直相交的两条线(蓝线),就可以计算出来,蓝线的长度就是画笔的宽度,结合这些就可以计算出红色的四个点。而红色的四个点就围住了线段,形成路径。

这里面用到两点连线的公式,采用点斜式:

y = k*x + b

黑线的斜率是:

k = (y2 - y1) / (x2 - x1)

垂直相交的两条线的斜率的关系是:

k1 * k2 = -1

所以,蓝线的斜率就可以计算出来了。有了斜率和线上的一个点,就可以求出这条线的点斜式中的b,点斜式就出来了。

然后,利用两点间距离公式:

已知一个点,这个点与另一个点的距离(画笔宽度除以2),斜率,代入两点间距离公式和蓝线的点斜式,就可以计算出两个红色的点了。

计算时用到的是一元二次方程a*x*x + bx + c = 0,求 x 时用的公式是:

好啦,最后,上代码:

package com.example.disappearinglines;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

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

/**
 * Created by foruok,欢迎关注我的订阅号“程序视界”.
 */

public class DisappearingDoodleView extends View {
 public static float convertDipToPx(Context context, float fDip) {
  float fPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fDip,
    context.getResources().getDisplayMetrics());
  return fPx;
 }

 final static String TAG = "DoodleView";
 class LineElement {
  static final public int ALPHA_STEP = 8;
  public LineElement(float pathWidth){
   mPaint = new Paint();
   mPaint.setARGB(255, 255, 0, 0);
   mPaint.setAntiAlias(true);
   mPaint.setStrokeWidth(0);
   mPaint.setStrokeCap(Paint.Cap.BUTT);
   mPaint.setStyle(Paint.Style.FILL);
   mPath = new Path();
   mPathWidth = pathWidth;
   for(int i= 0; i < mPoints.length; i++){
    mPoints[i] = new PointF();
   }
  }

  public void setPaint(Paint paint){
   mPaint = paint;
  }

  public void setAlpha(int alpha){
   mPaint.setAlpha(alpha);
   mPathWidth = (alpha * mPathWidth) / 255;
  }

  private boolean caculatePoints(float k, float b, float x1, float y1, float distance, PointF pt1, PointF pt2){
   //point-k formula
   // y= kx + b
   //distance formula of two points
   // distance*distance = Math.pow((x - x1), 2) + Math.pow((y - y1), 2)
   // |
   // V
   // ax*x + bx + c = 0;
   // |
   // V
   // x = (-b +/- Math.sqrt( b*b - 4*a*c ) ) / (2*a)
   double a1 = Math.pow(k, 2) + 1;
   double b1 = 2* k * (b - y1) - 2 * x1;
   double c1 = Math.pow(x1, 2) + Math.pow(b - y1, 2) - Math.pow(distance, 2);
   double criterion = Math.pow(b1, 2) - 4*a1*c1;
   if(criterion > 0) {
    criterion = Math.sqrt(criterion);
    pt1.x = (float) ((-b1 + criterion) / (2 * a1));
    pt1.y = k * pt1.x + b;
    pt2.x = (float) ((-b1 - criterion) / (2 * a1));
    pt2.y = k * pt2.x + b;
    return true;
   }
   return false;
  }

  private void swapPoint(PointF pt1, PointF pt2){
   float t = pt1.x;
   pt1.x = pt2.x;
   pt2.x = t;
   t = pt1.y;
   pt1.y = pt2.y;
   pt2.y = t;
  }

  public boolean updatePathPoints(){
   float distance = mPathWidth / 2;
   if(Math.abs(mEndX - mStartX) < 1){
    mPoints[0].x = mStartX + distance;
    mPoints[0].y = mStartY - distance;
    mPoints[1].x = mStartX - distance;
    mPoints[1].y = mPoints[0].y;
    mPoints[2].x = mPoints[1].x;
    mPoints[2].y = mEndY + distance;
    mPoints[3].x = mPoints[0].x;
    mPoints[3].y = mPoints[2].y;
   }else if(Math.abs(mEndY - mStartY) < 1){
    mPoints[0].x = mStartX - distance;
    mPoints[0].y = mStartY - distance;
    mPoints[1].x = mPoints[0].x;
    mPoints[1].y = mStartY + distance;
    mPoints[2].x = mEndX + distance;
    mPoints[2].y = mPoints[1].y;
    mPoints[3].x = mPoints[2].x;
    mPoints[3].y = mPoints[0].y;
   }else{
    //point-k formula
    //y= kx + b
    float kLine = (mEndY - mStartY) / (mEndX - mStartX);
    float kVertLine = -1 / kLine;
    float b = mStartY - (kVertLine * mStartX);
    if(!caculatePoints(kVertLine, b, mStartX, mStartY, distance, mPoints[0], mPoints[1])){
     String info = String.format(TAG, "startPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
       mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
     Log.i(TAG, info);
     return false;
    }
    b = mEndY - (kVertLine * mEndX);
    if(!caculatePoints(kVertLine, b, mEndX, mEndY, distance, mPoints[2], mPoints[3])){
     String info = String.format(TAG, "endPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
       mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
     Log.i(TAG, info);
     return false;
    }
    //reorder points to unti-clockwise
    if(mStartX < mEndX){
     if(mStartY < mEndY){
      if(mPoints[0].x < mPoints[1].x){
       swapPoint(mPoints[0], mPoints[1]);
      }
      if(mPoints[2].x > mPoints[3].x){
       swapPoint(mPoints[2], mPoints[3]);
      }
     }else{
      if(mPoints[0].x > mPoints[1].x){
       swapPoint(mPoints[0], mPoints[1]);
      }
      if(mPoints[2].x < mPoints[3].x){
       swapPoint(mPoints[2], mPoints[3]);
      }
     }
    }else{
     if(mStartY < mEndY){
      if(mPoints[0].x < mPoints[1].x){
       swapPoint(mPoints[0], mPoints[1]);
      }
      if(mPoints[2].x > mPoints[3].x){
       swapPoint(mPoints[2], mPoints[3]);
      }
     }else{
      if(mPoints[0].x > mPoints[1].x){
       swapPoint(mPoints[0], mPoints[1]);
      }
      if(mPoints[2].x < mPoints[3].x){
       swapPoint(mPoints[2], mPoints[3]);
      }
     }
    }
   }

   return true;
  }

  // for the first line
  public void updatePath(){
   //update path
   mPath.reset();
   mPath.moveTo(mPoints[0].x, mPoints[0].y);
   mPath.lineTo(mPoints[1].x, mPoints[1].y);
   mPath.lineTo(mPoints[2].x, mPoints[2].y);
   mPath.lineTo(mPoints[3].x, mPoints[3].y);
   mPath.close();
  }

  // for middle line
  public void updatePathWithStartPoints(PointF pt1, PointF pt2){
   mPath.reset();
   mPath.moveTo(pt1.x, pt1.y);
   mPath.lineTo(pt2.x, pt2.y);
   mPath.lineTo(mPoints[2].x, mPoints[2].y);
   mPath.lineTo(mPoints[3].x, mPoints[3].y);
   mPath.close();
  }

  public float mStartX = -1;
  public float mStartY = -1;
  public float mEndX = -1;
  public float mEndY = -1;
  public Paint mPaint;
  public Path mPath;
  public PointF[] mPoints = new PointF[4]; //path's vertex
  float mPathWidth;
 }

 private LineElement mCurrentLine = null;
 private List<LineElement> mLines = null;
 private float mLaserX = 0;
 private float mLaserY = 0;
 final Paint mPaint = new Paint();
 private int mWidth = 0;
 private int mHeight = 0;
 private long mElapsed = 0;
 private float mStrokeWidth = 20;
 private float mCircleRadius = 10;
 private Handler mHandler = new Handler(){
  @Override
  public void handleMessage(Message msg){
   DisappearingDoodleView.this.invalidate();
  }
 };

 public DisappearingDoodleView(Context context){
  super(context);
  initialize(context);
 }

 public DisappearingDoodleView(Context context, AttributeSet attrs){
  super(context, attrs);
  initialize(context);
 }

 private void initialize(Context context){
  mStrokeWidth = convertDipToPx(context, 22);
  mCircleRadius = convertDipToPx(context, 10);
  mPaint.setARGB(255, 255, 0, 0);
  mPaint.setAntiAlias(true);
  mPaint.setStrokeWidth(0);
  mPaint.setStyle(Paint.Style.FILL);
 }

 @Override
 protected void onSizeChanged (int w, int h, int oldw, int oldh){
  mWidth = w;
  mHeight = h;
  adjustLasterPosition();
 }

 private void adjustLasterPosition(){
  if(mLaserX - mCircleRadius < 0) mLaserX = mCircleRadius;
  else if(mLaserX + mCircleRadius > mWidth) mLaserX = mWidth - mCircleRadius;
  if(mLaserY - mCircleRadius < 0) mLaserY = mCircleRadius;
  else if(mLaserY + mCircleRadius > mHeight) mLaserY = mHeight - mCircleRadius;
 }

 private void updateLaserPosition(float x, float y){
  mLaserX = x;
  mLaserY = y;
  adjustLasterPosition();
 }
 @Override
 protected void onDraw(Canvas canvas){
  //canvas.drawText("ABCDE", 10, 16, mPaint);
  mElapsed = SystemClock.elapsedRealtime();

  if(mLines != null) {
   updatePaths();
   for (LineElement e : mLines) {
    if(e.mStartX < 0 || e.mEndY < 0 || e.mPath.isEmpty()) continue;
    //canvas.drawLine(e.mStartX, e.mStartY, e.mEndX, e.mEndY, e.mPaint);
    canvas.drawPath(e.mPath, e.mPaint);
   }
   compactPaths();
  }
  canvas.drawCircle(mLaserX, mLaserY, mCircleRadius, mPaint);
 }

 private boolean isValidLine(float x1, float y1, float x2, float y2){
  return Math.abs(x1 - x2) > 1 || Math.abs(y1 - y2) > 1;
 }

 @Override
 public boolean onTouchEvent(MotionEvent event){
  float x = event.getX();
  float y = event.getY();

  int action = event.getAction();
  if(action == MotionEvent.ACTION_UP){// end one line after finger release
   if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
    mCurrentLine.mEndX = x;
    mCurrentLine.mEndY = y;
    addToPaths(mCurrentLine);
   }
   //mCurrentLine.updatePathPoints();
   mCurrentLine = null;
   updateLaserPosition(x, y);
   invalidate();
   return true;
  }

  if(action == MotionEvent.ACTION_DOWN){
   mLines = null;
   mCurrentLine = new LineElement(mStrokeWidth);

   mCurrentLine.mStartX = x;
   mCurrentLine.mStartY = y;
   updateLaserPosition(x, y);
   return true;
  }

  if(action == MotionEvent.ACTION_MOVE) {
   if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
    mCurrentLine.mEndX = x;
    mCurrentLine.mEndY = y;
    addToPaths(mCurrentLine);

    mCurrentLine = new LineElement(mStrokeWidth);
    mCurrentLine.mStartX = x;
    mCurrentLine.mStartY = y;

    updateLaserPosition(x, y);
   }else{
    //do nothing, wait next point
   }
  }

  if(mHandler.hasMessages(1)){
   mHandler.removeMessages(1);
  }
  Message msg = new Message();
  msg.what = 1;
  mHandler.sendMessageDelayed(msg, 0);

  return true;
 }

 private void addToPaths(LineElement element){
  if(mLines == null) {
   mLines = new ArrayList<LineElement>() ;
  }
  mLines.add(element);
 }

 private void updatePaths() {
  int size = mLines.size();
  if (size == 0) return;

  LineElement line = null;
  int j = 0;
  for (; j < size; j++) {
   line = mLines.get(j);
   if (line.updatePathPoints()) break;
  }

  if (j == size) {
   mLines.clear();
   return;
  } else {
   for (j--; j >= 0; j--) {
    mLines.remove(0);
   }
  }

  line.updatePath();
  size = mLines.size();

  LineElement lastLine = null;
  for (int i = 1; i < size; i++) {
   line = mLines.get(i);
   if (line.updatePathPoints()){
    if (lastLine == null) {
     lastLine = mLines.get(i - 1);
    }
    line.updatePathWithStartPoints(lastLine.mPoints[3], lastLine.mPoints[2]);
    lastLine = null;
   }else{
    mLines.remove(i);
    size = mLines.size();
   }
  }
 }

 public void compactPaths(){

  int size = mLines.size();
  int index = size - 1;
  if(size == 0) return;
  int baseAlpha = 255 - LineElement.ALPHA_STEP;
  int itselfAlpha;
  LineElement line;
  for(; index >=0 ; index--, baseAlpha -= LineElement.ALPHA_STEP){
   line = mLines.get(index);
   itselfAlpha = line.mPaint.getAlpha();
   if(itselfAlpha == 255){
    if(baseAlpha <= 0 || line.mPathWidth < 1){
     ++index;
     break;
    }
    line.setAlpha(baseAlpha);
   }else{
    itselfAlpha -= LineElement.ALPHA_STEP;
    if(itselfAlpha <= 0 || line.mPathWidth < 1){
     ++index;
     break;
    }
    line.setAlpha(itselfAlpha);
   }
  }

  if(index >= size){
   // all sub-path should disappear
   mLines = null;
  }
  else if(index >= 0){
   //Log.i(TAG, "compactPaths from " + index + " to " + (size - 1));
   mLines = mLines.subList(index, size);
  }else{
   // no sub-path should disappear
  }

  long interval = 40 - SystemClock.elapsedRealtime() + mElapsed;
  if(interval < 0) interval = 0;
  Message msg = new Message();
  msg.what = 1;
  mHandler.sendMessageDelayed(msg, interval);
 }
}

这样自绘,效率不太好,还没想怎么去改进,大家可以讨论讨论。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android UI效果之绘图篇(二)

    一.Canvas Canvas中的方法很多,这里我们只挑常用的进行讲解说明 Canvas可以绘制的对象有: 弧线(arcs) canvas. 填充颜色(argb和color) Bitmap 圆(circle和oval) 点(point) 线(line) 矩形(Rect) 图片(Picture) 圆角矩形 (RoundRect) 文本(text) 顶点(Vertices) 路径(path) 绘制弧形 /** * 绘制弧形 * @param oval 绘制区域 * @param startAngle

  • Android 通过onDraw实现在View中绘图操作的示例

    Android绘图操作,通过继承View实现,在onDraw函数中实现绘图.下面是一个简单的例子: 复制代码 代码如下: public class AndroidTest extends Activity {    /** Called when the activity is first created. */    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(save

  • Android绘图常用方法汇总

    Android绘图常用方法有哪些,下面一一为大家列举: 1.有关画笔(Paint)的方法 Paint mPaint= new Paint(); mPaint.setAntiAlias(true); // 消除锯齿 mPaint.setStrokeWidth(mCircleWidth); // 设置圆环的宽度 mPaint.setStrokeCap(Paint.Cap.ROUND); // 定义线段断点形状为圆头 mPaint.setAntiAlias(true); // 消除锯齿 mPaint.

  • Android UI效果之绘图篇(三)

    一. PorterDuffXfermode PorterDuffXfermode类似于数据集合里面的交集并集概念,只是数据里面取的是两个集合,而我们这里取的是两个图形之间的交集并集,我们先来看一张Android API Demo里面一张金典的图 图中的Src和Dst相当于是数学中的两个集合,而在我们的代码中我们可以这样来操作这两个集合 canvas.drawSrc(); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX));

  • Android UI效果之绘图篇(四)

    上一篇博文说到了Shader的五个子类 - BitmapShader - LinearGradient - RadialGradient - SweepGradient - ComposeShader 其中BitmapShader和LinearGradient已经做了说明,今天就把剩余的三个Shader补充一下 3. RadialGradient 先看下构造方法 /** @param centerX 中心X坐标 @param centerY 中心Y坐标 @param radius 半径 @par

  • android图像绘制(七)ClipRect局部绘图/切割原图绘制总结

    杂语:看了很多程序猿都有写博客的习惯,看来我也得练练,不管写的好不好了,学到点什么体会就写写吧. 内容解说:这几天开始学游戏地图制作,今天小小的总结一下Canvas的clipRect()接口的使用. 1)选取要在画布上绘制(刷新)的区域,如图以(x, y)为起点坐标.宽w.高h的区域 2)选择要绘制的图片,不一定是刚好宽高为(w,h),大图就需要切割了(本例子绘制绿色区域) 3)将图片绘制到画布上,使得绿色区域与白色方块重合 4)最后效果图 代码解说: 复制代码 代码如下: canvas.sav

  • Android学习教程之2D绘图基础及绘制太极图

    前言 Android是通过graphics类来显示2D图形的.其中graphics中包括了Canvas.Paint.Color.Bitmap等类.graphics具有绘制点.线.颜色.2D几何图形.图像处理等功能.其中Color和Bitmap是很常用的类,本文主要要讲的是Canvas和Paint.顾名思义就是画布和画笔. Canvas类 Canvas即画布,我们需要做的就是使用之前设置好的Paint来绘制图形.系统通过 Canvas 为我们提供了一些基础的绘图 API : 1.canvas.dr

  • Android UI效果之绘图篇(一)

    最近准备整理一套关于UI效果的文章,算是对这段时间的一个总结,主要讲Android开发中的UI效果设计模块.初步分为一下几个篇幅: Android XML绘图(Shape.Layer.Selector) Android Canvas绘图(canvas.point.porterDuffXfermode.shader) Android 动画详解 Android 自定义控件 今天就当开胃菜,先讲讲最简单的xml绘图,相信这个大家都用的比较熟,这里就当给大家做一个小文档,当那个参数配置忘了,便于查阅 一

  • Android编程之OpenGL绘图技巧总结

    本文实例讲述了Android编程之OpenGL绘图技巧.分享给大家供大家参考,具体如下: 很久不用OpenGL ES绘图,怕自己忘记了,于是重新复习一遍,顺便原理性的东西总结如下: 1. Android 3D坐标系统 如图: Android的三维坐标系统中: 坐标原点位于中央, X轴从左向右延伸,原点左边的值为负数,右边为正数: Y轴从下向上延伸,原点下边的值为负数,上边为正数: Z轴屏幕里面向外面延伸,屏幕里面为负数,外面为正数. 2. 开发工具(OpenGL和OpenGL ES)介绍 Ope

  • Android自定义View绘图实现渐隐动画

    实现了一个有趣的小东西:使用自定义View绘图,一边画线,画出的线条渐渐变淡,直到消失.效果如下图所示: 用属性动画或者渐变填充(Shader)可以做到一笔一笔的变化,但要想一笔渐变(手指不抬起边画边渐隐),没在Android中找到现成的API可用.所以,自己做了一个. 基本的想法是这样的: •在View的onTouchEvent中记录触摸点,生成一条一条的线LineElement,放在一个List中.给每个LineElement配置一个Paint实例. •在onDraw中绘制线段. •变换Li

随机推荐