本系列文章
- 《VirtualView Android實現(xiàn)詳解(一)—— 文件格式與模板編譯》
- 《VirtualView Android 實現(xiàn)詳解(二)—— 虛擬控件的設(shè)計與實現(xiàn)》
前文介紹了模板的基本格式、虛擬控件與原生控件混合使用的方式。本文重點在把這兩塊內(nèi)容串起來介紹一下,如何實現(xiàn)從模板生成一個運行時的控件,并如何注冊一個自定義控件使用。
相關(guān)開源庫
Android
iOS
名詞解釋
- VirtualView:如果還不清楚,可以閱讀《天貓客戶端組件動態(tài)化的方案——VirtualView 上手體驗》大概了解下;
- 控件:基礎(chǔ)的 UI 單元,像文本、圖片、布局等,通過在 XML 里被引用然后描述一個復雜的界面。
從 XML 到運行時實例
舉個簡單的例子,在 XML 模板里,可能會有這么一塊控件的使用:
<NText
id="1"
text="title"
textSize="12"
textColor="#333333"
layoutWidth="wrap_content"
layoutHeight="wrap_content"
lineSpaceMultiplier="1.1"
lines="2"
flag="flag_event|flag_exposure|flag_clickable"
/>
這在 VirtualView 表示引用一個文本控件(VirtualView 內(nèi)置支持的所有控件見文檔),在《VirtualView Android實現(xiàn)詳解(一)—— 文件格式與模板編譯》里曾講過會將 XML 里的字符串等編譯成整型數(shù)值或者索引來降低解析成本。因此從在 XML 里使用一個控件到運行時渲染它,就要經(jīng)過一系列的轉(zhuǎn)換過程,其中有一半的過程是事先離線執(zhí)行的,另一半的過程才是在客戶端里運行時執(zhí)行。以下這張圖概括了整個流程:

