Paint畫筆

一、概念

畫筆,保存了繪製幾何圖形、文本和位圖的樣式和顏色信息

二、常用API

通過ALT+7查看Paint類結構圖,發現有大量的get、set方法。裏面還存有很多的native方法, 實際上當我們調用Paint類中的方法時,實際上是間接調用了native方法。以setSubpixelText方法爲例它就直接調用native方法nSetSubpixelText():

public void setSubpixelText(boolean subpixelText) {
    nSetSubpixelText(mNativePaint, subpixelText);
}

nSetSubpixelText()就是一個native方法:

@CriticalNative
private static native void nSetSubpixelText(long paintPtr, boolean subpixelText);

這需要我們掌握Paint類中常用的一些API方法:

mPaint = new Paint();//初始化
mPaint.setColor(Color.RED);//設置顏色
mPaint.setARGB(255,255,255,0);//設置paint對象顏色,範圍0~255
mPaint.setAlpha(200);//設置alpha透明度,範圍0~255
mPaint.setAntiAlias(true);//抗鋸齒
mPaint.setStyle(Paint.Style.STROKE);//描邊效果:FILL填充效果;STROKE:描邊;FILL_AND_STROKE:同時作用
mPaint.setStrokeWidth(4);//描邊寬度
mPaint.setStrokeCap(Paint.Cap.ROUND);//圓角效果:BUTT默認/ROUND圓角/SQUARE方角
mPaint.setStrokeJoin(Paint.Join.MITER);//拐角風格:MITER默認,尖角/ROUND/BEBEL切除尖角
mPaint.setShader(new SweepGradient(200,200,Color.BLUE,Color.RED));//設置環形渲染器,加上圓環效果
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));//設置圖層混合模式
mPaint.setColorFilter(new LightingColorFilter(0x00ffff,0x000000));//設置顏色過濾器
mPaint.setFilterBitmap(true);//設置雙線性過濾
mPaint.setMaskFilter(new BlurMaskFilter(10,BlurMaskFilter.Blur.NORMAL));//設置畫筆遮罩濾鏡,傳入度數和樣式
mPaint.setTextScaleX(2);//設置文本縮放倍數
mPaint.setTextSize(38);//設置文字大小
mPaint.setTextAlign(Paint.Align.LEFT);//對其方式
mPaint.setUnderlineText(true);//設置下劃線

String str="Android高級開發工程師";
Rect rect = new Rect();
mPaint.getTextBounds(str,0,str.length(),rect);//測量文本大小,將文本大小信息存放在rect中
mPaint.measureText(str);//獲取文本的寬
mPaint.getFontMetrics();//獲取文體度量對象
  • 1.setColor(int Color)參數具體的顏色紙,16進制數值,0xFFFF0000
  • 2.setARGB(int a,int r,int g,int b)參數:分別透明度、紅、綠、藍。0-255數值
  • 3.setShader(Shader shader)參數着色器對象,一般使用shader的幾個子類
    • LinearGradient:線性渲染
    • RadialGradient:環形渲染
    • SweepGradient:掃描渲染
    • BitmapShader:位圖渲染
    • ComposeShader:組合渲染,只能2個組合。例如LinearGradient+BitmapShader
  • 4.setColorFilter(ColorFilter colorFilter)設置顏色過濾。一般使用ColorFilter三個子類:
    • PorterDuffColorFilter:指定一個顏色和一種PorterDuff.Mode與繪製對象進行合成
    • ColorMatrixColorFilter:使用一個ColorMatrix來對顏色進行處理
    • LightingColorFilter光照效果

接下來重點看一下渲染器。

三、渲染器

3.1 LinearGradient線性渲染

構造方法:

public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
        @Nullable float positions[], @NonNull TileMode tile)	

