Android界面開發:自定義View實踐之繪製篇

關於做者html

郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。java

文章目錄android

  • 一 View
  • 二 Paint
    • 2.1 顏色處理
    • 2.2 文字處理
    • 2.3 特殊處理
  • 三 Canvas
    • 3.1 界面繪製
    • 3.2 範圍裁切
    • 3.3 幾何變換
  • 四 Path
    • 4.1 添加圖形
    • 4.3 畫線(直線或曲線)
    • 4.3 輔助設置和計算

第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄git

文章源碼程序員

本文還提供了三個綜合性的完整實例來輔助理解。github

  • View繪製 - 圖片標籤效果實現
  • Canvas繪圖 - 水面漣漪效果實現
  • 二階貝塞爾曲線的應用 - 杯中倒水效果實現





第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄算法

本篇文章咱們來分析View繪製方面的實踐。canvas

一個簡單的自定義View數組

public class DrawView extends View {

    Paint paint = new Paint();

    public DrawView(Context context) {
        super(context);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.BLACK);
        canvas.drawCircle(150, 150, 150, paint);
    }
}複製代碼

它在屏幕上繪製了一個圓形,如圖:緩存

在處理繪製的時候有如下幾個關鍵點:

  • 處理繪製須要重寫繪製方法,經常使用的是View的onDraw(),固然咱們也可使用其餘的繪製方法來處理遮蓋關係。
  • 完成繪製的是Canvas類,該類提供了繪製系列方法drawXXX()。裁剪系列方法clipXXX()以及幾何變換方法translate()方法,還有輔助繪製的Path與Matrix。
  • 定製繪製的是Paint類,該類是繪製所用的畫筆,能夠實現特殊的繪製效果。

咱們分別來看看這個關鍵的角色。

一 View

咱們討論的第一個問題就是View/ViewGroup的繪製順序問題,繪製在View.draw()方法裏調用的,具體的執行順序是:

  1. drawBackground():繪製背景,不能重寫。
  2. onDraw():繪製主體。
  3. dispatchDraw():繪製子View
  4. onDrawForeground():繪製滑動邊緣漸變、滾動條和前景。

咱們先從個小例子開始。

咱們若是繼承View來實現自定義View。View類的onDraw()是空實現,因此咱們的繪製代碼寫在super.onDraw(canvas)的前面或者後面都沒有關係,以下所示:

public class DrawView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製代碼,寫在super.onDraw(canvas)先後都可
    }
}複製代碼

可是若是咱們繼承特定的控件,例如TextView。咱們就須要去考慮TextView的繪製邏輯。

public class DrawView extends TextView {
    @Override
    protected void onDraw(Canvas canvas) {

        //寫在前面,DrawView的繪製會先於TextView的繪製,TextView繪製的內容能夠會覆蓋DrawView
        super.onDraw(canvas);
        //寫在後面,DrawView的繪製會晚於TextView的繪製,DrawView繪製的內容能夠會覆蓋TextView
    }
}複製代碼
  • 寫在前面,DrawView的繪製會先於TextView的繪製,TextView繪製的內容能夠會覆蓋DrawView
  • 寫在後面,DrawView的繪製會晚於TextView的繪製,DrawView繪製的內容能夠會覆蓋TextView

具體怎麼作取決於你實際的需求,例如你若是想給TextView加個背景,就寫在super.onDraw(canvas)前面,想給TextView前面加些點綴,就
寫在super.onDraw(canvas)後面。

咱們來寫個例子理解下。

舉例

public class LabelImageView extends AppCompatImageView {

    /** * 梯形距離左上角的長度 */
    private static final int LABEL_LENGTH = 100;
    /** * 梯形斜邊的長度 */
    private static final int LABEL_HYPOTENUSE_LENGTH = 100;

    private Paint textPaint;
    private Paint backgroundPaint;
    private Path pathText;
    private Path pathBackground;


    public LabelImageView(Context context) {
        super(context);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //計算路徑
        calculatePath(getMeasuredWidth(), getMeasuredHeight());
        canvas.drawPath(pathBackground, backgroundPaint);
        canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        super.onDrawForeground(canvas);
    }

    /** * 計算路徑 x1 x2 * ................................ distance(標籤離右上角的垂直距離) * . . . . * . . .. y1 * . . . * . . . * . . y2 height(標籤垂直高度) * . . * ................................ */
    private void calculatePath(int measuredWidth, int measuredHeight) {

        int top = 185;
        int right = measuredWidth;

        float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
        float x2 = right - LABEL_HYPOTENUSE_LENGTH;
        float y1 = top + LABEL_LENGTH;
        float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;

        pathText.reset();
        pathText.moveTo(x1, top);
        pathText.lineTo(right, y2);
        pathText.close();

        pathBackground.reset();
        pathBackground.moveTo(x1, top);
        pathBackground.lineTo(x2, top);
        pathBackground.lineTo(right, y1);
        pathBackground.lineTo(right, y2);
        pathBackground.close();
    }

