Android自定義View瞎扯

? ? ? ? 為什么選擇自定義View來做起頭?可能是因?yàn)槲易钣谐删透械氖虑槌舜笠粍偨佑|編程時(shí)就進(jìn)入了ACM之外,就是大二第一次用畫布畫出了一整套的游戲并拿得了第一了。所以那就用自定義View來做起筆吧。


前言: 自定義View

? ? ? ? 什么是自定義View呢?說到底就是一款畫布,一直畫筆。我們知道大部分的界面控件,無論是LinearLayout,TextView,ListView最終都是繼承于View,那么我們對(duì)這些控件的種種設(shè)置,最終都會(huì)被不同控件中的不同邏輯進(jìn)行處理,然后繪制到我們的畫布上。一般系統(tǒng)自帶的控件我們是無法操控其繪制的過程,也無法直接修改其內(nèi)部觸發(fā)和處理觸摸的邏輯,那么只要我們繼承重寫這個(gè)控件,拿到其繪制的過程,理論上我們就可以繪制任何的界面。


一. ?初始化一個(gè)自定義View

? ? ? ? 我們從最基礎(chǔ)的View來自定義,當(dāng)然你也可以選擇更高一層的諸如TextView或者LInearLayout來進(jìn)行自定義,那樣你就可以在系統(tǒng)控件的基礎(chǔ)上進(jìn)行自定義修改,但是別忘了在重寫的方法內(nèi)調(diào)用 super.xxxxx() 來告訴父控件執(zhí)行原有邏輯哦,還有要記得調(diào)用的順序,這個(gè)我們?cè)诤竺鏁?huì)接著提到。

? ? ? ? 我們?cè)谝粋€(gè)Activity內(nèi)部添加了一個(gè)自定義View ?—— CustomView,其內(nèi)部未做任何事情,為了和Activity完全區(qū)分開,我們?cè)O(shè)置其背景色為#dddddd。


? ? ? ? 可以看到,我們?cè)贑ustomVIew中未做任何事情,只是實(shí)現(xiàn)了其幾個(gè)關(guān)鍵的構(gòu)造函數(shù),而且各個(gè)構(gòu)造函數(shù)間用?this 鏈起來,這樣就可以在統(tǒng)一的構(gòu)造方法中進(jìn)行初始化處理。具體各構(gòu)造函數(shù)和參數(shù)的意義可以參見該博客: ? ?http://blog.csdn.net/wzy_1988/article/details/49619773

? ? ? ? 運(yùn)行后可以看到以下結(jié)果: ?

? ? ? ? 可以看到帶背景的部分是我們自定義的VIew顯示區(qū)域,目前除了背景外什么都沒有。下面我們開始引入canvas和paint來進(jìn)行繪制。


二、 第一次繪制

? ? 1. 規(guī)則圖形

? ? ? ? 我們首先引入的繪制函數(shù)為 onDraw() 。 Like this :

? ? ? ? 我們看到引入了一個(gè) ?Canvas 和 ?Paint , 其中Canvas為畫布,而Paint為畫筆。畫布可以使用drawxxxx() 來繪制我們想要的東西,無論是線,規(guī)則和不規(guī)則圖形,圖像等等。而畫筆可以為我們實(shí)現(xiàn)各種各樣的效果,我們?cè)诤罄m(xù)會(huì)談到。首先我們使用畫筆在畫布上繪制了一個(gè)矩形,其位置如注釋所訴,那么我們的坐標(biāo)系是什么呢?跟數(shù)學(xué)不一樣,假設(shè)我們認(rèn)為屏幕就是第一象限,那么在第一象限內(nèi),原點(diǎn)為左下角。而在Android中,原點(diǎn)為左上角,所以大家把屏幕放到?第二象限?內(nèi)就可以了,但是沿著Y軸,向下是自增的。?

????????結(jié)果如下:

? ? ? ? 當(dāng)然上述的一些 ?drawxxxx()?方法和?paint?相關(guān)設(shè)置大家都可以在官方文檔中或者直接猜測(cè)字面意思也可以了解個(gè)七七八八,在這里不再贅述。在開發(fā)中要注意這些方法的單位,包括draw的位置和設(shè)置的寬度等信息,是PX還是DP還是SP,還有一些圓的角度問題,是弧度還是角度,這些有時(shí)候會(huì)給我們開發(fā)帶來一些意想不到的bug。

Paint?類的幾個(gè)最常用的方法。

? ? ? ?Paint.setStyle(Style style)設(shè)置繪制模式

? ? ? ?Paint.setColor(int color)設(shè)置顏色

? ? ? Paint.setStrokeWidth(float width)設(shè)置線條寬度

? ? ? Paint.setTextSize(float textSize)設(shè)置文字大小