參數:

  • (x0,y0):漸變起始點座標
  • (x1,y1):漸變結束點座標
  • color0:漸變開始點顏色,16進制的顏色表示,必須要帶有透明度
  • color1:漸變結束顏色的顏色
  • colors:漸變數組
  • positions:浮點型數組,position的取值範圍爲【0,1】,作用是指定某個位置的顏色值。
  • title:用於指定控件區域大於指定的漸變區域時,空白區域的顏色填充方法。端點範圍之外的着色規則,類型是TitleMode

對於position,new float[]{0.5f,1} 影響了漸變的效果,是相對位置。即50%~100%纔開始漸變 。可爲null,null就表示漸變爲線性變化:

//如果positions不爲空且其長度與顏色長度不相等,就拋出異常
if (positions != null && colors.length != positions.length) {
    throw new IllegalArgumentException("color and position arrays must be of equal length");
} 

使用:

		mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
        mPaint = new Paint();//初始化
        mPaint.setAntiAlias(true);//抗鋸齒
        mPaint.setStyle(Paint.Style.FILL);
        //1.線性渲染
        mShader = new LinearGradient(0,0,500,500,new int[]{Color.RED,Color.BLUE},new float[]{0.5f,1}, Shader.TileMode.CLAMP);
        mPaint.setShader(mShader);
        canvas.drawCircle(250,250,250,mPaint);

效果:

由紅向藍漸變,如果圓不明顯,可以改成矩形
在這裏插入圖片描述

3.2 環形渲染RadialGradient

構造方法:

public RadialGradient(float centerX, float centerY, float radius,
        @NonNull @ColorInt int colors[], @Nullable float stops[],
        @NonNull TileMode tileMode)

參數:

  • centerX,CenterY:輻射中心的座標
  • radius:輻射半徑
  • centerColor:輻射中心的顏色
  • edgeColor:輻射邊緣的顏色
  • colors:漸變顏色數組
  • stops:漸變位置數組,類似掃描漸變的position數組,取值【0,1】中心點爲0,半徑到達位置爲1.0f
  • tileMode:輻射範圍之外的着色規則,類型是TileMode。shader表示未覆蓋以外的 填充方式

使用:

		//2.環形渲染
        mPaint.setShader(new RadialGradient(250,250,500,new int[]{Color.RED,Color.BLUE,Color.YELLOW},null, Shader.TileMode.CLAMP));
        canvas.drawCircle(250,250,250,mPaint);

效果:

由圓心開始向外環形渲染
在這裏插入圖片描述

3.3 掃描渲染SweepGradient

構造方法:

public SweepGradient(float cx, float cy, @ColorInt int color0, @ColorInt int color1)

參數:

  • cx,cy:掃描的中心
  • color0:掃描的起始位置
  • color1:掃描的終止位置

使用:

		//3.掃描渲染
        mPaint.setShader(new SweepGradient(250,250,Color.RED,Color.BLUE));
        canvas.drawCircle(250,250,250,mPaint);

效果:

從x軸正方向,沿着順時針方向選擇360°
在這裏插入圖片描述

3.4 位圖渲染BitmapShader

構造方法:

public BitmapShader(@NonNull Bitmap bitmap, @NonNull TileMode tileX, @NonNull TileMode tileY)

參數:

  • bitmap:用來做模板的Bitmap對象
  • tileX:X軸方向的着色規則,類型是TileMode
  • tileY:Y軸方向的着色規則,類型是TileMode

使用:

		//4.位圖渲染
        mPaint.setShader(new BitmapShader(mBitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP));
//        canvas.drawRect(0,0,mBitmap.getWidth(),mBitmap.getHeight(),mPaint);
        canvas.drawCircle(250,250,250,mPaint);

效果:
在水平、垂直方向的最後一個像素,有一個拉伸填充效果。這個效果源自參數TileMode:

  • CLAMP 當繪製的區域超出圖片自身的區域,會以最後一個像素進行拉伸、填充
  • REPEAT 繪製區域超出渲染區域的部分,重複排版–平鋪效果,也就是copy
  • MIRROR 繪製區域超出渲染區域的部分,鏡像填充