    private void init() {
        pathText = new Path();
        pathBackground = new Path();

        textPaint = new Paint();
        textPaint.setTextSize(50);
        textPaint.setFakeBoldText(true);
        textPaint.setColor(Color.WHITE);

        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.RED);
        backgroundPaint.setStyle(Paint.Style.FILL);
    }
}複製代碼

因此你能夠看到,當咱們繼承了一個View,根據需求的不一樣能夠選擇性重寫咱們須要的方法,在super前插入代碼和在super後插入代碼,效果是不同的。

  • draw():super.draw()以前,被背景蓋住;super.draw()後,蓋住前景;
  • onDraw():super.onDraw()以前,背景與主體內容以前;super.onDraw()以後,主體內容和子View之間;
  • dispatchDraw():super.dispatchDraw()以前,主體內容和子View之間;super.dispatchDraw()以後,子View和前景之間;
  • onDrawForeground():super.onDrawForeground()以前,子View和前景之間;super.onDrawForeground()以後,蓋住前景;

二 Paint

Paint:顧名思義,畫筆,經過Paint能夠對繪製行爲進行控制。

Paint有三種構造方法

public class Paint {
      //空的構造方法
      public Paint() {
          this(0);
      }

      //傳入flags來構造Paint,flags用來控制Paint的行爲,例如:抗鋸齒等
      public Paint(int flags) {
          mNativePaint = nInit();
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
          setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
          // TODO: Turning off hinting has undesirable side effects, we need to
          // revisit hinting once we add support for subpixel positioning
          // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
          // ? HINTING_OFF : HINTING_ON);
          mCompatScaling = mInvCompatScaling = 1;
          setTextLocales(LocaleList.getAdjustedDefault());
      }

      //傳入另一個Paint來構造新的Paint
      public Paint(Paint paint) {
          mNativePaint = nInitWithPaint(paint.getNativeInstance());
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
          setClassVariablesFrom(paint);
      }  
}複製代碼

2.1 顏色處理類

在Paint類中,處理顏色主要有三個方法。

  • setShader(Shader shader):用來處理顏色漸變
  • setColorFilter(ColorFilter filter):用來基於顏色進行過濾處理;
  • setXfermode(Xfermode xfermode) 用來處理源圖像和 View 已有內容的關係

setShader(Shader shader)

着色器是圖像領域的一個通用概念,它提供的是一套着色規則。

public Shader setShader(Shader shader)複製代碼

着色器具體由Shader的子類實現:

LinearGradient - 線性漸變

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)複製代碼
  • x0 y0 x1 y1:漸變的兩個端點的位置
  • color0 color1 是端點的顏色
  • tile:端點範圍以外的着色規則,類型是 TileMode。TileMode 一共有 3 個值可選: CLAMP, MIRROR 和 REPEAT。CLAMP

舉例

//線性漸變
Shader shader1 = new LinearGradient(0, 100, 200, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new LinearGradient(0, 600, 200, 600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new LinearGradient(0, 1100, 200, 1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);
canvas.drawRect(0, 1100, 1000, 1500, paint3);複製代碼

SweepGradient - 輻射漸變

public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode)複製代碼
  • centerX centerY:輻射中心的座標
  • radius:輻射半徑
  • centerColor:輻射中心的顏色
  • edgeColor:輻射邊緣的顏色
  • tileMode:輻射範圍以外的着色模式

舉例

//輻射漸變
Shader shader1 = new RadialGradient(0, 100, 200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new RadialGradient(0, 600, 200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new RadialGradient(0, 1100, 200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);複製代碼

BitmapShader - 位圖着色

使用位圖的像素來填充圖形或者文字。

public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)複製代碼
  • bitmap:用來作模板的 Bitmap 對象
  • tileX:橫向的 TileMode
  • tileY:縱向的 TileMode。

舉例

BitmapShader是一個頗有用的類,能夠利用該類作各類各樣的圖片裁剪。

//位圖着色
Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

//繪製圓形
canvas.drawCircle(500, 500, 300, paint1);複製代碼

ComposeShader - 組合Shader

ComposeShader能夠將連個Shader組合在一塊兒。

public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)複製代碼
  • shaderA, shaderB:兩個相繼使用的 Shader
  • mode: 兩個 Shader 的疊加模式,即 shaderA 和 shaderB 應該怎樣共同繪製。它的類型是PorterDuff.Mode。

PorterDuff.Mode用來指定兩個Shader疊加時顏色的繪製策略,它有不少種策略,也就是以一種怎樣的模式來與原圖像進行合成,具體以下:

藍色矩形爲原圖像,紅色圓形爲目標圖像。


更多細節能夠參見PorterDuff.Mode官方文檔

setColorFilter(ColorFilter filter)

顏色過濾器能夠將顏色按照必定的規則輸出,常見於各類濾鏡效果。

public ColorFilter setColorFilter(ColorFilter filter)複製代碼

咱們一般使用的是ColorFilter的三個子類:

LightingColorFilter - 模擬光照效果

public LightingColorFilter(int mul, int add)複製代碼

mul 和 add 都是和顏色值格式相同的 int 值,其中 mul 用來和目標像素相乘,add 用來和目標像素相加。

舉例

//顏色過濾器
ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
paint2.setColorFilter(colorFilter1);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);複製代碼