? ? ?Paint.setAntiAlias(boolean aa)設(shè)置抗鋸齒開關(guān)

? ? ? ? 繪制模式中有?FILL,STROKE,F(xiàn)ILL_AND_STROKE三種模式可選,分別對(duì)應(yīng)了 填充,描邊,填充并描邊 三種模式。我們分別使用三種模式來繪制之前的圖形。


? ? ? ? 設(shè)置顏色可以修改我們畫筆的顏色,從而繪制出不同顏色的圖形,例如這樣:

mPaint.setStyle(Paint.Style.FILL_AND_STROKE);

mPaint.setColor(Color.RED);

canvas.drawRect(10, 10, 100, 100, mPaint);

? ? ? ? ????????效果如下:


? ??????設(shè)置線條寬度 主要用于我們?cè)诶L制描邊時(shí),可以設(shè)置其寬度大小,例如這樣:

mPaint.setStyle(Paint.Style.STROKE);

mPaint.setColor(Color.RED);

mPaint.setStrokeWidth(20);

canvas.drawRect(10, 10, 100, 100, mPaint);

? ? ? ? ????????效果如下,這樣看起來就比之前的描邊線條粗了許多:

? ? ? ? 掌握了以上一些方法,基本上你就可以進(jìn)行繪制一些簡單的圖形圖像了。


????2.不規(guī)則的圖形

? ? ? ? 我們?cè)谌粘i_發(fā)中遇到的圖形肯定不都是規(guī)則的圖形,肯定有一些高大炫酷吊炸天的不規(guī)則圖形讓我們?nèi)ダL制,這時(shí)候我們就可以使用到Path來去處理。Path是什么?顧名思義,就是路線,就好像我們?cè)谧铋_始接觸繪畫程序時(shí)拿到的那根鉛筆一樣,鉛筆的軌跡就是我們想要的軌跡。OK,讓我們?cè)囈幌拢?/p>

path.moveTo(50,50); //移動(dòng)到 50,50 處

path.lineTo(100,100); // 連線到100,100處

path.lineTo(0,100); // 連線到0,100處

path.close(); //封閉圖形,回到第一個(gè)點(diǎn)處

canvas.drawPath(path,mPaint); // 繪制路徑

? ? ? ? 效果如下:


? ? ? ? 如果我們把style改為Fill呢?如果圖形不封閉呢?


Fill的情況下


把 path.close() 改為?path.lineTo(50,70)

? ? ? ? 我們看到,如果類型是FILL,畫筆會(huì)把我們繪制的圖形內(nèi)部填充成我們?cè)O(shè)置的顏色,這點(diǎn)跟我們普通的圖形一致,而當(dāng)我們把?path.close() 去掉, 改為 ?path.lineTo(50,70) ,我們發(fā)現(xiàn)其終點(diǎn)會(huì)自動(dòng)跟起點(diǎn)進(jìn)行連線,然后把封閉的圖形填充成我們?cè)O(shè)置的顏色。

? ? ? ? OK,那我們來個(gè)復(fù)雜的圖形,比如我想繪制一個(gè)簡單的心形 ?? ?,那我們需要先分析一下,心形的組成。為了方便,我們把心簡化掉,把其下半部曲線的部分看為直線,上半部分看為兩個(gè)半圓,這樣我們就有了個(gè)簡單的繪制Path


path.arcTo(new RectF(0,0,200,200),-225,225);

path.arcTo(new RectF(200,0,400,200),-180,225);

path.lineTo(200,300);

canvas.drawPath(path,mPaint);

? ? ? ? 效果如下:


? ? ? ? OK ,看到這里我們就大致了解了自定義VIew繪制的一大部分,繪制規(guī)則圖形和不規(guī)則圖形,這些可以幫助你解決80%的自定義VIew需求了。但是如果你沒有太多的接觸自定義View的理論,可能看起來會(huì)比較吃力,特別是不規(guī)則圖形那一塊。那么下面我們就來進(jìn)一步 的介紹我們上述所使用的一些東西。

二、進(jìn)階內(nèi)容

? 1. 簡單的圖層蒙版

? ? ? ? 在canvas中,我們可以使用drawColor來進(jìn)行簡單的圖層蒙版,比如在上一屆階段最后的心形上,加上一層簡單的綠色半透明模板,要想人生過得去,程序哪能不帶綠?。?!

canvas.drawColor(Color.parseColor("#5500ff00"));

? ? ? ? 于是我們就得到了:


? ? 2. 繪制弧形和扇形

? ? ? ? 我們之前講到了規(guī)則圖形的 ?drawxxxx() ?方法 ,其中基礎(chǔ)的圖形中,扇形和弧形是比較難理解的兩部分。在畫心的過程中我們有用到弧形的半圓,但我們是使用path來繪制的,與直接的draw是有些許區(qū)別。我們先看下draw的函數(shù)

