Android高级画笔效果实现的探索
来源:互联网 发布:淘宝退货纸条怎么写 编辑:程序博客网 时间:2024/05/17 03:03
中国铁路总公司近日发布消息,自11月11日起至20日,铁路部门将在京沪高铁“复兴号”列车上推出高铁“极速达”快运新产品,这将是国内最快的跨城快运产品。据铁路部门有关负责人介绍,在继续开展“当日达”、“次晨达”、“次日达”等铁路快捷运输服务的基础上,中铁快运联合国内快递服务商,发挥“复兴号”动车组的优势,首次在京沪高铁推出高铁“极速达”快运新产品,实现10小时货物送达客户,这也是目前中国最快的跨城快运产品。
本篇来自 豌豆射手_BiuBiu 的投稿,主要讲解了针对上周Android画笔效果的实现进行了扩展和补充,希望对大家有所帮助!
豌豆射手_BiuBiu 的博客地址:
http://www.jianshu.com/u/a58eb984bda4
下图效果的实现大家可以去参考上周分享的这篇文章:
水彩笔效果一
//不设置
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//使用原图的资源文件为
mOriginBitmap = BitmapFactory.decodeResource( mContext.getResources(), R.mipmap.brush);
水彩笔效果二,使用原图的资源文件为R.mipmap.cicrle
水彩笔效果三,使用原图的资源文件为R.mipmap.tranglie
设置paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));的效果
如何实现的细节在上篇文章有很仔细的说明。
代码上我抽取了一个基类,主要是方便后面的扩展
/** * @author shiming * @version v1.0 create at 2017/10/17 * @des 处理draw和touch事件的基类 */
public abstract class BasePen { /** * 绘制 * * @param canvas */ public abstract void draw(Canvas canvas); /** * 接受并处理onTouchEvent * * @param event * @return */ public boolean onTouchEvent(MotionEvent event,Canvas canvas){ return false; } }
关于BasePenExtend集成BasePen
draw方法的抽取,和上篇文章一样,当接触到的点很少的时候,不用去绘制,由于在实现水彩笔的时候,使用一只笔的时候会导致整个画布的笔的透明度发生改变,所以提供了一个抽象的方法让子类去实现,SteelPen和BrushPen中唯一在draw的差别就是多了一只paint。
@Override public void draw(Canvas canvas) { mPaint.setStyle(Paint.Style.FILL); //点的集合少 不去绘制 if (mHWPointList == null || mHWPointList.size() < 1) return; //当控制点的集合很少的时候,需要画个小圆,但是需要算法 if (mHWPointList.size() < 2) { ControllerPoint point = mHWPointList.get(0); //由于此问题在算法上还没有实现,所以暂时不给他画圆圈 //canvas.drawCircle(point.x, point.y, point.width, mPaint); } else { mCurPoint = mHWPointList.get(0); drawNeetToDo(canvas); } } /** * 这里由于在设置笔的透明度,会导致整个线,或者说整个画布的的颜透明度随着整个笔的透明度而变化, * 所以在这里考虑是不是说,绘制毛笔的时候,每次都给它new 一个paint ,但是这里我还没有找到更好的办法 * * @param canvas */
// TODO: 2017/10/17 这个问题 待解决
protected abstract void drawNeetToDo(Canvas canvas);
onTouchEvent 事件在 NewDrawPenView 调用,这里实现就可以了,注意这个问题就行, event 会被下一次事件重用,这里必须生成新的,否则会有问题
@Override
public boolean onTouchEvent(MotionEvent event,Canvas canvas) { // event会被下一次事件重用,这里必须生成新的,否则会有问题 MotionEvent event2 = MotionEvent.obtain(event); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: onDown(createMotionElement(event2)); return true; case MotionEvent.ACTION_MOVE: onMove(createMotionElement(event2)); return true; case MotionEvent.ACTION_UP: onUp(createMotionElement(event2),canvas); return true; default: break; } return super.onTouchEvent(event,canvas); }
onDown 事件的实现,由于在自定义 View 中,MotionEvent.ACTION_DOWN:事件的触发要比 onDraw 早,当实现水彩笔的时候,我在想每次能不能使用一只新的笔,那么改变笔的透明度的时候,画布其他上上的笔画的就不会发生改变,所以暴露了一个方法,让子类去实现,不从写的话,这个笔就会为null!
/** * 按下的事件 * @param mElement */
public void onDown(MotionElement mElement){ if (mPaint==null){ throw new NullPointerException("paint 笔不可能为null哦"); } if (getNewPaint(mPaint)!=null){ Paint paint=getNewPaint(mPaint); mPaint=paint; //当然了,不要因为担心内存泄漏,在每个变量使用完成后都添加xxx=null, // 对于消除过期引用的最好方法,就是让包含该引用的变量结束生命周期,而不是显示的清空 paint=null; System.out.println("shiming 当绘制的时候是否为新的paint"+mPaint+"原来的对象是否销毁了paint=="+paint); } mPointList.clear(); //如果在brush字体这里接受到down的事件,把下面的这个集合清空的话,那么绘制的内容会发生改变 //不清空的话,也不可能 mHWPointList.clear(); //记录down的控制点的信息 ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); //如果用笔画的画我的屏幕,记录他宽度的和压力值的乘,但是哇, if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { mLastWidth = mElement.pressure * mBaseWidth; } else { //如果是手指画的,我们取他的0.8 mLastWidth = 0.8 * mBaseWidth; } //down下的点的宽度 curPoint.width = (float) mLastWidth; mLastVel = 0; mPointList.add(curPoint); //记录当前的点 mLastPoint = curPoint; } protected Paint getNewPaint(Paint paint){ return null; }
onMove 事件其实实现原理和上篇文章一样,但是呢,当绘制水彩笔的时候,有个透明度的关系,所以需要交给子类去实现.
/** * 手指移动的事件 * @param mElement */ public void onMove(MotionElement mElement){ ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y); double deltaX = curPoint.x - mLastPoint.x; double deltaY = curPoint.y - mLastPoint.y; //deltaX和deltay平方和的二次方根 想象一个例子 1+1的平方根为1.4 (x²+y²)开根号 double curDis = Math.hypot(deltaX, deltaY); //我们求出的这个值越小,画的点或者是绘制椭圆形越多,这个值越大的话,绘制的越少,笔就越细,宽度越小 double curVel = curDis * IPenConfig.DIS_VEL_CAL_FACTOR; double curWidth; //点的集合少,我们得必须改变宽度,每次点击的down的时候,这个事件 if (mPointList.size() < 2) { if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { curWidth = mElement.pressure * mBaseWidth; } else { curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, mLastWidth); } curPoint.width = (float) curWidth; mBezier.init(mLastPoint, curPoint); } else { mLastVel = curVel; if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) { curWidth = mElement.pressure * mBaseWidth; } else { //由于我们手机是触屏的手机,滑动的速度也不慢,所以,一般会走到这里来 //阐明一点,当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着mlastWidth不断的变换 curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5, mLastWidth); } curPoint.width = (float) curWidth; mBezier.addNode(curPoint); } //每次移动的话,这里赋值新的值 mLastWidth = curWidth; mPointList.add(curPoint); moveNeetToDo(curDis); mLastPoint = curPoint; } /** * 移动的时候,这里由于需要透明度的处理,交给子类 * @param */
protected abstract void moveNeetToDo(double f);
在上篇文章中我们有详细介绍 calcNewWidth() 方法的作用:当滑动的速度很快的时候,这个值就越小,越慢就越大,依靠着 mlastWidth 不断的变换,如何变化的呢?在代码中做了很详细的解释
public double calcNewWidth(double curVel, double lastVel, double curDis, double factor, double lastWidth) { double calVel = curVel * 0.6 + lastVel * (1 - 0.6); //返回指定数字的自然对数 //手指滑动的越快,这个值越小,为负数 double vfac = Math.log(factor * 2.0f) * (-calVel); //此方法返回值e,其中e是自然对数的基数。 //Math.exp(vfac) 变化范围为0 到1 当手指没有滑动的时候 这个值为1 当滑动很快的时候无线趋近于0 //在次说明下,当手指抬起来,这个值会变大,这也就说明,抬起手太慢的话,笔锋效果不太明显 //这就说明为什么笔锋的效果不太明显 double calWidth = mBaseWidth * Math.exp(vfac); //滑动的速度越快的话,mMoveThres也越大 double mMoveThres = curDis * 0.01f; //对之值最大的地方进行控制 if (mMoveThres > IPenConfig.WIDTH_THRES_MAX) { mMoveThres = IPenConfig.WIDTH_THRES_MAX; } //滑动的越快的话,第一个判断会走 if (Math.abs(calWidth - mBaseWidth) / mBaseWidth > mMoveThres) { if (calWidth > mBaseWidth) { calWidth = mBaseWidth * (1 + mMoveThres); } else { calWidth = mBaseWidth * (1 - mMoveThres); } //滑动的越慢的话,第二个判断会走 } else if (Math.abs(calWidth - lastWidth) / lastWidth > mMoveThres) { if (calWidth > lastWidth) { calWidth = lastWidth * (1 + mMoveThres); } else { calWidth = lastWidth * (1 - mMoveThres); } } return calWidth; }
最后一步绘制,只需要要判断现在的点和触摸点的位置一样就不用去绘制,其余的交个子类去实现即可,在这里我有一个问题没有解决,在 onDown 事件下,就是一直按着屏幕某个点的时候,这个方法会一直走,意思就是说,用户的行为没有发生绘制笔,但是呢,代码一直在绘制,不断重复的绘制,可能在性能上不太友好!还需优化
/** * 当现在的点和触摸点的位置在一起的时候不用去绘制 * @param canvas * @param point * @param paint */
protected void drawToPoint(Canvas canvas, ControllerPoint point, Paint paint) { if ((mCurPoint.x == point.x) && (mCurPoint.y == point.y)) { return; } //毛笔的效果和钢笔的不太一样,交给自己去实现 doNeetToDo(canvas,point,paint); }
初始化是从资源文件中获取bitmap,在我个人的测试中,当资源文件如果形状很多种的时候,会构建很多中的笔画的效果,非常有意思,如上面的图例
public BrushPen(Context context) { super(context); initTexture(); } /** * 感谢公司的ui大哥 小伍哥 免费给的切图 * R.mipmap.tranglie 设置的时候有点像三角形的笔锋 * R.mipmap.cicrle 圆形的笔锋效果 * R.mipmap.six 六边形有点怪怪的,可以测试一下 * R.drawable.brush 这个才是用起来比较舒服,如果你的笔锋要很尖的话,叫ui爸爸给你裁剪这种图 越尖越好 */
private void initTexture() { //通过资源文件生成的原始的bitmap区域 后面的资源图有些更加有意识的东西 mOriginBitmap = BitmapFactory.decodeResource( mContext.getResources(), R.mipmap.brush); }
setBitmap(bitmap)方法,主要是得到需要绘制的rect的区域,这个没什么,一些列的操作的时候,但是这里有个很有趣的现象, paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));以下的话源自网络不是我说的,哈哈:PorterDuffXfermode其实就是简单的图形交并集计算,比如重叠的部分删掉或者叠加等等,事实上呢!PorterDuffXfermode的计算绝非是根据于此!如下图所示,最常见的应用就是蒙板绘制,利用源图作为蒙板“抠出”目标图上的图像。
Xfermode 国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,给大家一个非常赞的关于Xfermode的文章链接,地址:
http://www.cnblogs.com/tianzhijiexian/p/4297172.html
/** * 主要是得到需要绘制的rect的区域 * @param bitmap */
private void setBitmap(Bitmap bitmap) { Canvas canvas = new Canvas(); mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); //用指定的方式填充位图的像素。 mBitmap.eraseColor(Color.rgb(Color.red(mPaint.getColor()), Color.green(mPaint.getColor()), Color.blue(mPaint.getColor()))); //用画布制定位图绘制 canvas.setBitmap(mBitmap); Paint paint = new Paint(); //如果把这行代码注释掉这里生成的东西更加有意思 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); canvas.drawBitmap(bitmap, 0, 0, paint); //src 代表需要绘制的区域 mOldRect.set(0, 0, mBitmap.getWidth()/4, mBitmap.getHeight()/4); }
根据笔的宽度的变化,笔的透明度要和发生变化。
/** * 更具笔的宽度的变化,笔的透明度要和发生变化 * @param point * @return */
private ControllerPoint getWithPointAlphaPoint(ControllerPoint point) { ControllerPoint nPoint = new ControllerPoint(); nPoint.x = point.x; nPoint.y = point.y; nPoint.width = point.width; int alpha = (int) (255 * point.width / mBaseWidth / 2); if (alpha < 10) { alpha = 10; } else if (alpha > 255) { alpha = 255; } nPoint.alpha = alpha; return nPoint; }
这里才是关键的地方,原理就是不断的绘制 Bitmap,通过 Bitmap 构建成为一根线,同时绘制区域的大小,直接导致笔的宽度的变化。
protected void drawLine(Canvas canvas, double x0, double y0, double w0, int a0, double x1, double y1, double w1, int a1, Paint paint) { double curDis = Math.hypot(x0 - x1, y0 - y1); int factor = 2; if (paint.getStrokeWidth() < 6) { factor = 1; } else if (paint.getStrokeWidth() > 60) { factor = 3; } int steps = 1 + (int) (curDis / factor); double deltaX = (x1 - x0) / steps; double deltaY = (y1 - y0) / steps; double deltaW = (w1 - w0) / steps; double deltaA = (a1 - a0) / steps; double x = x0; double y = y0; double w = w0; double a = a0; for (int i = 0; i < steps; i++) { if (w < 1.5) w = 1.5; //根据点的信息计算出需要把bitmap绘制在什么地方 mNeedDrawRect.set((float) (x - w / 2.0f), (float) (y - w / 2.0f), (float) (x + w / 2.0f), (float) (y + w / 2.0f)); //每次到这里来的话,这个笔的透明度就会发生改变,但是呢,这个笔不用同一个的话,有点麻烦 //我在这里做了个不是办法的办法,每次呢?我都从新new了一个新的笔,每次循环就new一个,内存就有很多的笔了 //这里new 新的笔 我放到外面去做了 //Paint newPaint = new Paint(paint); //当这里很小的时候,透明度就会很小,个人测试在3.0左右比较靠谱 paint.setAlpha((int) (a / 3.0f)); //第一个Rect 代表要绘制的bitmap 区域,第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方 canvas.drawBitmap(mBitmap, mOldRect, mNeedDrawRect, paint); x += deltaX; y += deltaY; w += deltaW; a += deltaA; } }
关于SteelPen,由于在上篇文章已经很详细的介绍了,这里就不在多少,需要的话,移步上篇文章。
package com.shiming.pen.new_code; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import com.shiming.pen.old_code.ControllerPoint; import static com.shiming.pen.new_code.IPenConfig.STEPFACTOR; /** * @author shiming * @version v1.0 create at 2017/10/17 * @des 钢笔 */
public class SteelPen extends BasePenExtend { public SteelPen(Context context) { super(context); } @Override protected void drawNeetToDo(Canvas canvas) { for (int i = 1; i < mHWPointList.size(); i++) { ControllerPoint point = mHWPointList.get(i); drawToPoint(canvas, point, mPaint); mCurPoint = point; } } @Override protected void moveNeetToDo(double curDis) { int steps = 1 + (int) curDis / STEPFACTOR; double step = 1.0 / steps; for (double t = 0; t < 1.0; t += step) { ControllerPoint point = mBezier.getPoint(t); mHWPointList.add(point); } } @Override protected void doNeetToDo(Canvas canvas, ControllerPoint point, Paint paint) { drawLine(canvas, mCurPoint.x, mCurPoint.y, mCurPoint.width, point.x, point.y, point.width, paint); } /** * 其实这里才是关键的地方,通过画布画椭圆,每一个点都是一个椭圆,这个椭圆的所有细节,逐渐构建出一个完美的笔尖 * 和笔锋的效果,我觉得在这里需要大量的测试,其实就对低端手机进行排查,看我们绘制的笔的宽度是多少,绘制多少个椭圆 * 然后在低端手机上不会那么卡,当然你哪一个N年前的手机给我,那也的卡,只不过需要适中的范围里面 * * @param canvas * @param x0 * @param y0 * @param w0 * @param x1 * @param y1 * @param w1 * @param paint */ private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint) { //求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园 double curDis = Math.hypot(x0 - x1, y0 - y1); int steps = 1; if (paint.getStrokeWidth() < 6) { steps = 1 + (int) (curDis / 2); } else if (paint.getStrokeWidth() > 60) { steps = 1 + (int) (curDis / 4); } else { steps = 1 + (int) (curDis / 3); } double deltaX = (x1 - x0) / steps; double deltaY = (y1 - y0) / steps; double deltaW = (w1 - w0) / steps; double x = x0; double y = y0; double w = w0; for (int i = 0; i < steps; i++) { //都是用于表示坐标系中的一块矩形区域,并可以对其做一些简单操作 //精度不一样。Rect是使用int类型作为数值,RectF是使用float类型作为数值。 // Rect rect = new Rect(); RectF oval = new RectF(); oval.set((float) (x - w / 4.0f), (float) (y - w / 2.0f), (float) (x + w / 4.0f), (float) (y + w / 2.0f)); // oval.set((float)(x+w/4.0f), (float)(y+w/4.0f), (float)(x-w/4.0f), (float)(y-w/4.0f)); //最基本的实现,通过点控制线,绘制椭圆 canvas.drawOval(oval, paint); x += deltaX; y += deltaY; w += deltaW; } } }
最后说明几点:
当一直处于onDown事件的时候,就是按着屏幕不动的时候,最后绘制的关键方法也一直在走,消耗内存,需要优化。
绘制水彩笔的时候:这里由于在设置笔的透明度,会导致整个线,或者说整个画布的的颜透明度随着整个笔的透明度而变化, 所以在这里考虑是不是说,绘制毛笔的时候,每次都给它new 一个paint ,但是这里我还没有找到更好的办法,也需要优化。
关于扩展性的问题,目前这个Demo仅提供了三种的功能,后续会加上返回上一步,设置笔的颜色和宽度,在手指抬起来自动生成一张bitmap,设置画布等功能。
最后奉上项目GitHub的地址,欢迎点赞,欢迎提问,谢谢!
https://github.com/Shimingli/WritingPen
欢迎长按下图 -> 识别图中二维码
或者 扫一扫 关注我的公众号
- Android高级画笔效果实现的探索
- 安卓画笔实现的探索
- 探索Android 下拉刷新效果的实现
- 探索Android 下拉刷新效果的实现
- Android 在Canvas中实现画笔效果(一)--钢笔
- Android 画笔演示功能的实现
- 实现用画笔画出一个带有渐变效果的线条
- 创建android画笔程序的例子(有镜面效果)
- Android 画笔的使用
- Android高级UI ProgressBar实现各种效果的圆形进度
- Android屏幕上涂鸦画笔标记的实现
- android 画布,画笔,综合使用,可实现简单的画板
- Android画板开发(一) 基本画笔的实现
- Opencv实现画笔的功能
- android画笔的基本属性
- Android界面动画初探之--探索侧边栏折叠效果的实现
- Android 高级开发笔记 第一章 Activity的生命周期探索
- Android 画笔
- B/S和C/S架构系统的性能测试有感
- 《深入理解Java虚拟机》虚拟机类加载机制
- 欢迎使用CSDN-markdown编辑器
- git diff
- java的JDesktopPane背景图片自适应窗口大小示例
- Android高级画笔效果实现的探索
- iOS 11.0 iPhone X safeArea适配
- SAI绘画软件新手初学视频教程
- 社会矩阵:云技术如何激活十亿虚拟现实用户【52VR翻译】(附Carmack于OC4的演讲视频)
- 第2章 Git命令的基本操作
- Android 刮刮乐,自定义View
- 1.1数组和链表:19. Remove Nth Node From End of List(Leetcode)
- 一个java高级工程师的进阶之路【转】
- Thymeleaf 模板的使用