PorterDuffColorFilter - 模擬顏色混合效果

public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)複製代碼

PorterDuffColorFilter指定一種顏色和PorterDuff.Mode來與源圖像就行合成,也就是以一種怎樣的模式來與原圖像進行合成,咱們在上面已經講過這個內容。

舉例

//咱們在使用Xfermode的時候也是使用它的子類PorterDuffXfermode
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 畫方 
paint.setXfermode(xfermode); // 設置 Xfermode 
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 畫圓 
paint.setXfermode(null); // 用完及時清除 Xfermode複製代碼

ColorMatrixColorFilter - 顏色矩陣過濾

ColorMatrixColorFilter使用一個顏色矩陣ColorMatrix來對象圖像進行處理。

public ColorMatrixColorFilter(ColorMatrix matrix)複製代碼

ColorMatrix是一個4x5的矩陣

[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]複製代碼

經過計算,ColorMatrix能夠對要繪製的像素進行轉換,以下:

R’ = a*R + b*G + c*B + d*A + e;  
G’ = f*R + g*G + h*B + i*A + j;  
B’ = k*R + l*G + m*B + n*A + o;  
A’ = p*R + q*G + r*B + s*A + t;複製代碼

利用ColorMatrixColorFilter(能夠實現不少炫酷的濾鏡效果。

setXfermode(Xfermode xfermode)

Paint.setXfermode(Xfermode xfermode)方法,它也是一種混合圖像的方法。

Xfermode 指的是你要繪製的內容和 Canvas 的目標位置的內容應該怎樣結合計算出最終的顏色。但通俗地說,其實就是要你以繪製的內容做爲源圖像,以View中已有的內
容做爲目標圖像,選取一個PorterDuff.Mode做爲繪製內容的顏色處理方案。

小結

關於PorterDuff.Mode,咱們已經提到

  • ComposeShader:混合兩個Shader
  • PorterDuffColorFilter:增長一個單色的ColorFilter
  • Xfermode:指定原圖像與目標圖像的混合模式

這三種以不一樣的方式來使用PorterDuff.Mode,可是原理都是同樣的。

2.2 文字處理類

Paint裏有大量方法來設置文字的繪製屬性,事實上文字在Android底層是被當作圖片來處理的。

  • setTextSize(float textSize):設置文字大小
  • setTypeface(Typeface typeface):設置文字字體
  • setFakeBoldText(boolean fakeBoldText):是否使用僞粗體(並非提到size,而是在運行時描粗的)
  • setStrikeThruText(boolean strikeThruText):是否添加刪除線
  • setUnderlineText(boolean underlineText):是否添加下劃線
  • setTextSkewX(float skewX):設置文字傾斜度
  • setTextScaleX(float scaleX):設置文字橫向縮放
  • setLetterSpacing(float letterSpacing):設置文字間距
  • setFontFeatureSettings(String settings):使用CSS的font-feature-settings的方式來設置文字。
  • setTextAlign(Paint.Align align):設置文字對齊方式
  • setTextLocale(Locale locale):設置文字Local
  • setHinting(int mode):設置字體Hinting(微調),過向字體中加入 hinting 信息,讓矢量字體在尺寸太小的時候獲得針對性的修正,從而提升顯示效果。
  • setSubpixelText(boolean subpixelText):設置次像素級抗鋸齒,根據程序所運行的設備的屏幕類型,來進行鍼對性的次像素級的抗鋸齒計算,從而達到更好的抗鋸齒效果。

2.3 特殊效果類

setAntiAlias (boolean aa)

設置抗鋸齒,默認關閉,用來是圖像的繪製更加圓潤。咱們還能夠在初始化的時候設置Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);。

setStyle(Paint.Style style)

設置填充風格,

  • FILL 模式,填充
  • STROKE 模式,畫線
  • FILL_AND_STROKE 模式,填充 + 畫線

若是是劃線模式,咱們針對線條還能夠有多種設置。

setStrokeWidth(float width) - 設置線條粗細

setStrokeCap(Paint.Cap cap) - 設置線頭的形狀,默認爲 BUTT

  • UTT 平頭
  • ROUND 圓頭
  • SQUARE 方頭

setStrokeJoin(Paint.Join join) - 設置拐角的形狀。默認爲 MITER

  • MITER 尖角
  • BEVEL 平角
  • ROUND 圓角

setStrokeMiter(float miter)- 設置 MITER 型拐角的延長線的最大值

setDither(boolean dither)

設置圖像的抖動。

