自定義 View 梳理:用貝塞爾曲線繪製酷炫輪廓背景

OCN_Yang發表於2017-09-01

ContourView
ContourView

在閒逛一個圖片社群時看到這張圖片,個人對炫酷的東西比較敏感(視覺膚淺),本來想下載一下這個 App 看一下實際效果,可是沒找到。心有不甘,於是分析了一下,感覺實現起來不會太難,自己也花點時間實現了效果,釋出了一個庫。

Github地址:github.com/OCNYang/Con…

今天就藉助這個開源控制元件,來為大家梳理一下自定義 View 的整個流程:

  1. 分析需求、功能,確定實現方法;
  2. 總結所需的引數屬性以滿足可定製性,較明確的屬性歸納為自定義屬性,不適合自定義屬性的(比如傳入資料,物件等)提供方法來設定;
  3. 有時自定義 View 會提供一種或幾種預設及內建的樣式,(這時可以根據內建的樣式種類補充到自定義屬性中),同時分析,使用內建樣式或使用者定製擴充時的流程;
  4. 開始根據分析,按流程依次重寫: 建構函式(獲取自定義屬性,設定畫筆等) --> onMeasure()(測量大小) --> onSizeChanged()(確定大小,一般我們在這裡獲取大小) --> (onLayout()自定義View,因為沒有子控制元件,這一步是不需要的) --> onDraw()(按照需求和根據屬性繪製實際內容) --> 其他
  5. 如果有事件的需求,新增事件相關邏輯。

那麼現在我們就根據上面這個流程一步步來實現 ContourView。

分析

分析圖
分析圖

根據上面的分析,實現的思路大概都有了。那麼我們就開始尋找具體實現方法。
首先,我們選用三階貝塞爾曲線,我們都知道三階曲線的計算公式是:

path.moveTo(start.x, start.y);
path.cubicTo(control1.x, control1.y, control2.x,control2.y, end.x, end.y);複製程式碼

三階貝塞爾曲線
三階貝塞爾曲線

也就是說繪製一段曲線,我們需要知道兩個錨點的座標以及兩個控制點的座標,為了保證曲線的彎曲度能夠達到理想的狀態,控制點的座標也不能是隨意取的,這就要求我們必須通過一種計算方法合理的得出控制點的座標。Google 了一下,發現先驅們已經找到了很多種方法供我們選擇。

最終經過對比我們選用了這樣一種方法:

控制點計算方法
控制點計算方法

這種方法大概的形式如上圖,利用錨點集合,連續的4個錨點座標Pi-1、Pi、Pi+1、Pi+2,通過具體公式來計算出中間兩個錨點之間曲線的兩個控制點座標。

詳細的計算方法介紹請看 ContourView 的 WiKi:
Bézier-求貝塞爾曲線控制點

歸納自定義屬性

通過上面的分析,其實我們大概能總結出需要自定義的屬性有哪些了。這裡不著急,我們先總結一下自定義屬性相關的內容和步驟?

1. 建立自定義屬性檔案
在 res/values/ 下新建 attrs.xml 檔案(預設新建專案沒有這個檔案)。檔案內容類似如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="custom_color" format="color"/>

    <declare-styleable name="ContourView">
        <attr name="shader_color" format="color"/>
        <attr name="smoothness" format="float"/>
    </declare-styleable>
</resources>複製程式碼

其中 attr 和 declare-styleable 節點分別代表的意思如下:

attr: 定義了一個屬性,屬性名為 custom_color 這個是可以隨意起的,但是要注意不要和其他控制元件所衝突, format 所定義的是屬性的格式,其中格式又分為好多種,下面會細說,這裡定義的是顏色 color。

declare-styleable:定義了一個屬性組,在裡面我們可以單獨寫 attr 屬性,也可以引用直接在 resources 下定義的 attr,其中的區別就是引用的不用寫 format。

需要注意的是,attr 並不依賴與 declare-styleable,declare-styleable 只是方便了 attr 的使用,使屬性的使用更加明確。兩者在程式碼中的獲取方式並不相同,下面會細說。

在實際開發中,我們一般是採用 declare-styleable 方式,直接定義一組自己所編寫的自定義控制元件需要用到的屬性。