說明一下每個步驟:
- 先編寫 XML 文件,如圖所示引用了一個 NText 控件;
- 在 Config 文件里配置 NText 的標簽名及屬性的映射關(guān)系;像這種內(nèi)置控件都已經(jīng)配置好了,如果是自定義屬性和控件,才要操作自己添加配置;配置文件含義參考這篇文章:VirtualView 工具大更新啦。在這個示例中,NText 標簽被編譯工具編譯成數(shù)字 7,屬性名 id、text、textSize、textColor 都被編譯成一個 hashcode 索引,真正的字符串值會存儲到字符串資源區(qū);屬性值 title 也是被編譯成一個 hashcode 索引,真正的字符串值會存儲到字符串資源區(qū);屬性值 12 被直接編譯成數(shù)字 12; 屬性值 #333333 被編譯成顏色值 -13421773;
- 編譯工具根據(jù) XML 文件和 Config 文件編譯出一份二進制文件,交給客戶端使用;
- 客戶端初始化框架的時候會根據(jù) id 注冊控件,在這個示例中 7 代表了 NativeText 類控件,它就用來實例化 XML 里的 NText 標簽;
- 最后將 XML 里 NText 下的屬性傳給 NativeText 實例進一步用于渲染;
創(chuàng)建控件實例的過程
以創(chuàng)建一個 PicassoImage 為例(雖然內(nèi)置了 VImage 和 NImage 兩個控件,但在實際業(yè)務場景中,還是使用一個自定義的圖片控件比較合適,這樣可以更好利用起結(jié)合圖片庫的內(nèi)存管理、性能優(yōu)化等 feature)。
目標
- 實現(xiàn)一個原生 Image 控件,使用 Picasso 加載圖片
- 支持綁定 url 屬性用來加載網(wǎng)絡圖片
- 支持綁定 degree 屬性用來旋轉(zhuǎn)圖片
1. 定義標簽名及其 id,屬性名及類型
在編譯工具里配置文件里定義:
-
VIEW_ID_PicassoImage=1014,其中PicassoImage就是 XML 里的標簽名,id 值為 1014,這個是自定義的,建議從 1001開始,前 1000 保留給系統(tǒng)使用; -
degree=Float,表示屬性名是 degree ,屬性值按 Float 類型編譯解析; -
url=String,表示屬性名是 url,屬性值按 String 類型編譯,不過未在配置文件里聲明的屬性都是按 String 類型編譯的,所以可以省略;
2. 定義控件的載體 View
取名 PicassoImageView,繼承 ImageView,實現(xiàn) IView 接口,因為 demo 比較簡單,除此之外不做其他邏輯,主要實現(xiàn) IView 的接口調(diào)用對應的系統(tǒng) measure、layout 方法,因為這些方法是不能在外部調(diào)用的,只能通過 IView 的接口封裝一下暴露出去。
3. 定義控件 model
取名 PicassoImage,繼承 ViewBase,在構(gòu)造函數(shù)里實例化 PicassoImageView,并獲取自定義屬性的 id;
public PicassoImage(VafContext context,
ViewCache viewCache) {
super(context, viewCache);
mPicassoImageView = new PicassoImageView(context.getContext());
StringSupport mStringSupport = context.getStringLoader();
// 這里會取加載的模板數(shù)據(jù)里取獲取對應的 id,第一個參數(shù)是屬性名,第二個參數(shù)應當為 false;
urlId = mStringSupport.getStringId("url", false);
degreeId = mStringSupport.getStringId("degree", false);
}
由于 ViewBase 本身也是實現(xiàn) IView 接口的,所以復寫幾個 IView 的 measure、layout 接口,去調(diào)用對應的 PicassoImageView 里的接口。在 VirtualView 體系內(nèi)部,都是通過 ViewBase 對象來驅(qū)動布局計算的,因此必須通過 IView 接口調(diào)用系統(tǒng) View 真正的計算接口。
@Override
public void onComMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mPicassoImageView.onComMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void onComLayout(boolean changed, int l, int t, int r, int b) {
mPicassoImageView.onComLayout(changed, l, t, r, b);
}
@Override
public void comLayout(int l, int t, int r, int b) {
super.comLayout(l, t, r, b);
//這一步很關(guān)鍵,否則 view 不顯示。
mPicassoImageView.comLayout(l, t, r, b);
}
剩下的主要邏輯是處理自定義屬性,有幾個 setAttribute,setRPAttribute 重載的方法,它們用于接收不同類型的屬性值:
-
boolean setAttribute(int key, int value)處理編譯成整數(shù)類型的屬性; -
boolean setAttribute(int key, float value)處理編譯成浮點數(shù)類型的屬性; -
boolean setAttribute(int key, String stringValue)處理編譯成字符串類型的屬性,包括那些本該編譯成整數(shù)或者浮點數(shù)但因為寫了表達式被編譯成字符串類型的; -
boolean setRPAttribute(int key, int value)處理編譯成整數(shù)類型的尺寸屬性,單位是 rp(介紹在此); -
boolean setRPAttribute(int key, float value)處理編譯成浮點數(shù)類型的尺寸屬性,單位是 rp;
基礎(chǔ) ViewBase 里解析處理了大量基礎(chǔ)屬性,所以自定義控件只要處理新增的自定義屬性就行了。以上這些重載方法都有一個 Boolean 返回值,它遵循冒泡邏輯,當你返回 true 的時候,當前層級處理了這個屬性,否則表示當前層級處理不了這個屬性,需要進一步交給子類解析;在本文的示例里,是這么處理的:
@Override
protected boolean setAttribute(int key, float value) {
boolean ret = true;
if (key == degreeId) {
//從模板里直接獲取到旋轉(zhuǎn)角度屬性值
degrees = value;
} else {
ret = super.setAttribute(key, value);
}
return ret;
}
@Override
protected boolean setAttribute(int key, String stringValue) {
boolean ret = true;
if (key == degreeId) {
//從模板里直接獲取到旋轉(zhuǎn)角度屬性值是一個表達式,暫存到 viewCache 里,等傳入數(shù)據(jù)的時候再次解析,然后回調(diào)到上述 setAttribute(int key, float value) 方法里獲取最終值
if (Utils.isEL(stringValue)) {
mViewCache.put(this, degreeId, stringValue, Item.TYPE_FLOAT);
}
} else if (key == urlId) {
//從模板里直接獲取到url屬性值可能是一個表達式,也可能是個直接的 url,如果是表達式,暫存到 viewCache 里,等傳入數(shù)據(jù)的時候再次解析,然后回調(diào)本方法里獲取最終值
if (Utils.isEL(stringValue)) {
mViewCache.put(this, urlId, stringValue, Item.TYPE_STRING);
} else {
url = stringValue;
}
} else {
ret = super.setAttribute(key, stringValue);
}
return ret;
}
最后就是使用這些屬性值,在 onParseValueFinised() 里一次性應用屬性:
@Override
public void onParseValueFinished() {
super.onParseValueFinished();
Picasso.with(mContext.getContext()).load(url).rotate(degrees).into(mPicassoImageView);
}
詳細代碼:PicassoImage.java
4. 注冊控件
通過 ViewManager 里的 ViewFactory 注冊,如下:
sViewManager.getViewFactory().registerBuilder(1014,new PicassoImage.Builder());
5. 使用與運行效果
XML 里這么寫:
<VHLayout
flag="flag_exposure|flag_clickable"
orientation="V"
layoutWidth="match_parent"
layoutHeight="match_parent"
>
<VText
text="Title: Loading Image with Picasso"
textSize="12"
textColor="#333333"
background="#008899"
layoutWidth="match_parent"
layoutHeight="20" />
<PicassoImage
url="${url}"
degree="90"
layoutWidth="match_parent"
layoutHeight="300" />
</VHLayout>
綁定的數(shù)據(jù):
{
"url": "https://gw.alicdn.com/tfs/TB1l0HSgvxNTKJjy0FjXXX6yVXa-200-200.png"
}
運行的結(jié)果:

圖片原圖是這樣的:

可以看到,通過添加自定義的 degree 屬性,并調(diào)用 Picasso 的 ratate 方法,最終加載了圖片,也旋轉(zhuǎn)了圖片,可以根據(jù)此思路繼續(xù)為 PicassImage 添加更多 Picasso 支持的屬性。
本文里用到的例子也上傳到了 demo 里,從上午的源碼鏈接里可以獲取到完整的 demo。
體驗一下
還是那句話,講得再多,不如親自上手體驗一下,可以參考《天貓客戶端組件動態(tài)化的方案——VirtualView 上手體驗》、《提升開發(fā)體驗,預覽 VirtualView》來體驗。