抖動是指把圖像從較高色彩深度(便可用的顏色數)向較低色彩深度的區域繪製時,在圖像中有意地插入噪點,經過有規律地擾亂圖像來讓圖像對於肉眼更加真實的作法。

固然這個效果旨在低位色的時候比較有用,例如,ARGB_4444 或者 RGB_565,不過如今Android默認的色彩深度都是32位的ARGB_8888,這個方法的效果沒有那麼明顯。

setFilterBitmap(boolean filter)

設置是否使用雙線性過濾來繪製 Bitmap 。

圖像在放大繪製的時候,默認使用的是最近鄰插值過濾,這種算法簡單,但會出現馬賽克現象;而若是開啓了雙線性過濾,就可讓結果圖像顯得更加平滑。

etPathEffect(PathEffect effect)

設置圖形的輪廓效果。Android有六種PathEffect:

  • CornerPathEffect:將拐角繪製成圓角
  • DiscretePathEffect:將線條進行隨機偏離
  • DashPathEffect:繪製虛線
  • PathDashPathEffect:使用指定的Path來繪製虛線
  • SumPathEffect:組合兩個PathEffect,疊加應用。
  • ComposePathEffect:組合兩個PathEffect,疊加應用。

CornerPathEffect(float radius)

  • float radius圓角半徑

DiscretePathEffect(float segmentLength, float deviation)

  • float segmentLength:用來拼接每一個線段的長度,
  • float deviation:偏離量

DashPathEffect(float[] intervals, float phase)

  • float[] intervals:指定了虛線的格式,數組中元素必須爲偶數(最少是 2 個),按照「畫線長度、空白長度、畫線長度、空白長度」……的順序排列
  • float phase:虛線的偏移量

PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)

  • Path shape:用來繪製的Path
  • float advance:兩個相鄰Path段起點間的間隔
  • float phase:虛線的偏移量
  • PathDashPathEffect.Style style:指定拐彎改變的時候 shape 的轉換方式:TRANSLATE:位移、ROTATE:旋轉、MORPH:變體

SumPathEffect(PathEffect first, PathEffect second)

  • PathEffect first:同時應用的PathEffect
  • PathEffect second:同時應用的PathEffect

ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

  • PathEffect outerpe:後應用的PathEffect
  • PathEffect innerpe:先應用用的PathEffect

舉例

//圖形輪廓效果
//繪製圓角
PathEffect cornerPathEffect = new CornerPathEffect(20);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(5);
paint1.setPathEffect(cornerPathEffect);

//繪製尖角
PathEffect discretePathEffect = new DiscretePathEffect(20, 5);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);
paint2.setPathEffect(discretePathEffect);

//繪製虛線
PathEffect dashPathEffect = new DashPathEffect(new float[]{20,10, 5, 10}, 0);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
paint3.setPathEffect(dashPathEffect);

//使用path來繪製虛線
Path path = new Path();//畫一個三角來填充虛線
path.lineTo(40, 40);
path.lineTo(0, 40);
path.close();
PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40, 0, PathDashPathEffect.Style.TRANSLATE);
paint4.setStyle(Paint.Style.STROKE);
paint4.setStrokeWidth(5);
paint4.setPathEffect(pathDashPathEffect);複製代碼

setShadowLayer(float radius, float dx, float dy, int shadowColor)

設置陰影圖層,處於目標下層圖層。

  • float radius:陰影半徑
  • float dx:陰影偏移量
  • float dy:陰影偏移量
  • int shadowColor:陰影顏色

舉例

paint1.setTextSize(200);
paint1.setShadowLayer(10, 0, 0, Color.RED);
canvas.drawText("Android", 80, 300 ,paint1);複製代碼

注:在硬件加速開啓的狀況下, setShadowLayer() 只支持文字的繪製,文字以外的繪製必須關閉硬件加速才能正常繪製陰影。若是 shadowColor 是半透明的,陰影的透明度就使用 shadowColor 本身
的透明度;而若是 shadowColor 是不透明的,陰影的透明度就使用 paint 的透明度。

setMaskFilter(MaskFilter maskfilter)

設置圖層遮罩層,處於目標上層圖層。

MaskFilter有兩個子類:

  • BlurMaskFilter:模糊效果
  • BlurMaskFilter:浮雕效果

舉例

模糊效果

  • BlurMaskFilter.Blur.NORMAL
  • BlurMaskFilter.Blur.SOLD
  • BlurMaskFilter.Blur.INNER
  • BlurMaskFilter.Blur.OUTTER

分別爲:






//設置遮罩圖層,處於目標上層圖層
//關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
paint2.setMaskFilter(blurMaskFilter);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);複製代碼

注:在硬件加速開啓的狀況下, setMaskFilter(MaskFilter maskfilter)只支持文字的繪製,文字以外的繪製必須關閉硬件加速才能正常繪製陰影。關閉硬件加速能夠調用
View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)或者在Activity標籤裏設置android:hardwareAccelerated="false"。