2. 自定義屬性的可以設定哪些屬性

我們根據需要可以設定的自定義屬性的格式一共有一下幾種:

format="格式" 說明 app:myattr="使用值"
reference 參考某一資源ID "@drawable/圖片ID"
color 顏色值 "#FFFFFFFF" or "@color/顏色ID"
boolean 布林值 "true" or "false"
dimension 尺寸值 "0dp"
float 浮點型 "1.2"
integer 整型值 "10"
fraction 百分數值 "50%"
string 字串 "OCN.Yang"
enum 列舉值(詳見下) "自定義型別名稱"
flag 位或運算 "center | bottom"

附:
enum 列舉型定義:

<attr name="handsomeBoy">
    <enum name="OCNYang" value="0x01"/>
    <enum name="TFBOYS" value="0x10"/>
</attr>複製程式碼

enum 使用:

app:handsomeBoy="OCNYang"複製程式碼

flag 定義:

<attr name="gravity">
    <flag name="top" value="0"/>
    <flag name="center" value="1"/>
    <flag name="bottom" value="2"/>
</attr>複製程式碼

flag 使用:

app:gravity="center|bottom"複製程式碼

混搭使用

<attr name="background" format="reference|color"/>複製程式碼

這樣,你傳入資源ID或顏色值都是可以的了。

3. 獲取自定義屬性

那怎麼獲取這些自定義的屬性呢,只需要在自定義 View 的構造方法(兩個引數或兩個以上的引數)裡通過一下方式就能獲取到了:

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

    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ContourView);
    //注意:獲取時自定義的屬性名有變動,例如:定義名:contour_style -> 獲取名:ContourView_contour_style(即:自定義屬性組名_屬性名)
    mStyle = typedArray.getInt(R.styleable.ContourView_contour_style, STYLE_SAND);
}複製程式碼

當然獲取時,不同格式的屬性需要通過 TypedArray 對應的不同的方法獲取,那 TypedArray 都有哪些獲取方法呢?如下圖:

TypedArray 的方法有哪些
TypedArray 的方法有哪些

通過方法名稱,相信你能很輕易的知道,需要哪個對應方法獲取了。

如果你想更詳細的瞭解每個方法的詳細介紹,可以點選下面連結檢視:
developer.android.com/reference/a…
另外,比較特殊的 enum 的獲取方法:
由於 enum 的 value 值只能設定 int 型,所以,獲取enum的方式是 getInt()

好了,關於自定義屬性的介紹大概就是這麼多內容了,那麼回到原題,我們的 ContourView 需要哪幾種 自定義屬性呢?其實通過分析模組中我們就基本知道我們需要的屬性有哪些了:

  • 內建輪廓樣式: enum 型別,內建多少個 enum 就有多少型別;
  • 繪製顏色:純色繪製時,我們需要一個顏色值,Color 屬性
  • Shader 相關:
    1. 採用哪種 Shader,enum 型別,有RadialGradient、SweepGradient、LinearGradient;
    2. Shader 的顏色,Color 型別,需要兩個一個startColor,一個endColor;
    3. Shader 填充的控制,enum 型別,我們提供幾種填充的方向,比如左上角到右下角,從上到下,然後我們再通過這個方向和傳入的秒點集來動態計算起點和終點的座標

具體如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ContourView">
        <attr name="shader_mode">
            <enum name="RadialGradient" value="0x01"/>
            <enum name="SweepGradient" value="0x02"/>
            <enum name="LinearGradient" value="0x03"/>
        </attr>
        <attr name="shader_startcolor" format="color"/>
        <attr name="shader_endcolor" format="color"/>
        <attr name="shader_style">
            <enum name="LeftToBottom" value="0x00"/>
            <enum name="RightToBottom" value="0x11"/>
            <enum name="TopToBottom" value="0x12"/>
            <enum name="Center" value="0x13"/>
        </attr>
        <attr name="contour_style">
            <enum name="Beach" value="0x23"/>
            <enum name="Ripples" value="0x22"/>
            <enum name="Clouds" value="0x21"/>
            <enum name="Sand" value="0x00"/>
            <enum name="Shell" value="0x25"/>
        </attr>
        <attr name="shader_color" format="color"/>
        <!--彎曲係數,在通過貝塞爾曲線繪製曲線時,來控制彎曲度-->
        <attr name="smoothness" format="float"/>
    </declare-styleable>
