安卓画笔笔锋的实现探索(二)

  • 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

1、下图的效果的实现看这篇文章:http://www.jianshu.com/p/6746d68ef2c3

微信图片_20171018135736.jpg

2、水彩笔效果一

//不设置
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//使用原图的资源文件为
mOriginBitmap = BitmapFactory.decodeResource( mContext.getResources(), R.mipmap.brush);
微信图片_20171018135732.jpg

3、水彩笔效果二,使用原图的资源文件为R.mipmap.cicrle

微信图片_20171018135726.jpg

4、水彩笔效果三,使用原图的资源文件为R.mipmap.tranglie

微信图片_20171018135715.jpg

5、设置paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));的效果

微信图片_20171018143623.jpg

如何实现的细节在上篇文章有很仔细的说明,可先看上篇文章

image.png

1.代码上我抽取了一个基类,主要是方便后面的扩展

/**
 * @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;
     }
}

2、关于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);
    }

关于BrushPen

初始化是从资源文件中获取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的计算绝非是根据于此!如下图所示,最常见的应用就是蒙板绘制,利用源图作为蒙板“抠出”目标图上的图像。

image.png

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,由于在上篇文章已经很详细的介绍了,这里就不在多少,需要的话,移步上篇文章:http://www.jianshu.com/p/6746d68ef2c3

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

最后说明几点:

1、当一直处于onDown事件的时候,就是按着屏幕不动的时候,最后绘制的关键方法也一直在走,消耗内存,需要优化

2、绘制水彩笔的时候:这里由于在设置笔的透明度,会导致整个线,或者说整个画布的的颜透明度随着整个笔的透明度而变化, 所以在这里考虑是不是说,绘制毛笔的时候,每次都给它new 一个paint ,但是这里我还没有找到更好的办法,也需要优化

3、关于扩展性的问题,目前这个Demo仅提供了三种的功能,后续会加上返回上一步,设置笔的颜色和宽度,在手指抬起来自动生成一张bitmap,设置画布等功能

Git地址,欢迎点赞,欢迎提问,谢谢:https://github.com/Shimingli/WritingPen

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容