安卓畫筆筆鋒的實(shí)現(xiàn)探索(一)

  • 本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
  • 20181207日更新博客,寫這篇文章的時(shí)候,我對(duì)簡(jiǎn)書的寫作技巧還不熟悉,現(xiàn)在更新下說(shuō)明
  • 寫在前面的話:我就一直糾結(jié)啊,這種可以啊,沒(méi)毛病啊模庐,老鐵,我就一直做啊做油宜,實(shí)現(xiàn)的效果掂碱,就是一直Move事件中的筆的寬度都是一樣的怜姿,是不是崩潰啊,的確很崩潰疼燥,最后我在想沧卢,能不能拿到按壓值MotionEvent.getPressure();但是最后通過(guò)一查,這個(gè)方法的返回值是這樣決定的:感應(yīng)出用戶的手指壓力醉者,當(dāng)然具體的級(jí)別由驅(qū)動(dòng)和物理硬件決定的但狭,我一直用手寫,這個(gè)值永遠(yuǎn)不變撬即,奔潰立磁,又一次崩潰,最后在研究一個(gè)opengl寫的Demo的時(shí)候剥槐,我發(fā)現(xiàn)了一個(gè)真理:那就是息罗,我要畫多長(zhǎng),是用戶手指決定的才沧,但是它的Move事件中接受到的點(diǎn)的數(shù)量是和這個(gè)距離沒(méi)有相對(duì)應(yīng)的關(guān)系,啊哈哈绍刮,對(duì)不對(duì)温圆,我接受了這個(gè)多點(diǎn),但是我要畫很長(zhǎng)的線孩革,是不是我的線就細(xì)了岁歉,但Move中的接受到的點(diǎn)數(shù)量一樣,我畫的距離短了膝蜈,是不是線就粗了锅移,這就是這個(gè)Demo的原理,頓時(shí)豁然開朗饱搏,春暖花開非剃!

不逼逼,看效果推沸,感覺我的書法還闊以备绽,哈哈!!

設(shè)置筆寬度為60鬓催,效果如下


微信圖片_20170910184918.png
image.png

這個(gè)效果明顯一點(diǎn)肺素,哈哈,是不是很有大師的寫字風(fēng)格


微信圖片_20170902142924.jpg

實(shí)現(xiàn)這個(gè)效果宇驾,大體用了40個(gè)小時(shí)倍靡,熬了3天夜,我未來(lái)的女朋友給我作證课舍,看了無(wú)數(shù)的文檔塌西,在git上有個(gè)哥們用opengGl3.0實(shí)現(xiàn)比我這個(gè)更牛逼的效果他挎,但是發(fā)現(xiàn)在低端手機(jī)上會(huì)報(bào)錯(cuò),原因是不支持openGL3.0雨让,導(dǎo)致Apk裝入失敗雇盖,1.0的api有看不懂,你說(shuō)我能怎么辦栖忠,我也很絕望按尥凇!同時(shí)感覺opengl更加節(jié)手機(jī)性能庵寞,but我錯(cuò)了狸相,在低端手機(jī)上使用opengl簡(jiǎn)直就是噩夢(mèng),卡的一逼捐川,算了不提了脓鹃,此功能的實(shí)現(xiàn)還是基于安卓的Paint,通過(guò)事件去繪制路徑古沥。

1.創(chuàng)建DrawPenView類繼承View

image.png

初始化筆,筆鋒的效果瘸右,我個(gè)人嘗試了使用三個(gè)筆,每次繪制的時(shí)候岩齿,三個(gè)筆一起繪制太颤,根據(jù)手指的滑動(dòng)速率的快慢去使其中的某個(gè)筆不用繪制,但是這個(gè)效果稀爛盹沈,所以view的還是用一只筆即可龄章,

        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setStrokeWidth(14);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);//結(jié)束的筆畫為圓心
        mPaint.setStrokeJoin(Paint.Join.ROUND);//連接處元
        mPaint.setAlpha(0xFF);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeMiter(1.0f);

