<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                在上一次分享中,我們認識了 Flutter 中最常用也最經典的布局 Widget,即單子容器 Container、多子容器 Row/Column,以及層疊容器 Stack 與 Positioned,也學習了這些不同容器之間的擺放子 Widget 的布局規則,我們可以通過它們,來實現子控件的對齊、嵌套、層疊等,它們也是構建一個界面精美的 App 所必須的布局概念。 在實際開發中,我們會經常遇到一些復雜的 UI 需求,往往無法通過使用 Flutter 的基本 Widget,通過設置其屬性參數來滿足。這個時候,我們就需要針對特定的場景自定義 Widget 了。 在 Flutter 中,自定義 Widget 與其他平臺類似:可以使用基本 Widget 組裝成一個高級別的 Widget,也可以自己在畫板上根據特殊需求來畫界面。 接下來,我會分別與你介紹組合和自繪這兩種自定義 Widget 的方式。 ## 組裝 使用組合的方式自定義 Widget,即通過我們之前介紹的布局方式,擺放項目所需要的基礎 Widget,并在控件內部設置這些基礎 Widget 的樣式,從而組合成一個更高級的控件。 這種方式,對外暴露的接口比較少,減少了上層使用成本,但也因此增強了控件的復用性。在 Flutter 中,**組合的思想始終貫穿在框架設計之中**,這也是 Flutter 提供了如此豐富的控件庫的原因之一。 比如,在新聞類應用中,我們經常需要將新聞 Icon、標題、簡介與日期組合成一個單獨的控件,作為一個整體去響應用戶的點擊事件。面對這類需求,我們可以把現有的 Image、Text 及各類布局,組合成一個更高級的新聞 Item 控件,對外暴露設置 model 和點擊回調的屬性即可。 接下來,我通過一個例子為你說明如何通過組裝去自定義控件。 下圖是 App Store 的升級項 UI 示意圖,圖里的每一項,都有應用 Icon、名稱、更新日期、更新簡介、應用版本、應用大小以及更新 / 打開按鈕。可以看到,這里面的 UI 元素還是相對較多的,現在我們希望將升級項 UI 封裝成一個單獨的控件,節省使用成本,以及后續的維護成本。 :-: ![](https://img.kancloud.cn/01/57/0157ffe54a9cd933795af6c8d7141ecc_1125x2436.png) 圖 1 App Store 升級項 UI 在分析這個升級項 UI 的整體結構之前,我們先定義一個數據結構 UpdateItemModel 來存儲升級信息。在這里為了方便討論,我把所有的屬性都定義為了字符串類型,你在實際使用中可以根據需要將屬性定義得更規范(比如,將 appDate 定義為 DateTime 類型)。 ~~~ class UpdateItemModel { String appIcon;//App 圖標 String appName;//App 名稱 String appSize;//App 大小 String appDate;//App 更新日期 String appDescription;//App 更新文案 String appVersion;//App 版本 // 構造函數語法糖,為屬性賦值 UpdateItemModel({this.appIcon, this.appName, this.appSize, this.appDate, this.appDescription, this.appVersion}); } ~~~ 接下來,我以 Google Map 為例,和你一起分析下這個升級項 UI 的整體結構。 按照子 Widget 的擺放方向,布局方式只有水平和垂直兩種,因此我們也按照這兩個維度對 UI 結構進行拆解。 按垂直方向,我們用綠色的框把這個 UI 拆解為上半部分與下半部分,如圖 2 所示。下半部分比較簡單,是兩個文本控件的組合;上半部分稍微復雜一點,我們先將其包裝為一個水平布局的 Row 控件。 接下來,我們再一起看看水平方向應該如何布局。 :-: ![](https://img.kancloud.cn/dd/62/dd6241906557f49e184a5dc16d33e521_1318x736.png) 圖 2 升級項 UI 整體結構示意圖 我們先把升級項的上半部分拆解成對應的 UI 元素: * 左邊的應用圖標拆解為 Image; * 右邊的按鈕拆解為 FlatButton; * 中間部分是兩個文本在垂直方向上的組合,因此拆解為 Column,Column 內部則是兩個 Text。 拆解示意圖,如下所示: :-: ![](https://img.kancloud.cn/29/c1/29c1762d9c6271049c9149b5ab06bb0d_976x598.png) 圖 3 上半部分 UI 結構示意圖 通過與拆解前的 UI 對比,你就會發現還有 3 個問題待解決:即控件間的邊距如何設置、中間部分的伸縮(截斷)規則又是怎樣、圖片圓角怎么實現。接下來,我們分別來看看。 Image、FlatButton,以及 Column 這三個控件,與父容器 Row 之間存在一定的間距,因此我們還需要在最左邊的 Image 與最右邊的 FlatButton 上包裝一層 Padding,用以留白填充。 另一方面,考慮到需要適配不同尺寸的屏幕,中間部分的兩個文本應該是變長可伸縮的,但也不能無限制地伸縮,太長了還是需要截斷的,否則就會擠壓到右邊按鈕的固定空間了。 因此,我們需要在 Column 的外層用 Expanded 控件再包裝一層,讓 Image 與 FlatButton 之間的空間全留給 Column。不過,通常情況下這兩個文本并不能完全填滿中間的空間,因此我們還需要設置對齊格式,按照垂直方向上居中,水平方向上居左的方式排列。 最后一項需要注意的是,升級項 UI 的 App Icon 是圓角的,但普通的 Image 并不支持圓角。這時,我們可以使用 ClipRRect 控件來解決這個問題。ClipRRect 可以將其子 Widget 按照圓角矩形的規則進行裁剪,所以用 ClipRRect 將 Image 包裝起來,就可以實現圖片圓角的功能了。 下面的代碼,就是控件上半部分的關鍵代碼: ~~~ Widget buildTopRow(BuildContext context) { return Row(//Row 控件,用來水平擺放子 Widget children: <Widget>[ Padding(//Paddng 控件,用來設置 Image 控件邊距 padding: EdgeInsets.all(10),// 上下左右邊距均為 10 child: ClipRRect(// 圓角矩形裁剪控件 borderRadius: BorderRadius.circular(8.0),// 圓角半徑為 8 child: Image.asset(model.appIcon, width: 80,height:80) 圖片控件 // ) ), Expanded(//Expanded 控件,用來拉伸中間區域 child: Column(//Column 控件,用來垂直擺放子 Widget mainAxisAlignment: MainAxisAlignment.center,// 垂直方向居中對齊 crossAxisAlignment: CrossAxisAlignment.start,// 水平方向居左對齊 children: <Widget>[ Text(model.appName,maxLines: 1),//App 名字 Text(model.appDate,maxLines: 1),//App 更新日期 ], ), ), Padding(//Paddng 控件,用來設置 Widget 間邊距 padding: EdgeInsets.fromLTRB(0,0,10,0),// 右邊距為 10,其余均為 0 child: FlatButton(// 按鈕控件 child: Text("OPEN"), onPressed: onPressed,// 點擊回調 ) ) ]); } ~~~ 升級項 UI 的下半部分比較簡單,是兩個文本控件的組合。與上半部分的拆解類似,我們用一個 Column 控件將它倆裝起來,如圖 4 所示: :-: ![](https://img.kancloud.cn/7d/a3/7da3ec3d2068550fc20481ae3457173d_960x326.png) 圖 4 下半部分 UI 結構示意圖 與上半部分類似,這兩個文本與父容器之間存在些間距,因此在 Column 的最外層還需要用 Padding 控件給包裝起來,設置父容器間距。 另一方面,Column 的兩個文本控件間也存在間距,因此我們仍然使用 Padding 控件將下面的文本包裝起來,單獨設置這兩個文本之間的間距。 同樣地,通常情況下這兩個文本并不能完全填滿下部空間,因此我們還需要設置對齊格式,即按照水平方向上居左的方式對齊。 控件下半部分的關鍵代碼如下所示: ~~~ Widget buildBottomRow(BuildContext context) { return Padding(//Padding 控件用來設置整體邊距 padding: EdgeInsets.fromLTRB(15,0,15,0),// 左邊距和右邊距為 15 child: Column(//Column 控件用來垂直擺放子 Widget crossAxisAlignment: CrossAxisAlignment.start,// 水平方向距左對齊 children: <Widget>[ Text(model.appDescription),// 更新文案 Padding(//Padding 控件用來設置邊距 padding: EdgeInsets.fromLTRB(0,10,0,0),// 上邊距為 10 child: Text("${model.appVersion} ? ${model.appSize} MB") ) ] )); } ~~~ 最后,我們將上下兩部分控件通過 Column 包裝起來,這次升級項 UI 定制就完成了: ~~~ class UpdatedItem extends StatelessWidget { final UpdatedItemModel model;// 數據模型 // 構造函數語法糖,用來給 model 賦值 UpdatedItem({Key key,this.model, this.onPressed}) : super(key: key); final VoidCallback onPressed; @override Widget build(BuildContext context) { return Column(// 用 Column 將上下兩部分合體 children: <Widget>[ buildTopRow(context),// 上半部分 buildBottomRow(context)// 下半部分 ]); } Widget buildBottomRow(BuildContext context) {...} Widget buildTopRow(BuildContext context) {...} } ~~~ 試著運行一下,效果如下所示: :-: ![](https://img.kancloud.cn/87/37/8737980f8b42caf33b77197a7a165f66_828x1792.png) 圖 5 升級項 UI 運行示例 搞定! **按照從上到下、從左到右去拆解 UI 的布局結構,把復雜的 UI 分解成各個小 UI 元素,在以組裝的方式去自定義 UI 中非常有用,請一定記住這樣的拆解方法。** ## 自繪 Flutter 提供了非常豐富的控件和布局方式,使得我們可以通過組合去構建一個新的視圖。但對于一些不規則的視圖,用 SDK 提供的現有 Widget 組合可能無法實現,比如餅圖,k 線圖等,這個時候我們就需要自己用畫筆去繪制了。 在原生 iOS 和 Android 開發中,我們可以繼承 UIView/View,在 drawRect/onDraw 方法里進行繪制操作。其實,在 Flutter 中也有類似的方案,那就是 CustomPaint。 **CustomPaint 是用以承接自繪控件的容器,并不負責真正的繪制**。既然是繪制,那就需要用到畫布與畫筆。 在 Flutter 中,畫布是 Canvas,畫筆則是 Paint,而畫成什么樣子,則由定義了繪制邏輯的 CustomPainter 來控制。將 CustomPainter 設置給容器 CustomPaint 的 painter 屬性,我們就完成了一個自繪控件的封裝。 對于畫筆 Paint,我們可以配置它的各種屬性,比如顏色、樣式、粗細等;而畫布 Canvas,則提供了各種常見的繪制方法,比如畫線 drawLine、畫矩形 drawRect、畫點 DrawPoint、畫路徑 drawPath、畫圓 drawCircle、畫圓弧 drawArc 等。 這樣,我們就可以在 CustomPainter 的 paint 方法里,通過 Canvas 與 Paint 的配合,實現定制化的繪制邏輯。 接下來,我們看一個例子。 在下面的代碼中,我們繼承了 CustomPainter,在定義了繪制邏輯的 paint 方法中,通過 Canvas 的 drawArc 方法,用 6 種不同顏色的畫筆依次畫了 6 個 1/6 圓弧,拼成了一張餅圖。最后,我們使用 CustomPaint 容器,將 painter 進行封裝,就完成了餅圖控件 Cake 的定義。 ~~~ class WheelPainter extends CustomPainter { // 設置畫筆顏色 Paint getColoredPaint(Color color) {// 根據顏色返回不同的畫筆 Paint paint = Paint();// 生成畫筆 paint.color = color;// 設置畫筆顏色 return paint; } @override void paint(Canvas canvas, Size size) {// 繪制邏輯 double wheelSize = min(size.width,size.height)/2;// 餅圖的尺寸 double nbElem = 6;// 分成 6 份 double radius = (2 * pi) / nbElem;//1/6 圓 // 包裹餅圖這個圓形的矩形框 Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize), radius: wheelSize); // 每次畫 1/6 個圓弧 canvas.drawArc(boundingRect, 0, radius, true, getColoredPaint(Colors.orange)); canvas.drawArc(boundingRect, radius, radius, true, getColoredPaint(Colors.black38)); canvas.drawArc(boundingRect, radius * 2, radius, true, getColoredPaint(Colors.green)); canvas.drawArc(boundingRect, radius * 3, radius, true, getColoredPaint(Colors.red)); canvas.drawArc(boundingRect, radius * 4, radius, true, getColoredPaint(Colors.blue)); canvas.drawArc(boundingRect, radius * 5, radius, true, getColoredPaint(Colors.pink)); } // 判斷是否需要重繪,這里我們簡單的做下比較即可 @override bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this; } // 將餅圖包裝成一個新的控件 class Cake extends StatelessWidget { @override Widget build(BuildContext context) { return CustomPaint( size: Size(200, 200), painter: WheelPainter(), ); } } ~~~ 試著運行一下,效果如下所示: :-: ![](https://img.kancloud.cn/fb/03/fb03c4222e150a29a41d53a773b94984_828x1792.png) 圖 6 自繪控件示例 可以看到,使用 CustomPainter 進行自繪控件并不算復雜。這里,我建議你試著用畫筆和畫布,去實現更豐富的功能。 **在實現視覺需求上,自繪需要自己親自處理繪制邏輯,而組合則是通過子 Widget 的拼接來實現繪制意圖。**因此從渲染邏輯處理上,自繪方案可以進行深度的渲染定制,從而實現少數通過組合很難實現的需求(比如餅圖、k 線圖)。不過,當視覺效果需要調整時,采用自繪的方案可能需要大量修改繪制代碼,而組合方案則相對簡單:只要布局拆分設計合理,可以通過更換子 Widget 類型來輕松搞定。 ## 總結 在面對一些復雜的 UI 視圖時,Flutter 提供的單一功能類控件往往不能直接滿足我們的需求。于是,我們需要自定義 Widget。Flutter 提供了組裝與自繪兩種自定義 Widget 的方式,來滿足我們對視圖的自定義需求。 以組裝的方式構建 UI,我們需要將目標視圖分解成各個 UI 小元素。通常,我們可以按照從上到下、從左到右的布局順序去對控件層次結構進行拆解,將基本視覺元素封裝到 Column、Row 中。對于有著固定間距的視覺元素,我們可以通過 Padding 對其進行包裝,而對于大小伸縮可變的視覺元素,我們可以通過 Expanded 控件讓其填充父容器的空白區域。 而以自繪的方式定義控件,則需要借助于 CustomPaint 容器,以及最終承接真實繪制邏輯的 CustomPainter。CustomPainter 是繪制邏輯的封裝,在其 paint 方法中,我們可以使用不同類型的畫筆 Paint,利用畫布 Canvas 提供的不同類型的繪制圖形能力,實現控件自定義繪制。 無論是組合還是自繪,在自定義 UI 時,有了目標視圖整體印象后,我們首先需要考慮的事情應該是如何將它化繁為簡,把視覺元素拆解細分,變成自己立即可以著手去實現的一個小控件,然后再思考如何將這些小控件串聯起來。把大問題拆成小問題后,實現目標也逐漸清晰,落地方案就自然浮出水面了。 這其實就和我們學習新知識的過程是一樣的,在對整體知識概念有了初步認知之后,也需要具備將復雜的知識化繁為簡的能力:先理清楚其邏輯脈絡,然后再把不懂的知識拆成小點,最后逐個攻破。 我把今天分享講的兩個例子放到了[GitHub](https://github.com/cyndibaby905/15_custom_ui_demo)上,你可以下載后在工程中實際運行,并對照著今天的知識點進行學習,體會在不同場景下,組合和自繪這兩種自定義 Widget 的具體使用方法。 ## 思考題 最后,我給你留下兩道作業題吧。 * 請擴展 UpdatedItem 控件,使其能自動折疊過長的更新文案,并能支持點擊后展開的功能。 ![](https://img.kancloud.cn/bf/6c/bf6c18f1f391a7f9999e21fdcaeff9bf_1125x1068.png) * 請擴展 Cake 控件,使其能夠根據傳入的 double 數組(最多 10 個元素)中數值的大小,定義餅圖的圓弧大小。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看