三 Canvas

Canvas實現了Android 2D圖形的繪製,底層基於Skia實現。

3.1 界面繪製

Canvas提供了豐富的對象繪製方法,通常都以drawXXX()打頭,繪製的對象包括:

  • 弧線(Arcs)
  • 顏色(Argb、Color)
  • 位圖(Bitmap)
  • 圓(Circle)
  • 點(Point)
  • 線(Line)
  • 矩形(Rect)
  • 圖片(Picture)
  • 圓角矩形(RoundRect)
  • 文本(Text)
  • 頂點(Vertices)
  • 路徑(Path)

這裏的方法大都很簡單,咱們來描述下期中比較複雜的方法。

弧線

public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) {
    native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
            useCenter, paint.getNativeInstance());
}複製代碼
  • float left, float top, float right, float bottom:左、上、右、下的座標。
  • float startAngle:弧形起始角度,Android座標系x軸正右的方向是0度的位置,順時針爲正角度,逆時針爲負角度。
  • float sweepAngle:弧形劃過的角度。
  • boolean useCenter:是否鏈接到圓心。若是不鏈接到圓心就是弧形,若是鏈接到圓心,就是扇形。

例如

paint.setStyle(Paint.Style.FILL);//填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint);
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint);
paint.setStyle(Paint.Style.STROKE);//畫線模式
paint.setStrokeWidth(5);
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint);複製代碼

位圖

  • public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) - 繪製位圖
  • **public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
    @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
           @Nullable Paint paint) - 繪製拉伸位圖**複製代碼

第一個方法很簡單,就是在指定的座標處開始繪製位圖。咱們着重來看看第二個方法,這個方法不是很經常使用(多是計算比較複雜的鍋😓),但這並不影響它強大的功能。

drawBitmapMesh()方法將位圖分爲若干網格,而後對每一個網格進行扭曲處理。咱們先來看看這個方法的參數:

  • @NonNull Bitmap bitmap:源位圖
  • int meshWidth:橫向上將源位圖劃分紅多少格
  • int meshHeight:縱向上將源位圖劃分紅多少格
  • @NonNull float[] verts:網格頂點座標數組,記錄扭曲後圖片各頂點的座標,數組大小爲 (meshWidth+1) (meshHeight+1) 2 + vertOffset
  • int vertOffset:記錄verts數組從第幾個數組元素開始扭曲
  • @Nullable int[] colors:設置網格頂點的顏色,該顏色會和位圖對應像素的顏色疊加,數組大小爲 (meshWidth+1) * (meshHeight+1) + colorOffset
  • int colorOffset:記錄colors從幾個數組元素開始取色
  • @Nullable Paint paint:畫筆

咱們來用drawBitmapMesh()方法實現一個水面漣漪效果。

舉例

/** * 利用Canvas.drawBitmapMeshC()方法對圖像作扭曲處理,模擬水波效果。 * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * guoxiaoxingse@163.com * * @author guoxiaoxing * @since 2017/9/12 下午3:44 */
public class RippleLayout extends FrameLayout {

    /** * 圖片橫向、縱向的格樹 */
    private final int MESH_WIDTH = 20;
    private final int MESH_HEIGHT = 20;