初始化bitmap,和畫布乞封,畫布在這里主要是生成一張bitmap的

  private void initParameter(Context context) {
        mContext = context;
        DisplayMetrics dm = new DisplayMetrics();
        ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(dm);
        mBitmap = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888);
        //筆的控制類
        mVisualStrokePen=new VisualStrokePen(mContext);
        initPaint(mContext);
        initCanvas();
    }

  private void initCanvas() {
        mCanvas = new Canvas(mBitmap);
        //設(shè)置畫布的顏色的問(wèn)題
        mCanvas.drawColor(Color.TRANSPARENT);
    }

重寫onDraw()方法:由于項(xiàng)目需要做裙,在這里我僅僅提供了兩個(gè)方法:清除畫布和繪制。擴(kuò)展的功能有:返回上一步的繪制步驟肃晚,設(shè)置畫筆的屬性锚贱,mark筆,毛筆关串,鋼筆惋鸥,圓珠筆,鉛筆等一切的控制都在這里進(jìn)行

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        switch (mCanvasCode) {
            case CANVAS_NORMAL:
                mVisualStrokePen.draw(canvas);
                break;
            case CANVAS_RESET:
                reset();
                break;
            default:
                Log.e(TAG, "onDraw" + Integer.toString(mCanvasCode));
                break;
        }
        super.onDraw(canvas);
    }

2.認(rèn)識(shí)MotionEvent對(duì)象

當(dāng)用戶觸摸屏幕時(shí)悍缠,將創(chuàng)建一個(gè)MontionEvent對(duì)象卦绣。MotionEvent包含了關(guān)于發(fā)生觸摸的位置和時(shí)間的信息,以及觸摸事件的其他細(xì)節(jié)飞蚓。

   /**
     event.getAction() //獲取觸控動(dòng)作比如ACTION_DOWN
     event.getPointerCount(); //獲取觸控點(diǎn)的數(shù)量滤港,比如2則可能是兩個(gè)手指同時(shí)按壓屏幕
     event.getPointerId(nID); //對(duì)于每個(gè)觸控的點(diǎn)的細(xì)節(jié),我們可以通過(guò)一個(gè)循環(huán)執(zhí)行g(shù)etPointerId方法獲取索引
     event.getX(nID); //獲取第nID個(gè)觸控點(diǎn)的x位置,記錄的第一個(gè)點(diǎn)為getX,getY
     event.getY(nID); //獲取第nID個(gè)點(diǎn)觸控的y位置
     event.getPressure(nID); //LCD可以感應(yīng)出用戶的手指壓力溅漾,當(dāng)然具體的級(jí)別由驅(qū)動(dòng)和物理硬件決定的
     event.getDownTime() //按下開始時(shí)間
     event.getEventTime() // 事件結(jié)束時(shí)間
     event.getEventTime()-event.getDownTime()); //總共按下時(shí)花費(fèi)時(shí)間
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //測(cè)試過(guò)程中山叮,當(dāng)使用到event的時(shí)候,產(chǎn)生了沒(méi)有收到事件的問(wèn)題添履,所以在這里需要obtian的一下
        MotionEvent event2 = MotionEvent.obtain(event);
        switch (event2.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                setCanvasCode(CANVAS_NORMAL);
                mVisualStrokePen.onDown(mVisualStrokePen.createMotionElement(event2));
                break;
            case MotionEvent.ACTION_MOVE:
                mVisualStrokePen.onMove(mVisualStrokePen.createMotionElement(event2));
                break;
            case MotionEvent.ACTION_UP:
                long time = System.currentTimeMillis();
                mVisualStrokePen.onUp(mVisualStrokePen.createMotionElement(event2),mCanvas);
                break;
            default:
                break;
        }
        invalidate();
        return true;
    }

在這里我需要提到一個(gè)MotionEvent的api:motionEvent.getToolType(0);返回的以下四種的值屁倔,

TOOL_TYPE_UNKNOWN :不知道什么畫的
TOOL_TYPE_FINGER :手指
TOOL_TYPE_STYLUS :筆畫的
TOOL_TYPE_MOUSE :該工具是一個(gè)鼠標(biāo)或觸控板
TOOL_TYPE_ERASER :工具是一塊橡皮或一筆用于倒立的姿勢(shì)
看見沒(méi),臥槽暮胧,以前都不知道锐借,這個(gè)類知道我們用什么屬性在寫字,
event.getPressure(); //可以感應(yīng)出用戶的手指壓力往衷,當(dāng)然具體的級(jí)別由驅(qū)動(dòng)和物理硬件決定的,我的手機(jī)上為1
motionEvent.getEventTime():事件發(fā)生的事件钞翔,在我此時(shí)的事件是shiming==8359650,而且是跟隨著系統(tǒng)的時(shí)間而定
···

 /**
  * Tool type constant: Unknown tool type.
 * This constant is used when the tool type is not known or is not relevant,
 * such as for a trackball or other non-pointing device.
 *
 * @see #getToolType
 */
