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

作者:foruok 时间:2023-04-16 00:06:38 

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

先看效果吧:

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

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

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

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

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

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

先看一张图:

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

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

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

y = k*x + b

黑线的斜率是:

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

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

k1 * k2 = -1 

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

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

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

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

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

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

好啦,最后,上代码:


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);
}
}

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

标签:Android,View,拖影
0
投稿

猜你喜欢

  • Android中FoldingLayout折叠布局的用法及实战全攻略

    2021-05-23 19:29:17
  • Java Swing中JDialog实现用户登陆UI示例

    2021-10-12 13:58:00
  • myEclipse配置jdk1.7教程

    2022-07-21 11:25:35
  • JAVA IDEA入门使用手册(新手小白必备)

    2022-10-21 16:31:25
  • spring mvc利用ajax向controller传递对象的方法示例

    2022-10-22 15:06:13
  • Android自定义视图中图片的处理

    2023-06-29 12:07:27
  • Android实现无标题栏全屏的方法

    2023-06-25 11:14:27
  • Android Activity通用悬浮可拖拽View封装的思路详解

    2023-08-08 15:31:48
  • WPF仿LiveCharts实现饼图的绘制

    2022-02-08 02:17:05
  • c#中多线程访问winform控件的若干问题小结

    2023-09-23 17:04:27
  • Unity实现卡片循环滚动效果的示例详解

    2022-06-06 16:04:47
  • Java新手环境搭建 Tomcat安装配置教程

    2021-08-19 08:06:23
  • C#中将ListView中数据导出到Excel的实例方法

    2023-12-07 04:00:08
  • C#Js时间格式化问题简单实例

    2023-05-17 01:49:19
  • Java的MD5工具类和客户端测试类

    2022-04-23 03:08:48
  • Java 自定义Spring框架以及Spring框架的基本使用

    2021-05-29 19:35:57
  • Spring与Mybatis基于注解整合Redis的方法

    2022-09-19 09:19:56
  • 简单谈谈java中匿名内部类构造函数

    2021-09-08 18:54:10
  • C#实现学生成绩管理系统

    2021-06-05 22:51:31
  • Android 限制edittext 整数和小数位数 过滤器(详解)

    2023-11-24 17:33:52
  • asp之家 软件编程 m.aspxhome.com