    /** * 圖片頂點數 */
    private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);

    /** * 原座標數組 */
    private final float[] originVerts = new float[VERTS_COUNT * 2];

    /** * 轉換後的座標數組 */
    private final float[] targetVerts = new float[VERTS_COUNT * 2];

    /** * 當前空間的圖像 */
    private Bitmap bitmap;

    /** * 水波寬度的一半 */
    private float rippleWidth = 100f;

    /** * 水波擴展的速度 */
    private float rippleRadius = 15f;

    /** * 水波半徑 */
    private float rippleSpeed = 15f;

    /** * 水波動畫是否在進行中 */
    private boolean isRippling;

    public RippleLayout(@NonNull Context context) {
        super(context);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (isRippling && bitmap != null) {
            canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0, null, 0, null);
        } else {
            super.dispatchDraw(canvas);
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                showRipple(ev.getX(), ev.getY());
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /** * 顯示水波動畫 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 */
    public void showRipple(final float originX, final float originY) {
        if (isRippling) {
            return;
        }
        initData();
        if (bitmap == null) {
            return;
        }
        isRippling = true;
        //循環次數,經過控件對角線距離計算,確保水波紋徹底消失
        int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
        final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
        Observable.interval(0, 10, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .take(count + 1)
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(@NonNull Long aLong) throws Exception {
                        rippleRadius = aLong * rippleSpeed;
                        warp(originX, originY);
                        if (aLong == count) {
                            isRippling = false;
                        }
                    }
                });
    }

    /** * 初始化 Bitmap 及對應數組 */
    private void initData() {
        bitmap = getCacheBitmapFromView(this);
        if (bitmap == null) {
            return;
        }
        float bitmapWidth = bitmap.getWidth();
        float bitmapHeight = bitmap.getHeight();
        int index = 0;
        for (int height = 0; height <= MESH_HEIGHT; height++) {
            float y = bitmapHeight * height / MESH_HEIGHT;
            for (int width = 0; width <= MESH_WIDTH; width++) {
                float x = bitmapWidth * width / MESH_WIDTH;
                originVerts[index * 2] = targetVerts[index * 2] = x;
                originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
                index += 1;
            }
        }
    }

    /** * 圖片轉換 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 */
    private void warp(float originX, float originY) {
        for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
            float staticX = originVerts[i];
            float staticY = originVerts[i + 1];
            float length = getLength(staticX - originX, staticY - originY);
            if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
                PointF point = getRipplePoint(originX, originY, staticX, staticY);
                targetVerts[i] = point.x;
                targetVerts[i + 1] = point.y;
            } else {
                //復原
                targetVerts[i] = originVerts[i];
                targetVerts[i + 1] = originVerts[i + 1];
            }
        }
        invalidate();
    }

    /** * 獲取水波的偏移座標 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 * @param staticX 待偏移頂點的原 x 座標 * @param staticY 待偏移頂點的原 y 座標 * @return 偏移後坐標 */
    private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
        float length = getLength(staticX - originX, staticY - originY);
        //偏移點與原點間的角度
        float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
        //計算偏移距離
        float rate = (length - rippleRadius) / rippleWidth;
        float offset = (float) Math.cos(rate) * 10f;
        float offsetX = offset * (float) Math.cos(angle);
        float offsetY = offset * (float) Math.sin(angle);
        //計算偏移後的座標
        float targetX;
        float targetY;
        if (length < rippleRadius + rippleWidth && length > rippleRadius) {
            //波峯外的偏移座標
            if (staticX > originX) {
                targetX = staticX + offsetX;
            } else {
                targetX = staticX - offsetX;
            }
            if (staticY > originY) {
                targetY = staticY + offsetY;
            } else {
                targetY = staticY - offsetY;
            }
        } else {
            //波峯內的偏移座標
            if (staticX > originY) {
                targetX = staticX - offsetX;
            } else {
                targetX = staticX + offsetX;
            }
            if (staticY > originY) {
                targetY = staticY - offsetY;
            } else {
                targetY = staticY + offsetY;
            }
        }
        return new PointF(targetX, targetY);
    }

    /** * 根據寬高,獲取對角線距離 * * @param width 寬 * @param height 高 * @return 距離 */
    private float getLength(float width, float height) {
        return (float) Math.sqrt(width * width + height * height);
    }

    /** * 獲取 View 的緩存視圖 * * @param view 對應的View * @return 對應View的緩存視圖 */
    private Bitmap getCacheBitmapFromView(View view) {
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache(true);
        final Bitmap drawingCache = view.getDrawingCache();
        Bitmap bitmap;
        if (drawingCache != null) {
            bitmap = Bitmap.createBitmap(drawingCache);
            view.setDrawingCacheEnabled(false);
        } else {
            bitmap = null;
        }
        return bitmap;
    }
}複製代碼

路徑

public void drawPath(@NonNull Path path, @NonNull Paint paint) {
    if (path.isSimplePath && path.rects != null) {
        native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
    } else {
        native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
    }
}複製代碼

drawPath()能夠繪製自定義圖形,圖形的路徑用Path對象來描述。

Path對象能夠描述不少圖形,具體說來:

  • 直線
  • 二次曲線
  • 三次曲線
  • 橢圓
  • 弧形
  • 矩形
  • 圓角矩形

3.2 範圍裁切

Canvas裏的範圍裁切主要有兩類方法:

  • clipReact():按路徑裁切
  • clipPath():按座標裁切

舉例

clipReact

clipPath

//範圍裁切
canvas.save();//保存畫布
canvas.clipRect(200, 200, 900, 900);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢復畫布