public static final int TOOL_TYPE_UNKNOWN = 0;

/**
 * Tool type constant: The tool is a finger.
 *
 * @see #getToolType
 */
public static final int TOOL_TYPE_FINGER = 1;

/**
 * Tool type constant: The tool is a stylus.
 *
 * @see #getToolType
 */
public static final int TOOL_TYPE_STYLUS = 2;

/**
 * Tool type constant: The tool is a mouse or trackpad.
 *
 * @see #getToolType
 */
public static final int TOOL_TYPE_MOUSE = 3;

/**
 * Tool type constant: The tool is an eraser or a stylus being used in an inverted posture.
 *
 * @see #getToolType
 */
public static final int TOOL_TYPE_ERASER = 4:

關(guān)于MotionElement 類:記錄下五個(gè)參數(shù):坐標(biāo)x y席舍,壓力值布轿,什么在屏幕上寫的,還有事件發(fā)生的時(shí)間来颤。

  public static class MotionElement {

        public float x;
        public float y;
        public float pressure;
        public int tooltype;
        public long timestamp;

        public MotionElement(float mx, float my, float mp, int ttype, long mt) {
            x = mx;
            y = my;
            pressure = mp;
            tooltype = ttype;
            timestamp = mt;
        }

    }
 /**
     * event.getPressure(); //LCD可以感應(yīng)出用戶的手指壓力汰扭,當(dāng)然具體的級(jí)別由驅(qū)動(dòng)和物理硬件決定的,我的手機(jī)上為1
     * @param motionEvent
     * @return
     */
    public MotionElement createMotionElement(MotionEvent motionEvent) {
        System.out.println("shiming== 0000=="+motionEvent.getToolType(0));
        System.out.println("shiming=="+motionEvent.getPressure());
        System.out.println("shiming=="+motionEvent.getEventTime());
        MotionElement motionElement = new MotionElement(motionEvent.getX(), motionEvent.getY(),
                motionEvent.getPressure(), motionEvent.getToolType(0),
                motionEvent.getEventTime());
        return motionElement;
    }

3.清除畫布

Xfermode國(guó)外有大神稱之為過(guò)渡模式,這種翻譯比較貼切但恐怕不易理解福铅,大家也可以直接稱之為圖像混合模式东且,因?yàn)樗^的“過(guò)渡”其實(shí)就是圖像混合的一種把paint.setXfermode(Xfermode xfermode)的模式設(shè)置為clear,使用我們新建的canvas去drapaint這個(gè)筆本讥,記得清除完了,要把mode設(shè)置為null
有偏文檔介紹的很好鲁冯,我在這里拋磚引玉一下拷沸,就不班門弄斧了:http://www.cnblogs.com/tianzhijiexian/p/4297172.html

  /**
     *清除畫布,記得清除點(diǎn)的集合
     */
    public void reset() {
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        mCanvas.drawPaint(mPaint);
        mPaint.setXfermode(null);
        mVisualStrokePen.clear();
    }

4.關(guān)于Bezier曲線

先發(fā)個(gè)圖薯演,嘿嘿撞芍,我自己手畫的,看不清沒(méi)關(guān)系跨扮,只需知道4個(gè)點(diǎn)的關(guān)系序无,想象一下曲線就行

微信圖片_20170826183403.jpg
image.png

