基于Java
代码实现,并附有相应的Kotlin
版本
原创文章,转载请联系做者html
软草平莎过雨新,轻沙走马路无尘。
什么时候收拾耦耕身?java
先上效果图:
git
笔刷项目地址在此,你们要是喜欢的话,不妨来点个赞吧github
由于最终要实现的是windwos
下的画板喷漆笔刷,因此首先要对它作一个较为详细的效果解析。考虑到笔通常状况下笔刷的使用点,故此会分析一下点和线的效果细节。算法
从左至右依次是对同一坐标点击2次,点击8次,点击16次的效果展现;
当数量趋向更大时,点的密集程度并无很明显的偏向,基本能够肯定要在圆内均匀分布canvas
如图为匀速且缓慢滑过期,由点构成线bash
项目的大体框架由View
、BasePen
,两个大的模块构成。其中View
属于UI层面,BasePen
属于业务逻辑层面。接下来,将一一介绍这两个模块的具体功用和细节。app
此项目的承载View为PenView
,不承担业务逻辑,就是起到一个容器的做用。在PenView
中惟一的做用就是触发invalidate()
方法。框架
private BasePen mBasePen;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w != 0 && h != 0) {
if (mBasePen == null) {
mBasePen = new SprayPen(w, h);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
MotionEvent event1 = MotionEvent.obtain(event);
mBasePen.onTouchEvent(event1);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBasePen.onDraw(canvas);
}
复制代码
具体的业务逻辑,绘制、数据计算、触摸点移动Move等,全都由BasePen
以及它的子类来实现了。
低耦合性,表明着更多的自由度,对现有项目代码(若是应用到项目中)的冲击更小。在性能方面,若是View
知足不了要求,能够用更小的代价将其移植到性能更好的SurfaceView
里去。dom
业务方面,BasePen
做为基类,承担了一些基础的数据计算、绘制等功能,而具体的画笔效果则交由子类实现。
先看看BasePen
里作了什么:
private List<Point> mPoints;
public void onDraw(Canvas canvas) {
if (mPoints != null && !mPoints.isEmpty()) {
canvas.drawBitmap(mBitmap, 0, 0, null);
drawDetail(canvas);
}
}
复制代码
先将笔刷绘制到一张Bitmap
之上,再将这张Bitmap
交给PenView
来绘制出来。Point
是一个只记录了x和y坐标的类。drawDetail(Canvas canvas)
是一个抽象类,由子类实现具体的绘制。
BasePen
的onTouchEvent(MotionEvent event1)
方法里。以每次DOWN
事件为开始,记录MOVE
内的全部坐标信息。考虑到喷漆效果基本不用处理笔锋效果,暂不考虑记录UP
信息(后续若是实现其余笔刷效果会优化这里)。public void onTouchEvent(MotionEvent event1) {
switch (event1.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
clearPoints();
handlePoints(event1);
break;
case MotionEvent.ACTION_MOVE:
handlePoints(event1);
break;
case MotionEvent.ACTION_UP:
break;
}
}
private void handlePoints(MotionEvent event1) {
float x = event1.getX();
float y = event1.getY();
if (x > 0 && y > 0) {
mPoints.add(new Point(x, y));
}
}
private void clearPoints() {
if (mPoints == null) {
return;
}
mPoints.clear();
}
复制代码
protected void drawDetail(Canvas canvas) {
if (getPoints().isEmpty()) {
return;
}
mTotalNum = 由自定义粒子密度以及画笔宽度计算而来
drawSpray(当前最新坐标点.x, 当前最新坐标点.y, mTotalNum);
}
private void drawSpray(float x, float y, int totalNum) {
for (int i = 0; i < totalNum; i++) {
//算法计算出圆内随机点
float[] randomPoint = getRandomPoint(x, y, mPenW, true);
mCanvas.drawCircle(randomPoint[0], randomPoint[1], mCricleR, mPaint);
}
}
复制代码
以上是一部分伪代码,
SprayPen
内部定义了一个喷漆粒子密度,会根据画笔的宽度来实时改变粒子数量。每一个粒子的半径则由外部依赖的组件提供的width
计算而来。
在drawDetail(...)
方法内,每一次MOVE
和DOWN
事件都会在相应坐标处,绘制必定数目的圆内随机点。
当其串联起来时,就造成了喷漆效果。固然这只是初步完成,还有一些算法须要完善。伪代码表述不全,可参考SprayPen,在代码中有比较完善的注释。
接下来会说一些有关喷漆算法方面的问题。
在实现功能的过程当中,有两个问题是值得记录的。
一是圆内均匀随机点的分布问题;二是滑动速度快时,笔画的链接处理问题。
为了解决这个问题,主要尝试了三种方法:
java代码以下:
float x = mRandom.nextInt(r);
float y = (float) Math.sqrt(Math.pow(r, 2) - Math.pow(x, 2));
y = mRandom.nextInt((int) y);
x = 对值随机取正负(x);
y = 对值随机取正负(y);
复制代码
最终呈现效果以下:
当样本数量达到2000时,形状如上所示
能够很明显的看到,在x轴方向,左右两端的密集程度明显高于圆心
随机值在大量数据下会具备规律性,能够理解为当数据不少时,x的取值在(-r,r)大体为均匀分布的,y的取值亦是。当处于左右两端时,y的取值范围变小,视觉效果就显得紧凑了些。
固然若是用几率论数理统计公式来验证会更有说服力,但惋惜不会。。。(耸肩)
sin
和cos
来求解x和y。java代码以下:
float[] ints = new float[2];
int degree = mRandom.nextInt(360);
double curR = mRandom.nextInt(r)+1;
float x = (float) (curR * Math.cos(Math.toRadians(degree)));
float y = (float) (curR * Math.sin(Math.toRadians(degree)));
x = 对值随机取正负(x);
y = 对值随机取正负(y);
复制代码
最终呈现效果以下:
明显看到中心处的密集程度高于边缘地带,事实上当角度固定时,r在[0,R)范围内随机取值。当数量更大时,坐标点是均匀分布的。
当r越小时,所占用的面积越小,就会显得粒子很密集。
sin
和cos
来求解x和y。java代码以下:
int degree = mRandom.nextInt(360);
double curR = Math.sqrt(mRandom.nextDouble()) * r;
float x = (float) (curR * Math.cos(Math.toRadians(degree)));
float y = (float) (curR * Math.sin(Math.toRadians(degree)));
x = 对值随机取正负(x);
y = 对值随机取正负(y);
复制代码
最终呈现效果以下:
此次的视觉效果总算是达到了均匀的效果,这个算法是利用了一个根函数的特性,以下图:
红色是根函数,蓝色是线性函数。二者相比下来,根函数的取值会更大些,相应的,接近边缘的点就会更多一点,让粒子的分布效果更加均衡。 ![]()
当以比较慢的速度滑动时,笔画尚显流畅无明显断层。当速度过快时,MOVE
留下的点更少,且间距大。会出现画笔断层现象,这时候就须要一些特殊的处理方法。
代码中设定了一个标准值D
,这个值是由BasePen
所持有的w和h两个值计算而来的,通常来讲,这两个值指望为依附的View
的宽高。最初也考虑使用画笔的直径计算,但考虑到画笔直径是能够外部动态改变的。标准值最好保持必定的独立性,其所依赖的数据越稳定越好,要否则会影响平衡
。而后当MOVE
时,当前点距离上一个点的相对距离大于这个标准值D
时,就会断定此时处于快移速状态,间距越大移速越快,那么喷漆效果相应地就要减弱【直观而言就是粒子浓度要低】。
快移速状态时,代码会在当前点和上一个点之间,模拟出一些笔迹点。相应地,这些笔迹点的粒子密集度会低一些,其计算函数且是一个反驼峰的变化状态。即连续笔迹点的中间点粒子最稀疏,两边则最密集。
//手速过快时
float stepDis = mPenR * 1.6f;
//笔迹点的数量
int v = (int) (getLastDis() / stepDis);
float gapX = getPoints().get(getPoints().size() - 1).x - getPoints().get(getPoints().size() - 2).x;
float gapY = getPoints().get(getPoints().size() - 1).y - getPoints().get(getPoints().size() - 2).y;
//描绘笔迹点
for (int i = 1; i <= v; i++) {
float x = (float) (getPoints().get(getPoints().size() - 2).x + (gapX * i * stepDis / getLastDis()));
float y = (float) (getPoints().get(getPoints().size() - 2).y + (gapY * i * stepDis / getLastDis()));
drawSpray(x, y, (int) (mTotalNum * calculate(i, 1, v)), mRandom.nextBoolean());
}
/**
* 使用(x-(min+max)/2)^2/(min-(min+max)/2)^2做为粒子密度比函数
*/
private static float calculate(int index, int min, int max) {
float maxProbability = 0.6f;
float minProbability = 0.15f;
if (max - min + 1 <= 4) {
return maxProbability;
}
int mid = (max + min) / 2;
int maxValue = (int) Math.pow(mid - min, 2);
float ratio = (float) (Math.pow(index - mid, 2) / maxValue);
if (ratio >= maxProbability) {
return maxProbability;
} else if (ratio <= minProbability) {
return minProbability;
} else {
return ratio;
}
}
复制代码
本项目在写的时候,顺便也写了一个Kotlin
版本的。注意,并非用AS自带的代码转换的。因此Kotlin
版本会有不少没必要要的测试体验代码,不要在乎这些细节。
Kotlin版本这里这里,喜欢的不妨点个赞吧
以上就是本次Demo
的思路、以及一些算法的解析。数学之美,使人沉醉*(数学学渣留下了悔恨的泪水。。。)*
数学才是本体啊
笔刷项目地址在此,代码中的注释会更加清晰些,你们要是喜欢的话,不妨来点个赞吧
有欢迎关注个人公众号,技术与生活