canvas.save();//保存畫布
path.addCircle(500, 500, 300, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢復畫布複製代碼

3.3 幾何變換

關於幾何變換有三種實現方式:

  • Canvas:常規幾何變換
  • Matrix:自定義幾何變換
  • Camera:三維變換

Canvas常規幾何變換

Canvas還提供了對象的位置變換的方法,其中包括:

  • translate(float dx, float dy):平移
  • rotate(float degrees):旋轉,能夠設置旋轉圓點,默認在原點位置。
  • scale(float sx, float sy):縮放
  • skew(float sx, float sy):扭曲

舉例

canvas.save();//保存畫布
canvas.skew(0, 0.5f);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢復畫布

canvas.save();//保存畫布
canvas.rotate(45, 750, 750);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢復畫布複製代碼

注:1 爲了避免影響其餘繪製操做,在進行變換以前須要調用canvas.save()保存畫布,變換完成之後再調用canvas.restore()來恢復畫布。
2 Canvas幾何變換的順序是相反的,例如咱們在代碼寫了:canvas.skew(0, 0.5f); canvas.rotate(45, 750, 750); 它的實際調用順序是canvas.rotate(45, 750, 750); -> canvas.skew(0, 0.5f)

Matrix自定義幾何變換

Matrix也實現了Canvas裏的四種常規變換,它的實現流程以下:

  1. 建立 Matrix 對象;
  2. 調用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法來設置幾何變換;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 來把幾何變換應用到 Canvas。

Canvas.concat(matrix):用 Canvas 當前的變換矩陣和 Matrix 相乘,即基於 Canvas 當前的變換,疊加上 Matrix 中的變換。

舉例

//Matrix幾何變換
canvas.save();//保存畫布
matrix.preSkew(0, 0.5f);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢復畫布

canvas.save();//保存畫布
matrix.reset();
matrix.preRotate(45, 750, 750);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢復畫布複製代碼

Matrix除了四種基本的幾何變換,還能夠自定義幾何變換。

  • setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
  • setRectToRect(RectF src, RectF dst, ScaleToFit stf)

這兩個方法都是經過多點的映射的方式來直接設置變換,把指定的點移動到給出的位置,從而發生形變。

舉例

//Matrix幾何變換
canvas.save();//保存畫布
matrix.setPolyToPoly(src, 0, dst, 0, 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, 0, 0, paint1);
canvas.restore();//恢復畫布複製代碼

Camera三維變換

在講解Camera的三維變換以前,咱們須要先理解Camera的座標系系統。

咱們前面說過,Canvas使用的是二維座標系。

而Camera使用的是三維座標系,這裏偷個懶😊,借用凱哥的圖來描述一下。

關於Camera座標系:

  • 首先你要注意x、y、z軸的方向,z軸朝外是負軸。
  • 在z的負軸上有個虛擬相機(就是圖中的哪一個黃點),它就是用來作投影的,setLocation(float x, float y, float z)方法移動的也就是它的位置。
  • x、y、z軸旋轉的方向也在上圖中標出來了。

好比咱們在Camera座標系裏作個X軸方向的旋轉

Camera的三維變換包括:旋轉、平移與移動相機。

旋轉

  • rotateX(deg)
  • rotateY(deg)
  • rotateZ(deg)
  • rotate(x, y, z)

平移

  • translate(float x, float y, float z)

移動相機

  • setLocation(float x, float y, float z)

舉例

旋轉

//Camera三維變換
canvas.save();//保存畫布

camera.save();//保存camera
camera.rotateX(45);
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼

平移

//Camera三維變換
canvas.save();//保存畫布

camera.save();//保存camera
camera.translate(500, 500, 500);
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼

移動相機

//Camera三維變換
canvas.save();//保存畫布

camera.save();//保存camera
camera.setLocation(0, 0, - 1000);//相機往前移動,圖像變小
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼

四 Path

Path描述了繪製路徑,用它能夠完成不少複雜的圖形繪製。

咱們再來看看Path裏的方法。

4.1 添加圖形

例如:addCircle(float x, float y, float radius, Direction dir)

public void addCircle(float x, float y, float radius, Direction dir) {
    isSimplePath = false;
    native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
}複製代碼

該方法的參數含義:

  • float x:圓心x軸座標
  • float y:圓心y軸座標
  • float radius:圓半徑
  • Direction dir:畫圓的路徑的方向,順時針Direction.CN,逆時針Direction.CCN,它們在填充圖形(Paint.Style 爲 FILL 或 FILL_AND_STROKE)且圖形出現相交的時候
    用來判斷填充範圍。

其餘的方法都是這個方法相似。

4.2 畫線(直線或者曲線)

直線

//從當前位置,向目標位置畫一條直線,該方法使用相對於原點的絕對座標
public void lineTo(float x, float y) {
    isSimplePath = false;
    native_lineTo(mNativePath, x, y);
}

//從當前位置,向目標位置畫一條直線,該方法使用相對於當前位置的相對座標
public void rLineTo(float dx, float dy) {
    isSimplePath = false;
    native_rLineTo(mNativePath, dx, dy);
}複製代碼

當前位置:當前位置指的是最後一次盜用Path的方法的終點位置,初始原點爲(0, 0)

這裏說到當前位置,咱們再提一個方法Path.moveTo(float x, float y),它能夠移動當前位置到一個新的位置。

舉例

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
path.lineTo(300, 400);// 由當前位置 (0, 0) 向 (300, 400) 畫一條直線
path.rLineTo(400, 0);// 由當前位置 (300, 400) 向正右方400像素的位置畫一條直線
canvas.drawPath(path, paint);複製代碼

貝塞爾曲線

貝塞爾曲線:貝塞爾曲線是幾何上的一種曲線。它經過起點、控制點和終點來描述一條曲線,主要用於計算機圖形學。簡單來講,貝塞爾曲線就是將任意一條曲線轉換爲精確的數學公式。

在貝塞爾曲線中,有兩類點:

  • 數據點:通常指一條路徑的起點與終點。
  • 控制點:控制點決定了路徑的彎曲軌跡,根據控制點的個數,貝塞爾曲線分爲:一階貝塞爾曲線(0個控制點),二階貝塞爾曲線(1個控制點),三階貝塞爾曲線(2個控制點)等。

一階貝塞爾曲線

B(t)爲時間爲t時的座標,P0爲起點,P1爲終點。

二階貝塞爾曲線

三階貝塞爾曲線

貝塞爾曲線的模擬可使用bezier-curve

咱們再來看看Path類提供的關於貝塞爾曲線的方法。

//二階貝塞爾曲線,絕對座標,(x1, y1)表示控制點,(x2, y2)表示終點
public void quadTo(float x1, float y1, float x2, float y2) {
    isSimplePath = false;
    native_quadTo(mNativePath, x1, y1, x2, y2);
}

//二階貝塞爾曲線,相對座標
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
    isSimplePath = false;
    native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}