3.5 組合渲染ComposeShader

構造方法:

public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB,
        @NonNull PorterDuff.Mode mode)
        
public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode)

參數:

  • shaderA,shaderB:要混合的兩種shader
  • xfermode mode:組合兩種shader顏色的模式
  • porterDuff.Mode mode:組合兩種shader顏色的模式

使用:

		//5.組合渲染
        mPaint.setShader(new ComposeShader(new BitmapShader(mBitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT),
                new LinearGradient(0,0,1000,1600,new int[]{Color.RED,Color.GREEN,Color.BLUE},null,Shader.TileMode.CLAMP),
                PorterDuff.Mode.MULTIPLY ));
        canvas.drawCircle(250,250,250,mPaint);

效果:

繪製區域遠大於位圖,重複排版,同時線性渲染也加持了。
PorterDuff.Mode.MULTIPLY:兩個渲染器進行渲染的圖層混合規則

四、PorterDuff.Mode圖層混合模式

就是將所繪製圖形的像素與Canvas中對應位置的像素按照一定規則進行混合,形成新的像素值,從而更新Canvas中最終的像素顏色值。

一共有18種模式:

  • CLEAR
  • SRC
  • DST
  • SRC_OVER
  • DST_OVER
  • SRC_IN
  • DST_IN
  • SRC_OUT
  • DST_OUT
  • SRC_ATOP
  • DST_ATOP
  • XOR
  • DARKEN
  • LIGHTEN
  • MULTIPLY
  • SCREEN
  • ADD
  • OVERLAY

每一種模式都代表一個規則,圖層混合後的效果,是通過規則計算alpha通道值和顏色值C。src:原圖像;dst:目標圖像

這裏可以參考google的官方demo!

4.1 離屏繪製

畫筆Paint.setXfermode(),首先要禁止硬件加速(因爲圖層混合有些api是不支持硬件加速的,但是系統默認開啓)

關於離屏繪製,首先開啓離屏繪製:

	//禁止硬件加速,圖層混合有些api不支持,但是系統默認開啓
        setLayerType(View.LAYER_TYPE_SOFTWARE,null);

        setBackgroundColor(Color.GRAY);//給自定義view加一個背景
        //離屏繪製
        int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);

        //目標圖
        canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
        //設置混合模式
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        //源圖,重疊區域右下角部分
        canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
        //清除混合模式
        mPaint.setXfermode(null);

        //進行圖層恢復
        canvas.restoreToCount(layerId);

在這裏插入圖片描述然後關閉離屏繪製:

        //禁止硬件加速,圖層混合有些api不支持,但是系統默認開啓
        setLayerType(View.LAYER_TYPE_SOFTWARE,null);

        setBackgroundColor(Color.GRAY);//給自定義view加一個背景
        //離屏繪製
//        int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);

        //目標圖
        canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
        //設置混合模式
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        //源圖,重疊區域右下角部分
        canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
        //清除混合模式
        mPaint.setXfermode(null);

        //進行圖層恢復
//        canvas.restoreToCount(layerId);

在這裏插入圖片描述發現灰色背景不見了,它直接參與了圖層混合的計算。不使用離屏繪製,直接考慮canvas的繪製,導致計算結果不正確。

使用離屏繪製(或者叫離屏緩衝),先創建一個圖層,將兩個圖層混合後的結果繪製到畫布上。 通過使用離屏繪製,把要繪製的內容單獨繪製在緩衝層,保證Xfermode的使用不會出現任何錯誤。

4.2 使用離屏繪製

4.2.1 Canvas.saveLayer()

Canvas.saveLayer()可以做短時的離屏緩衝,在繪製之前保存,繪製之後恢復

int layerId=canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint);
//畫方
canvas.drawBitmap(createRectBitmap(mWidth, mHeight), 0, 0, mPaint);
//設置Xfermode
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
//畫圓
canvas.drawBitmap(createCircleBitmap(mWidth, mHeight), 0, 0, mPaint);
//用完及時清除Xfermode
mPaint.setXfermode(null);
//進行圖層恢復
canvas.restoreToCount(layerId);