public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

? ? ? ? 我們看到兩個(gè)方法中只有前面幾個(gè)參數(shù)不同,其中一個(gè)是傳入RectF 的變量,一個(gè)是傳入詳細(xì)的上左下右的坐標(biāo)。其實(shí)兩種可以是等價(jià)的,其中RectF也就是表示一個(gè) 可以設(shè)置上左下右 的矩形。這個(gè)矩形就是我們所需要繪制的弧線的所在圓的外切矩形。如圖所示:


綠色矩形表示外切矩形,紅色實(shí)線為繪制弧線,紅色曲線代表其完整的圓

? ? ? ? 所以我們?cè)谶@里可以傳入綠色矩形的坐標(biāo)作為RectF,然后startAngle代表了弧線開始的角度,sweepAngle代表了弧線跨越的角度,useCenter代表了是否與圓心連線閉合。 ?我們把x軸的正方向作為弧度起始為0的角度,順時(shí)針為正,逆時(shí)針為負(fù),比如上圖假設(shè)我們繪制的是一個(gè)120°的弧線,當(dāng)我們順時(shí)針繪制這個(gè)圖形時(shí),我們傳入為?startAngle 為 -180 , sweepAngle為 120。當(dāng)我們逆時(shí)針繪制這個(gè)圖形時(shí),startAngle 為 -60 ,?sweepAngle為 -120。



順時(shí)針繪制


逆時(shí)針繪制

? ? ? ? PS: ?注意以上使用的都是角度,也就是我們常說的0~360°代表一圈。而如果大家用到三角函數(shù)去計(jì)算時(shí),一般傳入傳出的都是弧度,也就是我們所說的0~2π為一圈。所以別忘了換算。


? ? 3. Path的進(jìn)階理解

? ? ? ? 我們?cè)诶L制心形的時(shí)候使用了Path這個(gè)東西,大家可以點(diǎn)出來Path下面有很多的方法,大家可以看到其大多分為了兩組,一組是addXXX一組是XXXTo,那這兩種有什么區(qū)別呢? 顧名思義,一種是add something,這種和上下文是沒有太大關(guān)系的,而一種是 to something,這種一般需要考慮上下文的關(guān)系。當(dāng)然我們這里沒有上下文的概念,所以這里的上下文就是指上一筆的繪制,因?yàn)槲覀兪荘ath,那么就是指上一個(gè)路徑的終點(diǎn)。但是光說肯定很難理解,我們后面用程序去看一下就知道了。當(dāng)然,在此之前,我們先看一些前提的東西。我們看到方法中有一些看起來一樣的方法,只是前面會(huì)不會(huì)有一個(gè)r,比如 moveTo rMoveTo lineTo rLineTo。這里的r 指的是 relative ,也就是我們之前學(xué)習(xí)的相對(duì)布局的相對(duì)二字。那就不難理解了吧,比如我們的畫筆目前是在 (100,100) 位置處,我們?nèi)ギ嬕粭l直線, 我們使用 lineTo(100,200) ?和 ?rLineTo(100,200) 所繪制出來的終點(diǎn)是不一樣的。其中lineTo的終點(diǎn)是100,200,而rLineTo的終點(diǎn)是200,300的位置。

mPaint.setColor(Color.RED);

path.moveTo(100,100);

path.lineTo(100,200);

canvas.drawPath(path,mPaint);

path.reset(); ?// 如果沒有reset的話后面繪制的黑色Path會(huì)把前面的紅色Path給覆蓋掉

mPaint.setColor(Color.BLACK);

path.moveTo(100,100);

path.rLineTo(100,200);

canvas.drawPath(path,mPaint);

紅色為lineTo,黑色為rLineTo

? ? ? ? 然后我們來理解addXXX和xxxTo的區(qū)別。代碼如下:

path.moveTo(100, 100);

path.lineTo(100, 200);

path.addArc(new RectF(0, 0, 100, 100), -120, 120);

path.addCircle(300, 300, 100, Path.Direction.CW);

canvas.drawPath(path, mPaint);

? ? ? ?結(jié)果如圖:

add的結(jié)果

? ? ? ? 我們看到我們用了一個(gè)path,分別繪制了直線,弧線,圓,雖然期間并沒有使用moveTo去移動(dòng)畫筆,但是繪制出來的東西沒有連筆。然后我們使用xxxTo來試一下:

mPaint.setColor(Color.RED);

path.moveTo(100, 100);

path.lineTo(100, 200);

path.arcTo(new RectF(0, 0, 100, 100), -120, 120);

path.arcTo(new RectF(200, 200, 400, 400), 0, 359);