知道兩點(diǎn)連接起來(lái)是直線,當(dāng)我們不斷的求出兩個(gè)點(diǎn)的控制點(diǎn)衡创,把無(wú)數(shù)的控制點(diǎn)繪制在一起就是一條完美的曲線帝嗡,反正我這樣子理解的,當(dāng)然我在這里也做了一個(gè)width的控制璃氢,和這種的原理差不多哟玷。

 public void init(float lastx, float lasty, float lastWidth, float x, float y, float width)
    {
        //資源點(diǎn)設(shè)置,最后的點(diǎn)的為資源點(diǎn)
        mSource.set(lastx, lasty, lastWidth);
        float xmid = getMid(lastx, x);
        float ymid = getMid(lasty, y);
        float wmid = getMid(lastWidth, width);
        //距離點(diǎn)為平均點(diǎn)
        mDestination.set(xmid, ymid, wmid);
        //控制點(diǎn)為當(dāng)前的距離點(diǎn)
        mControl.set(getMid(lastx,xmid),getMid(lasty,ymid),getMid(lastWidth,wmid));
        //下個(gè)控制點(diǎn)為當(dāng)前點(diǎn)
        mNextControl.set(x, y, width);
    }

 /**
     *
     * @param x1 一個(gè)點(diǎn)的x
     * @param x2 一個(gè)點(diǎn)的x
     * @return
     */
   /**
     *
     * @param x1 一個(gè)點(diǎn)的x
     * @param x2 一個(gè)點(diǎn)的x
     * @return
     */
    private float getMid(float x1, float x2) {
        return (float)((x1 + x2) / 2.0);
    }

    private double getWidth(double w0, double w1, double t){
        return w0 + (w1 - w0) * t;
    }

以上記得知道個(gè)步驟一也,才能方便理解巢寡,當(dāng)這個(gè)點(diǎn)是我們資源點(diǎn)的時(shí)候喉脖,或者是當(dāng)前點(diǎn),那么它下一步就會(huì)成為一個(gè)新的資源點(diǎn)抑月,需要不斷的替換當(dāng)前的起點(diǎn)和終點(diǎn)树叽,那么才可以形成一個(gè)曲線

    /**
     * 替換就的點(diǎn),原來(lái)的距離點(diǎn)變換為資源點(diǎn)谦絮,控制點(diǎn)變?yōu)樵瓉?lái)的下一個(gè)控制點(diǎn)题诵,距離點(diǎn)取原來(lái)控制點(diǎn)的和新的的一半
     * 下個(gè)控制點(diǎn)為新的點(diǎn)
     * @param x 新的點(diǎn)的坐標(biāo)
     * @param y 新的點(diǎn)的坐標(biāo)
     * @param width
     */
    public void addNode(float x, float y, float width){
        mSource.set(mDestination);
        mControl.set(mNextControl);
        mDestination.set(getMid(mNextControl.x, x), getMid(mNextControl.y, y), getMid(mNextControl.width, width));
        mNextControl.set(x, y, width);
    }

是不是看不懂,對(duì)挨稿,看不懂就對(duì)了仇轻,去下面看代碼,記得在本子上多畫幾個(gè)點(diǎn)奶甘,想象一下這樣變換的位置篷店,然后就會(huì)明白了這真的是一個(gè)美妙的曲線,比女朋友還漂亮臭家,哈哈疲陕,扯皮了

關(guān)于手指抬起來(lái)的時(shí)候的方法: 結(jié)合手指抬起來(lái)的動(dòng)作,告訴現(xiàn)在的曲線控制點(diǎn)也必須變化钉赁,其實(shí)在這里也不需要結(jié)合著up事件使用因?yàn)樵赿own的事件中蹄殃,所有點(diǎn)都會(huì)被重置,然后設(shè)置這個(gè)沒(méi)有多少意義你踩,但是可以改變下個(gè)事件的朝向改變先留著诅岩,因?yàn)楹竺嫒绻枰刂普麄€(gè)顏色的改變的話,我的依靠這個(gè)方法带膜,還有按壓的時(shí)間的變化

  /**
     * 結(jié)合手指抬起來(lái)的動(dòng)作吩谦,告訴現(xiàn)在的曲線控制點(diǎn)也必須變化,其實(shí)在這里也不需要結(jié)合著up事件使用
     * 因?yàn)樵赿own的事件中膝藕,所有點(diǎn)都會(huì)被重置式廷,然后設(shè)置這個(gè)沒(méi)有多少意義,但是可以改變下個(gè)事件的朝向改變
     * 先留著芭挽,因?yàn)楹竺嫒绻枰刂普麄€(gè)顏色的改變的話滑废,我的依靠這個(gè)方法,還有按壓的時(shí)間的變化
     */
     /**
     * 結(jié)合手指抬起來(lái)的動(dòng)作袜爪,告訴現(xiàn)在的曲線控制點(diǎn)也必須變化蠕趁,其實(shí)在這里也不需要結(jié)合著up事件使用
     * 因?yàn)樵赿own的事件中,所有點(diǎn)都會(huì)被重置辛馆,然后設(shè)置這個(gè)沒(méi)有多少意義妻导,但是可以改變下個(gè)事件的朝向改變
     * 先留著,因?yàn)楹竺嫒绻枰刂普麄€(gè)顏色的改變的話,我的依靠這個(gè)方法倔韭,還有按壓的時(shí)間的變化
     */
    public void end() {
        mSource.set(mDestination);
        float x = getMid(mNextControl.x, mSource.x);
        float y = getMid(mNextControl.y, mSource.y);
        float w = getMid(mNextControl.width, mSource.width);
        mControl.set(x, y, w);
        mDestination.set(mNextControl);
    }

