自定義Widget的實現和使用方法

2021-02-19 Flutter編程指南
前言

前面已經通過三個篇幅向大家介紹了Flutter原生提供的常用Widget和其使用方法,實際開發中我們會遇到一些特殊的需求或者規範一些常用的Widget作為基礎UI組件來使用,這時我們就需要根據需求自定義Widget了。Flutter中的自定義Widget和安卓、iOS原生平臺類似,可以使用現有Widget進行組合,也可以自己根據需求來繪製,下面分別對兩種自定義Widget的實現和使用方法做詳細介紹。

現有Widget組合自定義Widget

現有Widget組合即是根據前面所介紹的基礎Widget根據需求來組合成一個通用的Widget,這樣在使用過程中避免設置過多的屬性,且增強其復用性。 比如,在實際開發中,我們經常會碰到一個Icon和一個標題組合而成的UI,且這個組合的區塊可以處理點擊事件,那麼我們就可以根據現有Icon和Text widget組合成通用的類似UI組件。

首先創建一個單獨的dart文件來實現該自定義Widget,比如命名為custom_combined_widget.dart。

import 'package:flutter/material.dart';class CustomCombinedWidget extends StatefulWidget {  final IconData iconData;  final String title;  final GestureTapCallback onTap;  const CustomCombinedWidget({Key key, this.iconData, this.title, this.onTap}): super(key: key);  @override  State<StatefulWidget> createState() {    // TODO: implement createState    return CustomCombinedWidgetState();  }}class CustomCombinedWidgetState extends State<CustomCombinedWidget> {  @override  Widget build(BuildContext context) {    // TODO: implement build    return GestureDetector(      onTap: this.widget.onTap,      child: Column(        children: <Widget>[          Icon(this.widget.iconData, size: 45.0,),          Text(this.widget.title == null ? "" : this.widget.title, style: TextStyle(fontSize: 14.0, color: Colors.black),),        ],      ),    );  }}

上述代碼中我們自定義了一個CustomCombinedWidget,這裡面Icon的大小和標題的文本大小、顏色都是定好的,所以在使用時無法改變這些值,如果想在使用過程中改變這些值則需要從外部傳值進來,比如標題的文本內容、icon和該Widget點擊事件回調就是通過外部使用者傳入的。

以上自定義Widget的使用方法如下:

//首先導入自定義Widget所在文件import 'package:demo_module/custom_combined_widget.dart';//在頁面Widget中使用class HomePageState extends State<HomePage> {  String tips = '這裡是提示';  @override  Widget build(BuildContext context) {    // TODO: implement build    return Scaffold(      appBar: AppBar(        title: Text('自定義組合Widget'),      ),      body: Container(        child: buildCombinedWidget(),      ),    );  }  Widget buildCombinedWidget() {    return Center(      child: Column(        children: <Widget>[          Row(            mainAxisAlignment: MainAxisAlignment.spaceAround,            children: <Widget>[              CustomCombinedWidget( //使用自定義Widget                iconData: Icons.home,                title: '首頁',                onTap: () {                  setState(() {                    this.tips = '點擊了首頁';                  });                },              ),              CustomCombinedWidget(                iconData: Icons.list,                title: '產品',                onTap: () {                  setState(() {                    this.tips = '點擊了產品';                  });                },              ),              CustomCombinedWidget(                iconData: Icons.more_horiz,                title: '更多',                onTap: () {                  setState(() {                    this.tips = '點擊了更多';                  });                },              ),            ],          ),          Padding(            padding: EdgeInsets.only(top: 50),            child: Text(              this.tips,              style: TextStyle(fontSize: 20, color: Colors.blue),            ),          ),        ],      ),    );  }}

以上代碼模擬器運行效果如下:

通過CustomPainter繪製自定義Widget

自定義繪製的Widget需要我們繼承官方提供的CustomPainter抽象類,重寫paint方法來實現,我們可以在paint方法中根據需求來繪製各種UI圖形,最後根據該自定義CustomerPainter類創建一個painter對象作為系統提供的CustomPaint的painter屬性值來實現自定義Widget。下面根據一個常用的柱狀圖的實現來學習CustomPainter的用法。

首先我們創建一個自定義Widget對應的dart文件my_custom_painter.dart

import 'dart:ui';import 'dart:ui' as ui show TextStyle;import 'package:flutter/material.dart';class BaseData {  String name;  int num;  BaseData({this.name, this.num});}class MyCustomPainter extends CustomPainter {  //繪製區域寬度  final int width = 300;  //繪製區域高度  final int height = 350;  //坐標原點  final Offset origin = const Offset(50.0, 280.0);  //縱坐標頂點  final Offset vertexVer = const Offset(50.0, 20.0);  //橫坐標頂點  final Offset vertexHor = const Offset(300.0, 280.0);  //縱坐標刻度間隔  final int scaleInterval = 1000;  const MyCustomPainter(this.data);  final List<BaseData> data;  //根據文本內容和字體大小等構建一段文本  Paragraph buildParagraph(String text, double textSize, double constWidth) {    ParagraphBuilder builder = ParagraphBuilder(      ParagraphStyle(        textAlign: TextAlign.right,        fontSize: textSize,        fontWeight: FontWeight.normal,      ),    );    builder.pushStyle(ui.TextStyle(color: Colors.black));    builder.addText(text);    ParagraphConstraints constraints = ParagraphConstraints(width: constWidth);    return builder.build()..layout(constraints);  }  @override  void paint(Canvas canvas, Size size) {    // TODO: implement paint    var paint = Paint()      ..color = Colors.black      ..strokeWidth = 2.0      ..strokeCap = StrokeCap.square;    //繪製縱坐標軸線    canvas.drawLine(origin, vertexVer, paint);    canvas.drawLine(        vertexVer, Offset(vertexVer.dx - 5, vertexVer.dy + 10), paint);    canvas.drawLine(        vertexVer, Offset(vertexVer.dx + 5, vertexVer.dy + 10), paint);    canvas.drawParagraph(buildParagraph('印刷量', 14, origin.dx-5),        Offset(0, vertexVer.dy-8));    //繪製橫坐標軸線    canvas.drawLine(origin, vertexHor, paint);    canvas.drawLine(        vertexHor, Offset(vertexHor.dx - 10, vertexHor.dy - 5), paint);    canvas.drawLine(        vertexHor, Offset(vertexHor.dx - 10, vertexHor.dy + 5), paint);    canvas.drawParagraph(buildParagraph('書籍名', 14, origin.dx-5),        Offset(vertexHor.dx, origin.dy+8));    //繪製縱坐標刻度    //實際最大值    double realMaxY = origin.dy - vertexVer.dy - 20;    //刻度間隔實際值    double scaleInte = realMaxY / 5;    for (int i = 0; i < 5; i++) {      canvas.drawLine(Offset(origin.dx, origin.dy - (i + 1) * scaleInte),          Offset(origin.dx + 5, origin.dy - (i + 1) * scaleInte), paint);      canvas.drawParagraph(buildParagraph(((i+1)*scaleInterval).toString(), 12, origin.dx-5),          Offset(0, origin.dy - (i + 1) * scaleInte - 8.0));    }    if (data == null || data.length == 0) {      return;    }    //計算縱坐標上的刻度    int size = data.length;    //柱狀圖間隔值    double horiScalInte = (vertexHor.dx - origin.dx - 20) / size;    //柱狀圖寬    double chartWidth = 5;    for (int i = 0; i < size; i++) {      BaseData curData = data[i];      double valueY = curData.num * scaleInte / scaleInterval;      canvas.drawRect(Rect.fromLTWH(origin.dx + (i+1)*horiScalInte, origin.dy - valueY, chartWidth, valueY), paint);      canvas.drawParagraph(buildParagraph(curData.name, 12, origin.dx-5),          Offset(origin.dx + (i+1) * horiScalInte - 25, origin.dy+8));    }  }  @override  bool shouldRepaint(MyCustomPainter oldDelegate) {    // TODO: implement shouldRepaint    return oldDelegate.data != data;  }}

代碼實現中表示我們將傳入一個包含柱狀圖數據的data對象,paint方法會根據data數據集合來在一個坐標系中繪製柱狀圖。在具體的Widget頁面應用方法如下:

class HomePageState extends State<HomePage> {  List<BaseData> data = List();  @override  Widget build(BuildContext context) {    // TODO: implement build    return Scaffold(      appBar: AppBar(        title: Text('自定義繪製Widget'),      ),      body: Container(        child: buildDemoPaintWidget(),      ),    );  }  Widget buildDemoPaintWidget() {    return Column(      crossAxisAlignment: CrossAxisAlignment.start,      children: <Widget>[        Container(          height: 350,          child: buildPaintWidget(),        ),        Container(          child: Row(            mainAxisAlignment: MainAxisAlignment.spaceAround,            children: <Widget>[              MaterialButton(                onPressed: () {                  BaseData book1 = BaseData(name: '書籍A', num: 5000);                  BaseData book2 = BaseData(name: '書籍B', num: 2000);                  BaseData book3 = BaseData(name: '書籍C', num: 3000);                  List<BaseData> dataList = List();                  dataList.add(book1);                  dataList.add(book2);                  dataList.add(book3);                  setState(() {                    this.data = dataList;                  });                },                child: Text('三本書'),              ),              MaterialButton(                onPressed: () {                  BaseData book4 = BaseData(name: '書籍D', num: 4500);                  setState(() {                    this.data.add(book4);                  });                },                child: Text('四本書'),              ),              MaterialButton(                onPressed: () {                  BaseData book5 = BaseData(name: '書籍E', num: 2500);                  setState(() {                    this.data.add(book5);                  });                },                child: Text('五本書'),              ),            ],          ),        ),      ],    );  }  //構建自定義繪製的Widget  Widget buildPaintWidget() {    return CustomPaint(      painter: MyCustomPainter(data),    );  }}

以上代碼,初始時data數據為空,通過點擊三個按鈕來改變data的值從而重新繪製柱狀圖,具體效果如下:

總結

本篇我們對自定義Widget進行了詳細介紹,實際開發中無非就是使用者兩種方式來實現五花八門的UI需求,其中繪製自定義Widget在圖表較多的APP中會經常使用,後續我們會專門用一篇文章來介紹各種圖形的繪製方法,敬請期待!

相關焦點

  • Flutter widget點擊事件和點擊態
    ,控制項四周出現一個深藍色的邊框– 放開點擊控制項顏色會改變顏色,active狀態綠色,inactive灰色實現過程首先你需要知道的是:我們處理手勢可以使用GestureDetector組件,它是可以添加手勢的一個widget,觀察它的源碼:可以看到GestureDetector的本質就是一個普通的widget,它擁有很多的手勢onTapDown
  • Flutter自定義Stateful(有狀態)組件
    相對應的就有statelesswidget 沒有內部狀態變化的組件,例如 Icon、 IconButton, 和Text 都是無狀態widget, 他們都是 StatelessWidget的子類。>2.創建一個繼承自State的類來處理這個可變控制項的狀態和顯示樣式(build方法)。
  • 聊聊iOS14的Widget 和背後的 SwiftUI
    升級後最顯著的感知莫過於全新的 widget 和 App 資源庫。升級之前看過一些討論,有部分開發者認為蘋果對小部件的限制導致了實用性的缺乏,對比之前是一種倒退。不過在我個人使用和開發之後卻有些相反的體驗,全新的桌面系統具有完全不同的使用邏輯,隨之改變自己的使用習慣,會比之前的更加簡潔和實用。這需要稍稍適應一下,改變從來如此。
  • 探秘 iOS 14 的 WidgetKit
    而 Widget 的展示的內容也是可以讓用戶去定製的,比如天氣 widget 可以讓用戶去選擇展示的城市,這個定製的選項是使用了 Intents 這個框架,這個框架最開始是在SiriKit上使用的,開發者是不需要編寫代碼,只需要加相關的配置項就可以自動生成界面和選項了。最後 Widget 點擊的跳轉是通過一個叫Link的類去處理,類似NSURL。
  • Flutter 中文文檔:Widget 測試介紹
    使用 WidgetTester 建立 Widget下一步,為了在測試環境中建立 MyWidget,我們可以使用 WidgetTester 提供的 pumpWidget() 方法。 pumpWidget 方法會建立並渲染我們提供的 widget。
  • widgetsmith是什麼 Widgetsmith怎麼設置小組件使用教程
    widgetsmith讓大家都能在手機上體驗小組件的樂趣,自從iOS14 更新以來,其中的新功能不斷的被人挖掘出來,這款小部件的設置也是更新功能之後才能讓你們使用的,但是對於設置方式很多的用戶都不知道怎麼弄,下面可以來看看詳細的教程哦!
  • 全新App 整理方式、桌面 widget!
    此次 iOS 把重點放在桌面使用界面的更改與創新功能兩大部分;在桌面使用體驗提出三大改變: app Library 、桌面 widget與 Picture to Picture 浮動視窗;創新功能則包括離線翻譯的新 Siri ,新版地圖、可群聊的 Apple Messenger 與 App Clips 感應。
  • Flutter篇:Widget詳解篇
    (通過熱重載)修復崩潰並繼續從應用程式停止的地方進行調試創建美觀,高度定製的用戶體驗受益於使用Flutter框架提供的豐富的Material Design和Cupertino(iOS風格)的widget
  • AutoMapper實現模塊化註冊自定義擴展MapTo<>()
    上次回顧在上篇文章講了Sukt.Core框架的一個整體架構搭建和怎樣實現模塊化註冊和批量注入,如果沒有看過上篇文章的請查看從我做起[原生DI實現模塊化和批量注入].Net Core 之一,本篇來講進行AutoMapper的模塊化註冊;階段一  上篇文章有講到我們有一個自動註冊的基類,那麼我們這次來回顧一下上次的依賴注入也是重寫的這個自動註冊基類他就是SuktAppModuleBase
  • Faker Providers使用及自定義開發
    下面看看如何創建一個自己的Provider,直接看代碼實例# -*- coding: utf-8 -*-__author__ = "苦葉子"""" 實現一個簡單的fakerfaker.providers import BaseProvider# 創建一個我們的providerclass MyProvider(BaseProvider): def my_name(self): return "DeepTest"if __name__ == "__main__": print("使用自定義
  • 了解 C# foreach 內部語句和使用 yield 實現的自定義迭代器
    你對 IEnumerable<T> 和 IEnumerator<T> 之間關係的熟悉程度如何? 而且,就算你了解可枚舉接口,是否熟練掌握使用 yield 語句實現此類接口呢? 此外,許多集合類(包括 Stack<T>、Queue<T> 和 Dictionary<TKey and TValue>)都不支持按索引檢索元素。因此,需要使用一種更為通用的方法來迭代元素集合。迭代器模式就派上用場了。假設可以確定第一個、第二個和最後一個元素,那麼就沒有必要知道元素數量,也沒有必要支持按索引檢索元素。
  • Koo.js加入回調函數支持,實現自定義功能擴展
    使用十分簡便,不需寫任何代碼即可實現各種的表單驗證,同時支持select,checkbox,radio等默認值,一個標籤全部搞定。本次更新加入回調函數支持,通過回調函數實現自定義功能擴展,使用方法如下:<script type="text/javascript"> var callback = function () { alert('執行回調函數'); return false; } $(document
  • EditText 使用詳解
    和你一起終身學習,這裡是程式設計師Android
  • Jaskson精講第6篇-自定義JsonSerialize與Deserialize實現數據類型...
    有的小夥伴以為Jackson只能在Spring框架內使用,其實不是的,沒有這種限制。它提供了很多的JSON數據處理方法、註解,也包括流式API、樹模型、數據綁定,以及複雜數據類型轉換等功能。它雖然簡單易用,但絕對不是小玩具,更多的內容我會寫成一個系列,5-10篇文章,請您繼續關注我。
  • qtreewidget選擇節點專題及常見問題 - CSDN
    在使用QTreeWidget時,默認是帶有虛線的,如下圖所示:  接下來,再介紹一下,只設置頂級節點無虛線,子節點有虛線的方法: 同樣可以採用在源碼裡設置樣式和qtcreator中設置樣式兩種方法,這裡就不再贅述,將設置的參數簡單介紹一下。
  • Slider Widget:簡單實用的安卓音量調節插件
    安卓系統將音量分為了多種,包括鈴聲通知音量、鬧鐘、通話、多媒體等多種音量,這種設計讓用戶在使用的時候更加自由,可有時候也會因為設置項過多而感覺麻煩。不妨試試Slider Widget,或許它會讓音量調節變的得心應手。
  • 《AI少女》貼圖覆蓋MOD使用方法介紹 怎麼在妹子身上自定義圖案
    AI少女怎麼在妹子身上自定義圖案?
  • 如何在QQ界面使用自定義圖片
    相信有不少朋友熱衷於體驗不同的QQ皮膚,但有的朋友已經對騰訊的官方皮膚感到厭倦,或者限於會員門欄不能使用心儀的皮膚,既然如此,何不使用自己挑選的圖片作為界麵皮膚呢,小編今天為各位簡述一下使用自定義圖片作為界面及聊天背景的方法。
  • 《骰子地下城》怎麼自定義角色 自定義角色方法
    同時遊戲當中玩家也是可以進行角色自定義,通過簡單的方式打造一個屬於自己的強力角色,來看看要怎麼自定義角色吧。 自定義角色方法 1、進入到遊戲主界面,點擊編輯器。 2、左邊可以編輯玩家使用的角色,右邊則是自定義敵人(右上角可以隨機)
  • Android RecyclerView自定義LayoutManager
    在第一篇中已經講過,LayoutManager主要用於布局其中的Item,在LayoutManager中能夠對每個Item的大小,位置進行更改,將它放在我們想要的位置,在很多優秀的效果中,都是通過自定義LayoutManager來實現的,比如:Github: https://github.com