前几天在“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);
}
}

这样自绘,效率不太好,还没想怎么去改进,哪位有办法,可以留言给我。^_^


参考:

Android自定义View绘图实现拖影动画的更多相关文章

  1. Android自己定义View画图实现拖影动画

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

  2. Android自定义view绘图

    一.新建一个视图类,继承自View,重写OnDraw()函数,在函数内绘图 public class myView extends View {//新建一个视图类,继承自View myView(Con ...

  3. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  4. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  5. [原] Android 自定义View步骤

    例子如下:Android 自定义View 密码框 例子 1 良好的自定义View 易用,标准,开放. 一个设计良好的自定义view和其他设计良好的类很像.封装了某个具有易用性接口的功能组合,这些功能能 ...

  6. Android 自定义 View 圆形进度条总结

    Android 自定义圆形进度条总结 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 微信公众号:牙锅子 源码:CircleProgress 文中如有纰漏,欢迎大家留言指出. 最近 ...

  7. Android自定义View实战(SlideTab-可滑动的选择器)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/52178553 本文出自:[openXu的博客] 目录: 初步分析重写onDraw绘制 重写o ...

  8. Android 自定义View及其在布局文件中的使用示例(二)

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ...

  9. Android自定义View之ProgressBar出场记

    关于自定义View,我们前面已经有三篇文章在介绍了,如果筒子们还没阅读,建议先看一下,分别是android自定义View之钟表诞生记.android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检 ...

随机推荐

  1. struts2视频学习笔记 28(OGNL表达式)

    课时28 OGNL表达式 OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写,它是一个开源项目. Struts 2框架使用OGNL作为默认的表达式语 ...

  2. git在公司内部的使用实践(转)

    从2011.10月左右,开始在后台组推行git版本控制,到现在也差不多半年了,也形成了一套基于git flow的副官模式工作流程: 版本定义: 版本号使用x.x.x进行定义,第一个x代表大版本只有在项 ...

  3. Mysql 字符串处理函数

    函数: 1.从左开始截取字符串 left(str, length) 说明:left(被截取字段,截取长度) 例:select left(content,200) as abstract from my ...

  4. java面试---summay

    1:合适的建立索引,数据量比较大的时候,如果频繁的进行修改插入则不建议建立索引! 2:什么时候适合建索引,在什么字段上面建立索引? (被当做查询条件的) 3:什么叫做编译错误,什么叫做运行时异常 能被 ...

  5. Error in Android Studio - "Default Activity Not Found"

    Make sure you have specified the default activity in your AndroidManisfest.xml file. Within your def ...

  6. 15个让人惊讶的 CSS3 动画效果演示

    CSS 是网页设计非常重要的一部分,随着越来越多的浏览器对 CSS3 支持的不断完善,设计师和开发者们有了更多的选择.如今,用纯 CSS 就可以实现各种各样很酷的效果,甚至是动画. 本文收集了15个惊 ...

  7. JS引用类型之——RegExp

    前言 之前小猪分享过关于正则表达式的一些内容,具体请看传送门,今天小猪来分享怎么在js中使用正则表达式. 定义 ECMAScript通过RegExp类型来支持正则表达式.使用下面类似Perl的语法,就 ...

  8. 如何对Linux的grub进行加密

    一.加密 设置grub密码: 众所周知,通过编辑GRUB启动参数可以轻松的进入单用户模式从而修改root密码,这对于一台多用户的计算机或服务器来说,无疑增加了安全隐患.大家一定很像为自己的GRUB加一 ...

  9. Android之sdcard操作

    private static final String FILENAME = "abc.txt"; private static final String DIR = " ...

  10. HTML特殊符号对应代码

    HTML中的特殊符号所对应代码,收藏使用 符号 HTML 符号 HTML     & & < < > > ⁄ ⁄ " " ¸ ¸ ° ° ½ ...