還有個(gè)方法:我的提一句术浪,是不是想一個(gè)一元二次的方程,哈哈寿酌!這個(gè)不是我寫的胰苏,這個(gè)是基于git上開源的寫的,是不是有點(diǎn)高中數(shù)學(xué)的影響了醇疼,哈哈硕并,對(duì)就是這樣的,

image.png
 * 三階曲線的控制點(diǎn)
 * @param p0
 * @param p1
 * @param p2
 * @param t
 * @return
 */
private double getValue(double p0, double p1, double p2, double t){
    double A = p2 - 2 * p1 + p0;
    double B = 2 * (p1 - p0);
    double C = p0;
    return A * t * t + B * t + C;
}

5.關(guān)于StrokePen秧荆,這個(gè)類才是所有的關(guān)鍵倔毙,如圖分析:其實(shí)原理就是,通過(guò)安卓事件收集一個(gè)點(diǎn)的集合乙濒,這個(gè)點(diǎn)的集合的第一點(diǎn)和第二個(gè)點(diǎn)陕赃,繪制一個(gè)橢圓一個(gè)橢圓。

image.png
 private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint){
         //求兩個(gè)數(shù)字的平方根 x的平方+y的平方在開方記得X的平方+y的平方=1颁股,這就是一個(gè)園
        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++){
            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));
            //最基本的實(shí)現(xiàn)么库,通過(guò)點(diǎn)控制線,繪制橢圓
            canvas.drawOval(oval, paint);
            x+=deltaX;
            y+=deltaY;
            w+=deltaW;
        }
    }

說(shuō)明

  • 求兩個(gè)數(shù)字的平方根 x的平方+y的平方在開方記得X的平方+y的平方=1甘有,這就是一個(gè)園
    double curDis = Math.hypot(x0-x1, y0-y1);
  • 繪制多少個(gè)橢圓诉儒,我們可以根據(jù)筆的寬度,當(dāng)筆的寬度和大的時(shí)候,我們繪制的可以適當(dāng)減少步驟
  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);
        }
  • 關(guān)于Rext和RexF的區(qū)別:Rect是使用int類型作為數(shù)值亏掀,RectF是使用float類型作為數(shù)值忱反。很明顯這里我們需要更高的精度
  • 繪制的原理,就是每個(gè)記錄下的點(diǎn)繪制一個(gè)橢圓滤愕,當(dāng)無(wú)數(shù)個(gè)的橢圓重合在一起就是一個(gè)線温算,這個(gè)線的寬度和橢圓的形狀有關(guān)系
            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));
            //最基本的實(shí)現(xiàn),通過(guò)點(diǎn)控制線该互,繪制橢圓
            canvas.drawOval(oval, paint);

這里需要在view中的onDraw中調(diào)用,本來(lái)我開始是想說(shuō)能不能再一開始的時(shí)候韭畸,down事件的時(shí)候宇智,給他畫個(gè)園,但是這個(gè)園的半徑我控制不好胰丁,所以在代碼中我留下這個(gè)問(wèn)題随橘,以后需要做更難的效果的時(shí)候,我來(lái)把這個(gè)開始的步驟補(bǔ)上锦庸。