canvas.drawPath(path, mPaint);

? ? ? ? 結(jié)果如下:

xxxTo

? ? ? ? 可以看到,xxxTo 方法繪制出來的圖形有明顯的畫筆移動(dòng)軌跡,直線終點(diǎn)移動(dòng)到弧線起點(diǎn),弧線終點(diǎn)移動(dòng)到圓起點(diǎn)。需要知道的是,在xxxTo的方法中有一個(gè)參數(shù)為forceMoveTo的bool變量,當(dāng)傳入為true時(shí),代表著強(qiáng)制移動(dòng)到繪制位置,此時(shí)就與add相應(yīng)的函數(shù)等價(jià)了。

? ? ? ? 在理解了Path的基礎(chǔ)知識(shí)后,我們介紹一下更高一些的知識(shí),在此之前我們先理解一下什么叫做?“非零環(huán)繞數(shù)原則”? 和 “奇-偶規(guī)則” 。這是圖形學(xué)中比較簡便的區(qū)分點(diǎn)是否在圖像內(nèi)部的算法。其定義如下:

不自交的多邊形:

????????多邊形僅在頂點(diǎn)處連接,而在平面內(nèi)沒有其他公共點(diǎn),此時(shí)可以直觀的劃分內(nèi)-外部分。????

自相交的多邊形:

????????多邊形在平面內(nèi)除頂點(diǎn)外還有其他公共點(diǎn),此時(shí)劃分內(nèi)-外部分需要采用以下的方法。???

????????????????????(1)奇-偶規(guī)則(Odd-even Rule):奇數(shù)表示在多邊形內(nèi),偶數(shù)表示在多邊形外????從任意位置p作一條射線,若與該射線相交的多邊形邊的數(shù)目為奇數(shù),則p是多邊形內(nèi)部點(diǎn),否則是外部點(diǎn)。???

????????????????????(2)非零環(huán)繞數(shù)規(guī)則(Nonzero Winding Number Rule):若環(huán)繞數(shù)為0表示在多邊形內(nèi),非零表示在多邊形外????首先使多邊形的邊變?yōu)槭噶?。將環(huán)繞數(shù)初始化為零。再從任意位置p作一條射線。當(dāng)從p點(diǎn)沿射線方向移動(dòng)時(shí),對(duì)在每個(gè)方向上穿過射線的邊計(jì)數(shù),每當(dāng)多邊形的邊從右到左穿過射線時(shí),環(huán)繞數(shù)加1,從左到右時(shí),環(huán)繞數(shù)減1。處理完多邊形的所有相關(guān)邊之后,若環(huán)繞數(shù)為非零,則p為內(nèi)部點(diǎn),否則,p是外部點(diǎn)。?


P點(diǎn)是在五角星內(nèi)部

? ? ? ? 介紹了這么多,主要用于Path中的setFillType 方法,其參數(shù)有WINDING, EVEN_ODD,?INVERSE_WINDING,INVERSE_EVEN_ODD四個(gè),其中INVERSE_WINDING,INVERSE_EVEN_ODD 分別為WINDING,EVEN_ODD的反選,所以這里只介紹前兩個(gè)WINDING和EVEN_ODD。 其中WINDING就可以簡要的理解為使用奇偶規(guī)則去處理,而EVEN_ODD可以理解為使用非零環(huán)繞數(shù)規(guī)則去處理。具體我們?cè)诖a中查看:

path.addCircle(90, 100, 40, Path.Direction.CW);

path.addCircle(110, 100, 40, Path.Direction.CW);

path.setFillType(Path.FillType.EVEN_ODD); // 1

path.setFillType(Path.FillType.WINDING); // ?2

path.setFillType(Path.FillType.INVERSE_EVEN_ODD); // ?3

path.setFillType(Path.FillType.INVERSE_WINDING); // ?4

canvas.drawPath(path, mPaint);


分別順時(shí)針繪制兩個(gè)圓
1 ? ? ? ? ? ? ? ? ? ? ? ? ?2 ? ? ? ? ? ? ? ? ? ? ? ? ?3 ? ? ? ? ? ? ? ? ? ? ? ? ? ?4?

? ? ? ? 然后我們改變其中一個(gè)圓的繪制方向。

path.addCircle(90, 100, 40, Path.Direction.CW);

path.addCircle(110, 100, 40, Path.Direction.CCW);


不同方向繪制


? ? ? ? 由此我們可以得出,當(dāng)Type 為EVEN_ODD 時(shí),繪制的方向并不影響填充,而當(dāng)Type為 WINDING??時(shí),繪制的方向會(huì)影響到最終的效果。我們對(duì)WINDING進(jìn)行詳細(xì)分析。


對(duì) WINDING進(jìn)行分析
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容