? ? ? ? 為什么選擇自定義View來做起頭弃舒?可能是因?yàn)槲易钣谐删透械氖虑槌舜笠粍偨佑|編程時就進(jìn)入了ACM之外滩字,就是大二第一次用畫布畫出了一整套的游戲并拿得了第一了吨凑。所以那就用自定義View來做起筆吧伴网。
前言: 自定義View
? ? ? ? 什么是自定義View呢逃沿?說到底就是一款畫布透揣,一直畫筆济炎。我們知道大部分的界面控件,無論是LinearLayout辐真,TextView须尚,ListView最終都是繼承于View,那么我們對這些控件的種種設(shè)置拆祈,最終都會被不同控件中的不同邏輯進(jìn)行處理恨闪,然后繪制到我們的畫布上。一般系統(tǒng)自帶的控件我們是無法操控其繪制的過程放坏,也無法直接修改其內(nèi)部觸發(fā)和處理觸摸的邏輯咙咽,那么只要我們繼承重寫這個控件,拿到其繪制的過程淤年,理論上我們就可以繪制任何的界面钧敞。
一. ?初始化一個自定義View
? ? ? ? 我們從最基礎(chǔ)的View來自定義,當(dāng)然你也可以選擇更高一層的諸如TextView或者LInearLayout來進(jìn)行自定義麸粮,那樣你就可以在系統(tǒng)控件的基礎(chǔ)上進(jìn)行自定義修改溉苛,但是別忘了在重寫的方法內(nèi)調(diào)用 super.xxxxx() 來告訴父控件執(zhí)行原有邏輯哦,還有要記得調(diào)用的順序弄诲,這個我們在后面會接著提到愚战。
? ? ? ? 我們在一個Activity內(nèi)部添加了一個自定義View ?—— CustomView娇唯,其內(nèi)部未做任何事情,為了和Activity完全區(qū)分開寂玲,我們設(shè)置其背景色為#dddddd塔插。
? ? ? ? 可以看到,我們在CustomVIew中未做任何事情拓哟,只是實(shí)現(xiàn)了其幾個關(guān)鍵的構(gòu)造函數(shù)想许,而且各個構(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 :
? ? ? ? 我們看到引入了一個 ?Canvas 和 ?Paint 较雕, 其中Canvas為畫布碉哑,而Paint為畫筆。畫布可以使用drawxxxx() 來繪制我們想要的東西亮蒋,無論是線扣典,規(guī)則和不規(guī)則圖形,圖像等等慎玖。而畫筆可以為我們實(shí)現(xiàn)各種各樣的效果贮尖,我們在后續(xù)會談到。首先我們使用畫筆在畫布上繪制了一個矩形趁怔,其位置如注釋所訴湿硝,那么我們的坐標(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è)置大家都可以在官方文檔中或者直接猜測字面意思也可以了解個七七八八倚聚,在這里不再贅述线衫。在開發(fā)中要注意這些方法的單位,包括draw的位置和設(shè)置的寬度等信息惑折,是PX還是DP還是SP授账,還有一些圓的角度問題枯跑,是弧度還是角度,這些有時候會給我們開發(fā)帶來一些意想不到的bug矗积。
Paint?類的幾個最常用的方法全肮。
? ? ? ?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三種模式可選,分別對應(yīng)了 填充休建,描邊乍恐,填充并描邊 三種模式。我們分別使用三種模式來繪制之前的圖形测砂。
? ? ? ? 設(shè)置顏色可以修改我們畫筆的顏色茵烈,從而繪制出不同顏色的圖形,例如這樣:
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(Color.RED);
canvas.drawRect(10, 10, 100, 100, mPaint);
? ? ? ? ????????效果如下:
? ??????設(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ī)則的圖形
? ? ? ? 我們在日常開發(fā)中遇到的圖形肯定不都是規(guī)則的圖形,肯定有一些高大炫酷吊炸天的不規(guī)則圖形讓我們?nèi)ダL制纵东,這時候我們就可以使用到Path來去處理粘招。Path是什么?顧名思義,就是路線偎球,就好像我們在最開始接觸繪畫程序時拿到的那根鉛筆一樣洒扎,鉛筆的軌跡就是我們想要的軌跡。OK衰絮,讓我們試一下:
path.moveTo(50,50); //移動到 50,50 處
path.lineTo(100,100); // 連線到100,100處
path.lineTo(0,100); // 連線到0,100處
path.close(); //封閉圖形袍冷,回到第一個點(diǎn)處
canvas.drawPath(path,mPaint); // 繪制路徑
? ? ? ? 效果如下:
? ? ? ? 如果我們把style改為Fill呢?如果圖形不封閉呢猫牡?
? ? ? ? 我們看到胡诗,如果類型是FILL,畫筆會把我們繪制的圖形內(nèi)部填充成我們設(shè)置的顏色镊掖,這點(diǎn)跟我們普通的圖形一致乃戈,而當(dāng)我們把?path.close() 去掉, 改為 ?path.lineTo(50,70) 亩进,我們發(fā)現(xiàn)其終點(diǎn)會自動跟起點(diǎn)進(jìn)行連線症虑,然后把封閉的圖形填充成我們設(shè)置的顏色。
? ? ? ? OK归薛,那我們來個復(fù)雜的圖形谍憔,比如我想繪制一個簡單的心形 ?? ?匪蝙,那我們需要先分析一下,心形的組成习贫。為了方便逛球,我們把心簡化掉,把其下半部曲線的部分看為直線苫昌,上半部分看為兩個半圓颤绕,這樣我們就有了個簡單的繪制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的理論袜硫,可能看起來會比較吃力氯葬,特別是不規(guī)則圖形那一塊。那么下面我們就來進(jìn)一步 的介紹我們上述所使用的一些東西婉陷。
二帚称、進(jìn)階內(nèi)容
? 1. 簡單的圖層蒙版
? ? ? ? 在canvas中,我們可以使用drawColor來進(jìn)行簡單的圖層蒙版秽澳,比如在上一屆階段最后的心形上闯睹,加上一層簡單的綠色半透明模板,要想人生過得去肝集,程序哪能不帶綠U鞍印!杏瞻!
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)
? ? ? ? 我們看到兩個方法中只有前面幾個參數(shù)不同斩披,其中一個是傳入RectF 的變量,一個是傳入詳細(xì)的上左下右的坐標(biāo)讹俊。其實(shí)兩種可以是等價的垦沉,其中RectF也就是表示一個 可以設(shè)置上左下右 的矩形。這個矩形就是我們所需要繪制的弧線的所在圓的外切矩形仍劈。如圖所示:
? ? ? ? 所以我們在這里可以傳入綠色矩形的坐標(biāo)作為RectF贩疙,然后startAngle代表了弧線開始的角度讹弯,sweepAngle代表了弧線跨越的角度况既,useCenter代表了是否與圓心連線閉合。 ?我們把x軸的正方向作為弧度起始為0的角度组民,順時針為正棒仍,逆時針為負(fù),比如上圖假設(shè)我們繪制的是一個120°的弧線臭胜,當(dāng)我們順時針繪制這個圖形時莫其,我們傳入為?startAngle 為 -180 , sweepAngle為 120耸三。當(dāng)我們逆時針繪制這個圖形時榜配,startAngle 為 -60 ,?sweepAngle為 -120吕晌。
? ? ? ? PS: ?注意以上使用的都是角度,也就是我們常說的0~360°代表一圈临燃。而如果大家用到三角函數(shù)去計(jì)算時睛驳,一般傳入傳出的都是弧度,也就是我們所說的0~2π為一圈膜廊。所以別忘了換算乏沸。
? ? 3. Path的進(jìn)階理解
? ? ? ? 我們在繪制心形的時候使用了Path這個東西,大家可以點(diǎn)出來Path下面有很多的方法爪瓜,大家可以看到其大多分為了兩組蹬跃,一組是addXXX一組是XXXTo,那這兩種有什么區(qū)別呢铆铆? 顧名思義蝶缀,一種是add something,這種和上下文是沒有太大關(guān)系的薄货,而一種是 to something翁都,這種一般需要考慮上下文的關(guān)系。當(dāng)然我們這里沒有上下文的概念谅猾,所以這里的上下文就是指上一筆的繪制柄慰,因?yàn)槲覀兪荘ath,那么就是指上一個路徑的終點(diǎn)税娜。但是光說肯定很難理解坐搔,我們后面用程序去看一下就知道了辈末。當(dāng)然纽哥,在此之前,我們先看一些前提的東西献雅。我們看到方法中有一些看起來一樣的方法谤绳,只是前面會不會有一個r占锯,比如 moveTo 和 rMoveTo 袒哥,lineTo 和 rLineTo。這里的r 指的是 relative 消略,也就是我們之前學(xué)習(xí)的相對布局的相對二字堡称。那就不難理解了吧,比如我們的畫筆目前是在 (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會把前面的紅色Path給覆蓋掉
mPaint.setColor(Color.BLACK);
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mPaint);
? ? ? ? 然后我們來理解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é)果如圖:
? ? ? ? 我們看到我們用了一個path巫俺,分別繪制了直線,弧線肿男,圓介汹,雖然期間并沒有使用moveTo去移動畫筆,但是繪制出來的東西沒有連筆舶沛。然后我們使用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 方法繪制出來的圖形有明顯的畫筆移動軌跡,直線終點(diǎn)移動到弧線起點(diǎn)如庭,弧線終點(diǎn)移動到圓起點(diǎn)叹卷。需要知道的是,在xxxTo的方法中有一個參數(shù)為forceMoveTo的bool變量坪它,當(dāng)傳入為true時骤竹,代表著強(qiáng)制移動到繪制位置,此時就與add相應(yīng)的函數(shù)等價了哟楷。
? ? ? ? 在理解了Path的基礎(chǔ)知識后瘤载,我們介紹一下更高一些的知識,在此之前我們先理解一下什么叫做?“非零環(huán)繞數(shù)原則”? 和 “奇-偶規(guī)則” 卖擅。這是圖形學(xué)中比較簡便的區(qū)分點(diǎn)是否在圖像內(nèi)部的算法鸣奔。其定義如下:
不自交的多邊形:
????????多邊形僅在頂點(diǎn)處連接,而在平面內(nèi)沒有其他公共點(diǎn)惩阶,此時可以直觀的劃分內(nèi)-外部分挎狸。????
自相交的多邊形:
????????多邊形在平面內(nèi)除頂點(diǎn)外還有其他公共點(diǎn),此時劃分內(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)槭噶客亮瘛h(huán)繞數(shù)初始化為零诀姚。再從任意位置p作一條射線。當(dāng)從p點(diǎn)沿射線方向移動時玷禽,對在每個方向上穿過射線的邊計(jì)數(shù)赫段,每當(dāng)多邊形的邊從右到左穿過射線時,環(huán)繞數(shù)加1矢赁,從左到右時糯笙,環(huán)繞數(shù)減1。處理完多邊形的所有相關(guān)邊之后撩银,若環(huán)繞數(shù)為非零给涕,則p為內(nèi)部點(diǎn),否則额获,p是外部點(diǎn)稠炬。?
? ? ? ? 介紹了這么多,主要用于Path中的setFillType 方法咪啡,其參數(shù)有WINDING, EVEN_ODD暮屡,?INVERSE_WINDING撤摸,INVERSE_EVEN_ODD四個,其中INVERSE_WINDING褒纲,INVERSE_EVEN_ODD 分別為WINDING准夷,EVEN_ODD的反選,所以這里只介紹前兩個WINDING和EVEN_ODD莺掠。 其中WINDING就可以簡要的理解為使用奇偶規(guī)則去處理衫嵌,而EVEN_ODD可以理解為使用非零環(huán)繞數(shù)規(guī)則去處理。具體我們在代碼中查看:
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);
? ? ? ? 然后我們改變其中一個圓的繪制方向彻秆。
path.addCircle(90, 100, 40, Path.Direction.CW);
path.addCircle(110, 100, 40, Path.Direction.CCW);
? ? ? ? 由此我們可以得出楔绞,當(dāng)Type 為EVEN_ODD 時,繪制的方向并不影響填充唇兑,而當(dāng)Type為 WINDING??時酒朵,繪制的方向會影響到最終的效果。我們對WINDING進(jìn)行詳細(xì)分析扎附。