/**
     * 早onDraw需要調(diào)用
     * @param canvas 畫布
     */
    public void draw(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        //點(diǎn)的集合少 不去繪制
        if (mHWPointList == null || mHWPointList.size() < 1)
            return;
        //當(dāng)控制點(diǎn)的集合很少的時(shí)候机蔗,需要畫個(gè)小圓,但是需要算法
        if (mHWPointList.size() < 2) {
            ControllerPoint point = mHWPointList.get(0);
            //由于此問(wèn)題在算法上還沒(méi)有實(shí)現(xiàn),所以暫時(shí)不給他畫圓圈
            //canvas.drawCircle(point.x, point.y, point.width, mPaint);
        } else {
            curPoint = mHWPointList.get(0);
            for (int i = 1; i < mHWPointList.size(); i++) {
                ControllerPoint point = mHWPointList.get(i);
                drawToPoint(canvas, point, mPaint);
                curPoint = point;
            }
        }
    }

Down事件處理

     * 手指的down事件
     * @param mElement
     */
    public void onDown(MotionElement mElement) {
        mPaint.setXfermode(null);
        mPath = new Path();
        mPointList.clear();
        mHWPointList.clear();
        //記錄down的控制點(diǎn)的信息
        ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y);
        //如果用筆畫的畫我的屏幕萝嘁,記錄他寬度的和壓力值的乘梆掸,但是哇,
        if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
            mLastWidth = mElement.pressure * mBaseWidth;
        } else {
            //如果是手指畫的牙言,我們?nèi)∷?.8
            mLastWidth = 0.8 * mBaseWidth;
        }
        //down下的點(diǎn)的寬度
        curPoint.width = (float) mLastWidth;
        mLastVel = 0;

        mPointList.add(curPoint);
        //記錄當(dāng)前的點(diǎn)
        mLastPoint = curPoint;
        //繪制起點(diǎn)
        mPath.moveTo(mElement.x, mElement.y);
    }
···

Move事件的處理

 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平方和的二次方根 想象一個(gè)例子 1+1的平方根為1.4 (x2+y2)開根號(hào)
        double curDis = Math.hypot(deltaX, deltaY);
        //我們求出的這個(gè)值越小酸钦,畫的點(diǎn)或者是繪制橢圓形越多,這個(gè)值越大的話咱枉,繪制的越少卑硫,筆就越細(xì),寬度越小
        double curVel = curDis * DIS_VEL_CAL_FACTOR;
        System.out.println("shiming==="+curDis+" "+curVel+" "+deltaX+" "+deltaY);
        double curWidth;
        //點(diǎn)的集合少蚕断,我們得必須改變寬度,每次點(diǎn)擊的down的時(shí)候欢伏,這個(gè)事件
        if (mPointList.size() < 2) {
            System.out.println("shiming==dian shao");
            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 {
            System.out.println("shiming==dian duo");
            mLastVel = curVel;
            if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
                curWidth = mElement.pressure * mBaseWidth;
            } else {
                //由于我們手機(jī)是觸屏的手機(jī),滑動(dòng)的速度也不慢亿乳,所以硝拧,一般會(huì)走到這里來(lái)
                //闡明一點(diǎn),當(dāng)滑動(dòng)的速度很快的時(shí)候风皿,這個(gè)值就越小河爹,越慢就越大,依靠著mlastWidth不斷的變換
                curWidth = calcNewWidth(curVel, mLastVel, curDis, 1.5,
                        mLastWidth);
                System.out.println("shiming=="+curVel+" "+mLastVel+" "+curDis+" " +mLastWidth);
                System.out.println("shiming==dian duo"+curWidth);
            }
            curPoint.width = (float) curWidth;
            mBezier.AddNode(curPoint);
        }
        //每次移動(dòng)的話桐款,這里賦值新的值
        mLastWidth = curWidth;

        mPointList.add(curPoint);

        int steps = 1 + (int) curDis / STEPFACTOR;
        System.out.println("shiming-- steps"+steps);
        double step = 1.0 / steps;
        for (double t = 0; t < 1.0; t += step) {
            ControllerPoint point = mBezier.GetPoint(t);
            mHWPointList.add(point);
        }

        mPath.quadTo(mLastPoint.x, mLastPoint.y,
                (mElement.x + mLastPoint.x) / 2,
                (mElement.y + mLastPoint.y) / 2);

        mLastPoint = curPoint;
    }