</resources>複製程式碼

內建樣式

既然自定義 View,那我們一定會為它提供一種或幾種內建好的樣式呀。這樣別人在偷懶不想自己定製樣式時,可以也有不錯的顯示效果呀!
通過上面知道,ContourView 的輪廓樣式主要是通過給出的錨點集控制的,所有的錨點圍成的閉合曲線就是輪廓的大概樣式了。
所以,這裡我們想內建幾種樣式,就等於內建幾個錨點集就行了,這裡的我們內建的錨點座標為了使得不同大小顯示效果相同,我們先在 onSizeChanged() 獲得了 View 的寬高,然後根據寬高按照百分比來設定座標。

設定的內建輪廓有以下幾種(醜爆了),只是輪廓,顏色是自己設定的:

樣式(contuor_style) 效果
Sand(預設)
sand
sand
Clouds
clouds
clouds
Beach
beach
beach
Ripples
ripples
ripples
Shell
shell
shell

重寫各方法

關於自定義 View 重寫各方法的介紹,網上已經有太多太多,這裡就不再囉嗦了。

這裡推薦一個關於自定義 View 尤其關於繪製方面講解特別詳細的系列部落格:
github.com/GcsSloop/An…
另外厚臉皮的放上一篇自己的關於講解“自定義組合控制元件”的部落格地址:
www.jianshu.com/p/4bbc96721…

我們知道,在自定義 View 時,必須要有建構函式的,對於4個建構函式,有時可能大家不確定到底該重寫哪個,也不知道每個建構函式有什麼區別,這裡對常用的做法做下說明。

//在程式碼中直接 new 一個 Custom View 例項時,會呼叫第一個建構函式.這個沒有任何爭議.
public View(Context context);  
//在 xml 佈局檔案中使用自定義 View 時,會呼叫第二個建構函式.這個也沒有爭議.
public View(Context context, AttributeSet attrs);  
//關於這個建構函式的呼叫,網上真是眾說紛紜,我也不說哪種說法正確,下面提供詳解
public View(Context context, AttributeSet attrs, int defStyle);
//4個引數的建構函式這裡不做考慮複製程式碼

關於內部這4個建構函式是怎麼呼叫的,這裡直接放原始碼圖片,自己一目瞭然:

View 原始碼
View 原始碼

大家在自定義 View 時,如果沒有特別的需求,只要重寫前兩個建構函式就可以了,我習慣性的寫成下面的形式:

public class MyView extends View {

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

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

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化畫筆,做一些屬性的預設賦值等;
        //獲取自定義的屬性等;
    }
}複製程式碼

那,說了這麼多還是沒有提第3個引數到底是幹什麼的有什麼用呀,這裡我就不再為大家詳細講解了,這裡找到了一片文章,講解了第3個引數在什麼時候怎麼使用,大家可以看一下:

www.cnblogs.com/angeldevil/…

迴歸到 ContourView,其實 ContourView 內部很簡單,只對 onDraw() 進行了重寫,畢竟 ContourView 的主要部分就是繪製。繪製的邏輯,就是遍歷錨點集,然後利用上面 WiKi 裡提到的公式求出各段曲線的控制點,然後用三階貝塞爾曲線畫出路徑。當遍歷完錨點集時,閉合曲線的輪廓基本上就得到了,然後就用Shader對路徑進行繪製就行。

好了,本次的梳理內容就到這了,感興趣的可以檢視 ContourView 的原始碼進行分析,同時 ContourView 的這種背景效果還是不錯的,需要的時候大家真的可以用到呢!

ContourView GitHub:github.com/OCNYang/Con…

如果大家想看一些高階的自定義 View 的例子可以檢視上次開源的 App 的天氣模組,其中的天氣頁面以及天氣折線圖等等控制元件都是通過自定義 ViewGroup 或自定義 View 實現的。地址是:
Qbox Github:github.com/OCNYang/QBo…

相關文章