//三階貝塞爾曲線,絕對座標,(x1, y1)、(x2, y2)表示控制點,(x3, y3)表示終點
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
    isSimplePath = false;
    native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

//三階貝塞爾曲線,相對座標
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
    isSimplePath = false;
    native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}複製代碼

咱們來用貝塞爾曲線實現一個杯中倒水效果。

舉例

/** * 控制點的X座標不斷左右移動,造成波浪效果。 * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * guoxiaoxingse@163.com * * @author guoxiaoxing * @since 2017/9/11 下午6:11 */
public class WaveView extends View {

    private static final String TAG = "WaveView";

    /** * 波浪從屏幕外開始,在屏幕外結束,這樣效果更真實 */
    private static final float EXTRA_DISTANCE = 200;

    private Path mPath;
    private Paint mPaint;

    /** * 控件寬高 */
    private int mWidth;
    private int mHeight;

    /** * 控制點座標 */
    private float mControlX;
    private float mControlY;

    /** * 波浪峯值 */
    private float mWaveY;

    /** * 是否移動控制點 */
    private boolean mMoveControl = true;

    public WaveView(Context context) {
        super(context);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;

        mControlY = mHeight - mHeight / 8;
        mWaveY = mHeight - mHeight / 32;
    }

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

        //波浪從屏幕外開始,效果更真實
        mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
        //二階貝塞爾曲線
        mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
        //閉合曲線
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaint);

        //mControlX座標在 -EXTRA_DISTANCE ~ mWidth + EXTRA_DISTANCE 範圍內,先自增再自減,左右移動
        //造成波浪效果
        if (mControlX <= -EXTRA_DISTANCE) {
            mMoveControl = true;
        } else if (mControlX >= mWidth + EXTRA_DISTANCE) {
            mMoveControl = false;
        }
        mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;

        //水面不斷上升
        if (mControlY >= 0) {
            mControlY -= 2;
            mWaveY -= 2;
        }

        Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);

        mPath.reset();
        invalidate();
    }


    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setColor(Color.parseColor("#4CAF50"));
    }
}複製代碼

弧線

//畫弧線
public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) {
    isSimplePath = false;
    native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}複製代碼

咱們來看看這個方法的參數:

  • float left, float top, float right, float bottom:左、上、右、下的座標。
  • float startAngle:弧形起始角度,Android座標系x軸正右的方向是0度的位置,順時針爲正角度,逆時針爲負角度。
  • float sweepAngle:弧形劃過的角度。
  • boolean forceMoveTo):是否留下移動的痕跡file

注:能夠發現,這個方法與一樣用來畫弧線的方法Canvas.drawArc()少了個boolean useCenter參數,這是由於arcTo()方法只用來畫弧線。

4.3 輔助設置和計算

public void setFillType(FillType ft) - 設置填充方式

方法用來設置填充方式,填充的方式有四種:

  • WINDING:non-zero winding rule,非零環繞數原則,
  • EVEN_ODD:even-odd rule,奇偶原則
  • INVERSE_WINDING:WINDING的反轉
  • INVERSE_EVEN_ODD:EVEN_ODD的反轉

WINDING:non-zero winding rule,非零環繞數原則,該原則基於全部圖形的繪製都有繪製方向(前面提到的Direction描述的順時針與逆向時針),對於平面上的任意一點,向任意方向射出一條射線,射線遇到每一個順時針
的交點則加1,遇到逆時針的交點則減1,最後的結果若是不爲0,則認爲該點在圖形內部,染色。若是結果爲0,則認爲該點在圖形外部,不染色。

EVEN_ODD:even-odd rule,奇偶原則,對於平面上的任意一點,向任意方向射出一條射線,這條射線與圖形相交(不是相切)的次數爲奇數則說明這個點在圖形內部,則進行染色。若爲偶數則認爲在圖形外部,不進行染色。
這是一中交叉染色的狀況。

相關文章
相關標籤/搜索