Up事件的處理:當(dāng)需要關(guān)心我們畫的這個(gè)bitmap的時(shí)候咸这,記得在up結(jié)束的時(shí)候,需要把這個(gè)繪制的東西需要重新繪制到我們自定義View的畫布上魔眨,這個(gè)畫筆是自己定義的媳维,而不是View里面onDraw(cavns)里面的畫布

 public void onUp(MotionElement mElement, Canvas canvas) {
        ControllerPoint curPoint = new ControllerPoint(mElement.x, mElement.y);
        double deltaX = curPoint.x - mLastPoint.x;
        double deltaY = curPoint.y - mLastPoint.y;
        double curDis = Math.hypot(deltaX, deltaY);

        if (mElement.tooltype == MotionEvent.TOOL_TYPE_STYLUS) {
            curPoint.width = (float) (mElement.pressure * mBaseWidth);
        } else {
            curPoint.width = 0;
        }

        mPointList.add(curPoint);

        mBezier.AddNode(curPoint);

        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);
        }
        //
        mBezier.End();
        for (double t = 0; t < 1.0; t += step) {
            ControllerPoint point = mBezier.GetPoint(t);
            mHWPointList.add(point);
        }

        mPath.quadTo(mLastPoint.x, mLastPoint.y,
                (mElement.x + mLastPoint.x) / 2,
                (mElement.y + mLastPoint.y) / 2);
        mPath.lineTo(mElement.x, mElement.y);
       // 手指up 我畫到紙上上
        draw(canvas);

    }

其實(shí)這里才是關(guān)鍵的地方,通過(guò)畫布畫橢圓遏暴,每一個(gè)點(diǎn)都是一個(gè)橢圓侄刽,這個(gè)橢圓的所有細(xì)節(jié),逐漸構(gòu)建出一個(gè)完美的筆尖 和筆鋒的效果,我覺得在這里需要大量的測(cè)試朋凉,其實(shí)就對(duì)低端手機(jī)進(jìn)行排查州丹,看我們繪制的筆的寬度是多少,繪制多少個(gè)橢圓然后在低端手機(jī)上不會(huì)那么卡杂彭,當(dāng)然你哪一個(gè)N年前的手機(jī)給我墓毒,那也的卡,只不過(guò)需要適中的范圍里面

   private void drawLine(Canvas canvas, double x0, double y0, double w0, double x1, double y1, double w1, Paint paint){
        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++){
            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));
            //最基本的實(shí)現(xiàn)亲怠,通過(guò)點(diǎn)控制線所计,繪制橢圓
            canvas.drawOval(oval, paint);
            x+=deltaX;
            y+=deltaY;
            w+=deltaW;
        }
    }

最后來(lái)張自畫像,可以,帥的一比团秽!

image.png

寫在最后的話主胧,不要皮叭首,講個(gè)原理,實(shí)現(xiàn)筆鋒的效果踪栋?到底怎么實(shí)現(xiàn)焙格,我先前糾結(jié)的是我一定要拿到手指的滑動(dòng)的速率,安卓也提供了這個(gè)api己英,ViewPager的源碼中提供了思路:如下圖所示