4.2.2 View.setLayerType()

View.setLayerType()直接把整個View都繪製在離屏緩衝中

//使用GPU來緩衝
setLayerType(LAYER_TYPE_HARDWARE);
//使用一個Bitmap來緩衝
setLayerType(LAYER_TYPE_SOFTWARE);

圖層混合模式只作用於src源圖像區域。每一種模式都有對應的公式來計算各自的alpha通道值、顏色值。以DARKEN爲例:

//alpha通道值:源圖像的alpha通道值+目標圖像的alpha通道值-源圖像的alpha通道值*目標圖像的alpha通道值
<p>\(\alpha_{out} = \alpha_{src} + \alpha_{dst} - \alpha_{src} * \alpha_{dst}\)</p>
//顏色的計算方式:(1-目標圖像的alpha通道值)* 源圖像的alpha顏色值 + (1-源圖像的alpha通道值)* 目標圖像的顏色值 + min(源圖像的顏色值,目標圖像的顏色值)
<p>\(C_{out} = (1 - \alpha_{dst}) * C_{src} + (1 - \alpha_{src}) * C_{dst} + min(C_{src}, C_{dst})\)</p>

4.3 案例-幸運刮刮卡

這裏用到了觸摸,在onTouchEvent方法中處理,繪製貝塞爾曲線,最後調用invalidate()方法,繪是的onDraw方法被調用。

onDraw方法中首先繪製結果,然後進行離屏繪製,將我們手勢的二階 貝塞爾曲線繪製到bitmap上,然後再繪製目標圖像。之後設置圖層混合模式爲SRC_OUT(清除相交的區域),最後繪製源圖像、清除圖層混合模式、圖層恢復。

package com.example.admin.uimaster.xfermode;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.example.admin.uimaster.R;


public class XfermodeEraserView extends View {
    private Paint  mPaint;
    private Bitmap mDstBmp, mSrcBmp, mTxtBmp;
    private Path mPath;

    public XfermodeEraserView(Context context) {
        this(context, null);
    }

    public XfermodeEraserView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public XfermodeEraserView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化畫筆
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(80);

        //禁用硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        //初始化圖片對象
        mTxtBmp = BitmapFactory.decodeResource(getResources(), R.drawable.result);
        mSrcBmp = BitmapFactory.decodeResource(getResources(), R.drawable.eraser);
        mDstBmp = Bitmap.createBitmap(mSrcBmp.getWidth(), mSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);

        //路徑(貝塞爾曲線)
        mPath = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //繪製刮獎結果
        canvas.drawBitmap(mTxtBmp, 0, 0, mPaint);

        //使用離屏繪製
        int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);

        //先將路徑繪製到 bitmap上
        Canvas dstCanvas = new Canvas(mDstBmp);
        dstCanvas.drawPath(mPath, mPaint);

        //繪製 目標圖像
        canvas.drawBitmap(mDstBmp, 0, 0, mPaint);
        //設置 模式 爲 SRC_OUT, 擦橡皮區域爲交集區域需要清掉像素
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
        //繪製源圖像
        canvas.drawBitmap(mSrcBmp, 0, 0, mPaint);

        mPaint.setXfermode(null);

        canvas.restoreToCount(layerID);
    }

    private float mEventX, mEventY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mEventX = event.getX();
                mEventY = event.getY();
                mPath.moveTo(mEventX, mEventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - mEventX) / 2 + mEventX;
                float endY = (event.getY() - mEventY) / 2 + mEventY;
                //畫二階貝塞爾曲線
                mPath.quadTo(mEventX, mEventY, endX, endY);
                mEventX = event.getX();
                mEventY = event.getY();
                break;
        }
        invalidate();
        return true; //消費事件
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章