image.png
image.png
image.png
  • 當(dāng)這個(gè)速度大于了mMinimumVelocity這個(gè)值的時(shí)候间螟, Math.abs(velocity) > mMinimumVelocity那么我們的頁(yè)面就需要翻頁(yè)了,下面是ViewPager的實(shí)現(xiàn)的代碼损肛,很明顯就知道
    final float density = context.getResources().getDisplayMetrics().density;

        mTouchSlop = configuration.getScaledPagingTouchSlop();
        mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);

    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
            targetPage = velocity > 0 ? currentPage : currentPage + 1;
        } else {
            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
            targetPage = currentPage + (int) (pageOffset + truncator);
        }

        if (mItems.size() > 0) {
            final ItemInfo firstItem = mItems.get(0);
            final ItemInfo lastItem = mItems.get(mItems.size() - 1);

            // Only let the user target pages we have items for
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }

        return targetPage;
    }
  • 我就一直糾結(jié)啊厢破,這種可以啊,沒(méi)毛病啊治拿,老鐵摩泪,我就一直做啊做,實(shí)現(xiàn)的效果劫谅,就是一直Move事件中的筆的寬度都是一樣的见坑,是不是崩潰啊,的確很崩潰捏检,最后我在想荞驴,能不能拿到按壓值MotionEvent.getPressure();但是最后通過(guò)一查,這個(gè)方法的返回值是這樣決定的:感應(yīng)出用戶的手指壓力贯城,當(dāng)然具體的級(jí)別由驅(qū)動(dòng)和物理硬件決定的熊楼,我一直用手寫,這個(gè)值永遠(yuǎn)不變能犯,奔潰鲫骗,又一次崩潰,最后在研究一個(gè)opengl寫的Demo的時(shí)候踩晶,我發(fā)現(xiàn)了一個(gè)真理:那就是执泰,我要畫多長(zhǎng),是用戶手指決定的渡蜻,但是它的Move事件中接受到的點(diǎn)的數(shù)量是和這個(gè)距離沒(méi)有相對(duì)應(yīng)的關(guān)系术吝,啊哈哈,對(duì)不對(duì)茸苇,我接受了這個(gè)多點(diǎn)排苍,但是我要畫很長(zhǎng)的線,是不是我的線就細(xì)了税弃,但Move中的接受到的點(diǎn)數(shù)量一樣纪岁,我畫的距離短了凑队,是不是線就粗了则果,這就是這個(gè)Demo的原理幔翰,頓時(shí)豁然開朗,春暖花開西壮!
  • 最后奉上Git地址遗增,求贊,如果后續(xù)有空閑的時(shí)間款青,我會(huì)試著實(shí)現(xiàn)毛筆的效果和馬克筆的效果做修,求star,謝謝抡草!
    WritingPen
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饰及,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子康震,更是在濱河造成了極大的恐慌燎含,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腿短,死亡現(xiàn)場(chǎng)離奇詭異屏箍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)橘忱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門赴魁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人钝诚,你說(shuō)我怎么就攤上這事颖御。” “怎么了敲长?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵郎嫁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我祈噪,道長(zhǎng)泽铛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任辑鲤,我火速辦了婚禮盔腔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘月褥。我一直安慰自己弛随,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布宁赤。 她就那樣靜靜地躺著舀透,像睡著了一般。 火紅的嫁衣襯著肌膚如雪决左。 梳的紋絲不亂的頭發(fā)上愕够,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天走贪,我揣著相機(jī)與錄音,去河邊找鬼惑芭。 笑死坠狡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遂跟。 我是一名探鬼主播逃沿,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼幻锁!你這毒婦竟也來(lái)了凯亮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤哄尔,失蹤者是張志新(化名)和其女友劉穎触幼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體究飞,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡置谦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亿傅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片媒峡。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖葵擎,靈堂內(nèi)的尸體忽然破棺而出谅阿,到底是詐尸還是另有隱情,我是刑警寧澤酬滤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布签餐,位于F島的核電站,受9級(jí)特大地震影響盯串,放射性物質(zhì)發(fā)生泄漏氯檐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一体捏、第九天 我趴在偏房一處隱蔽的房頂上張望冠摄。 院中可真熱鬧,春花似錦几缭、人聲如沸河泳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拆挥。三九已至,卻和暖如春某抓,著一層夾襖步出監(jiān)牢的瞬間纸兔,已是汗流浹背黄锤。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留食拜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓副编,卻偏偏與公主長(zhǎng)得像负甸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痹届,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布 1呻待、下圖的效果的實(shí)現(xiàn)看這篇文章:http://...
    仕明同學(xué)閱讀 7,748評(píng)論 17 29
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料队腐? 從這篇文章中你...
    hw1212閱讀 12,693評(píng)論 2 59
  • 1. Touch事件和繪制事件的異同之處 Touch事件和繪制事件很類似蚕捉,都是由ViewRoot派發(fā)下來(lái)的,但是不...
    JackChen1024閱讀 585評(píng)論 0 1
  • View的事件體系 View的基礎(chǔ) view位置參數(shù)View的位置主要由它的四個(gè)頂點(diǎn)來(lái)決定柴淘,分別對(duì)應(yīng)于View的四...
    MZzF2HC閱讀 503評(píng)論 0 2
  • 2018年5月15日 星期二 陣雨 今天上班一直忙迫淹,下午做了一下午手術(shù),頸椎和腰椎已經(jīng)僵硬为严,小腿也有點(diǎn)拖不動(dòng)了敛熬。快...
    芳敏瑤閱讀 185評(píng)論 0 0