Unity的Flutter——UIWidgets簡介及入門

介紹

UIWidgets(https://github.com/UnityTech/UIWidgets)是Unity編輯器的一個插件包,可幫助開發人員通過Unity引擎來創建、調試和部署高效的跨平臺應用。

UIWidgets主要來自Flutter。但UIWidgets通過使用強大的Unity引擎爲開發人員提供了許多新功能,顯著地改進他們開發的應用性能和工作流程。

效率

通過使用最新的Unity渲染SDK,UIWidgets應用可以非常快速地運行並且大多數時間保持大於60fps的速度。

跨平臺

與任何其他Unity項目一樣,UIWidgets應用可以直接部署在各種平臺上,包括PC,移動設備和網頁等。

多媒體支持

除了基本的2D UI之外,開發人員還能夠將3D模型,音頻,粒子系統添加到UIWidgets應用中。

文檔

官方文檔只是簡單介紹,代碼中沒有任何中英文註釋,部分demo還未完成。
不過由於基於Flutter,大部分API與Flutter一致,所以可以參考Flutter文檔,或者先學習一下Flutter。

Unity Connect App

Unity Connect App是使用UIWidgets開發的一個移動App產品,可以在Android下載以及iOS App Store下載最新的版本.

github地址https://github.com/UnityTech/ConnectAppCN.

安裝

首先需要 Unity 2018.3 或更高版本。

新建一個unity項目或一個已有的項目。

訪問UIWidgets的github庫https://github.com/UnityTech/UIWidgets下載最新UIWidgets包,將其移至項目的Package文件夾中

或者可以在終端中通過git命令來完成這個操作:

 cd <YourProjectPath>/Packages
 git clone https://github.com/UnityTech/UIWidgets.git com.unity.uiwidgets

官方示例

在示例中,我們將創建一個非常簡單的UIWidgets應用。 該應用只包含文本標籤和按鈕。 文本標籤將計算按鈕上的點擊次數。

首先,使用Unity編輯器打開項目。

場景構建

選擇 File > New Scene來創建一個新場景。

選擇 GameObject > UI > Canvas
在場景中創建UI Canvas。

右鍵單擊Canvas並選擇UI > Panel,將面板添加到UI Canvas中。 然後刪除面板中的 Image 組件。

最後爲場景命名並保存至Assets/Scenes目錄下

創建部件

創建一個新C#腳本,命名爲“UIWidgetsExample.cs”

    using System.Collections.Generic;
    using Unity.UIWidgets.animation;
    using Unity.UIWidgets.engine;
    using Unity.UIWidgets.foundation;
    using Unity.UIWidgets.material;
    using Unity.UIWidgets.painting;
    using Unity.UIWidgets.ui;
    using Unity.UIWidgets.widgets;
    using UnityEngine;
    using FontStyle = Unity.UIWidgets.ui.FontStyle;

    namespace UIWidgetsSample {
        public class UIWidgetsExample : UIWidgetsPanel {
            protected override void OnEnable() {
                // if you want to use your own font or font icons.
                // FontManager.instance.addFont(Resources.Load<Font>(path: "path to your font"), "font family name");

                // load custom font with weight & style. The font weight & style corresponds to fontWeight, fontStyle of
                // a TextStyle object
                // FontManager.instance.addFont(Resources.Load<Font>(path: "path to your font"), "Roboto", FontWeight.w500,
                //    FontStyle.italic);

                // add material icons, familyName must be "Material Icons"
                // FontManager.instance.addFont(Resources.Load<Font>(path: "path to material icons"), "Material Icons");

                base.OnEnable();
            }

            protected override Widget createWidget() {
                return new WidgetsApp(
                    home: new ExampleApp(),
                    pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) =>
                        new PageRouteBuilder(
                            settings: settings,
                            pageBuilder: (BuildContext context, Animation<float> animation,
                                Animation<float> secondaryAnimation) => builder(context)
                        )
                );
            }

            class ExampleApp : StatefulWidget {
                public ExampleApp(Key key = null) : base(key) {
                }

                public override State createState() {
                    return new ExampleState();
                }
            }

            class ExampleState : State<ExampleApp> {
                int counter = 0;

                public override Widget build(BuildContext context) {
                    return new Column(
                        children: new List<Widget> {
                            new Text("Counter: " + this.counter),
                            new GestureDetector(
                                onTap: () => {
                                    
                    //這裏使用setState來改變counter的值則可以同步改變Text顯示,如果不用直接counter++則無法改變顯示                this.setState(() => {
                                        this.counter++;
                                    });
                                },
                                child: new Container(
                                    padding: EdgeInsets.symmetric(20, 20),
                                    color: Colors.blue,
                                    child: new Text("Click Me")
                                )
                            )
                        }
                    );
                }
            }
        }
    }

保存腳本,並附加到panel中作爲其組件。
(如果添加失敗,請檢查文件名與類名是否一致)

保存場景,運行就可以看到效果了。

Image組件

簡單介紹一下Image組件

加載資源文件

將資源圖片放入Assets/Resources目錄下

使用asset函數即可創建一個Image並加載相應資源

Unity.UIWidgets.widgets.Image.asset("test")

注意不需要文件後綴。

加載網絡資源

Unity.UIWidgets.widgets.Image.network("https://www.baidu.com/img/xinshouyedong_4f93b2577f07c164ae8efa0412dd6808.gif")

Image支持Gif!可以直接加載顯示Gif。

除了上面兩種方式,還可以通過文件和byte數組來加載資源,函數分別爲file()和memory()。

改變大小等屬性

Unity.UIWidgets.widgets.Image.asset(
  name: "test",
  height: 100
)

這裏涉及到默認參數,先來看一個aseet函數源碼

public static Image asset(
            string name,
            Key key = null,
            AssetBundle bundle = null,
            float? scale = null,
            float? width = null,
            float? height = null,
            Color color = null,
            BlendMode colorBlendMode = BlendMode.srcIn,
            BoxFit? fit = null,
            Alignment alignment = null,
            ImageRepeat repeat = ImageRepeat.noRepeat,
            Rect centerSlice = null,
            bool gaplessPlayback = false,
            FilterMode filterMode = FilterMode.Bilinear
        ) {
            var image = scale != null
                ? (AssetBundleImageProvider) new ExactAssetImage(name, bundle: bundle, scale: scale.Value)
                : new AssetImage(name, bundle: bundle);

            return new Image(
                key,
                image,
                width,
                height,
                color,
                colorBlendMode,
                fit,
                alignment,
                repeat,
                centerSlice,
                gaplessPlayback,
                filterMode
            );
        }

除了name,其他參數都設置了默認參數,這樣在使用這個函數時,無需改變默認參數的參數不必傳入,這樣就需要傳參時帶上參數名。但是如果只傳一個name參數的時候,可以省略參數名。

所以想改變或設置Image或其他組件的屬性時,只需要添加對應參數即可。

如改變圖片的拉伸規則

Unity.UIWidgets.widgets.Image.asset(
   name: "test",
   height: 100,
   width: 100,
   fit: Unity.UIWidgets.painting.BoxFit.fill
)

Navigation頁面跳轉

參考官方示例中的demo,簡化代碼如下:

using System;
using System.Collections.Generic;
using Unity.UIWidgets.animation;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.foundation;
using Unity.UIWidgets.gestures;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.rendering;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using TextStyle = Unity.UIWidgets.painting.TextStyle;

namespace UIWidgetsSample {
   public class NavigationEx : UIWidgetsPanel{

       protected override Widget createWidget() {
           return new WidgetsApp(
               initialRoute: "/",
               textStyle: new TextStyle(fontSize: 24),
               pageRouteBuilder: this.pageRouteBuilder,
               //初始化所有route路由
               routes: new Dictionary<string, WidgetBuilder> {
                   {"/", (context) => new HomeScreen()},
                   {"/detail", (context) => new DetailScreen()}
               });
       }
       
       protected PageRouteFactory pageRouteBuilder {
           get {
               return (RouteSettings settings, WidgetBuilder builder) =>
                   new PageRouteBuilder(
                       settings: settings,
                       pageBuilder: (BuildContext context, Animation<float> animation,
                           Animation<float> secondaryAnimation) => builder(context),
                       //設置轉場動畫,去掉則沒有轉場動畫
                       transitionsBuilder: (BuildContext context, Animation<float>
                               animation, Animation<float> secondaryAnimation, Widget child) =>
                           new _FadeUpwardsPageTransition(
                               routeAnimation: animation,
                               child: child
                           )
                   );
           }
       }
   }

   //首頁
   class HomeScreen : StatelessWidget {
       public override Widget build(BuildContext context) {
           //封裝了一個NavigationPage,詳細見後續代碼
           return new NavigationPage(
               body: new Container(
                   //設置背景色
                   color: new Color(0xFF888888),
                   //Center組件可以實現相對parent居中
                   child: new Center(
                       //設置按鈕點擊後跳轉到“/detail”
                       child: new GestureDetector(onTap: () => { Navigator.pushNamed(context, "/detail"); },
                           child: new Text("Go to Detail"))
                   )),
               title: "Home"
           );
       }
   }

   //詳情頁
   class DetailScreen : StatelessWidget {
       public override Widget build(BuildContext context) {
           return new NavigationPage(
               body: new Container(
                   color: new Color(0xFF1389FD),
                   child: new Center(
                       child: new Column(
                           children: new List<Widget>() {
                               //設置按鈕點擊關閉頁面,回到上一頁
                               new GestureDetector(onTap: () => { Navigator.pop(context); }, child: new Text("Back"))
                           }
                       )
                   )),
               title: "Detail");
       }
   }

   //轉場動畫
   class _FadeUpwardsPageTransition : StatelessWidget {
       internal _FadeUpwardsPageTransition(
           Key key = null,
           Animation<float> routeAnimation = null, // The route's linear 0.0 - 1.0 animation.
           Widget child = null
       ) : base(key: key) {
           //設置滑動的偏移動畫,並且添加了動畫曲線fastOutSlowIn,具體見後面代碼
           this._positionAnimation = _bottomUpTween.chain(_fastOutSlowInTween).animate(routeAnimation);
           //設置顯隱動畫,並且添加了動畫曲線easeIn,具體見後面代碼
           this._opacityAnimation = _easeInTween.animate(routeAnimation);
           this.child = child;
       }

       //設置(滑動)動畫的x、y偏移,是百分比值。開始點是x無偏移,y偏移0.25,即從頁面高度的1/4開始。結束點是無偏移,即原位置。所以動畫是從頁面1/4處向上滑動到頂部
       static Tween<Offset> _bottomUpTween = new OffsetTween(
           begin: new Offset(0.0f, 0.25f),
           end: Offset.zero
       );

       //動畫曲線fastOutSlowIn,先加速再減速
       static Animatable<float> _fastOutSlowInTween = new CurveTween(curve: Curves.fastOutSlowIn);
       //動畫曲線easeIn,初始緩動
       static Animatable<float> _easeInTween = new CurveTween(curve: Curves.easeIn);

       readonly Animation<Offset> _positionAnimation;
       readonly Animation<float> _opacityAnimation;
       public readonly Widget child;

       public override Widget build(BuildContext context) {
           //轉場動畫是包含一個滑動動畫和一個顯隱動畫,是一個層次關係
           return new SlideTransition(
               position: this._positionAnimation,
               child: new FadeTransition(
                   opacity: this._opacityAnimation,
                   //需要實現動畫的組件
                   child: this.child
               )
           );
       }
   }

   //封裝了一個導航佈局,頂部導航欄,底部頁面內容
   class NavigationPage : StatelessWidget {
       //頁面內容
       public readonly Widget body;
       //頁面標題,顯示在導航欄
       public readonly string title;

       public NavigationPage(Widget body = null, string title = null) {
           this.title = title;
           this.body = body;
       }

       public override Widget build(BuildContext context) {
           Widget back = null;
           //判斷是否可以回退頁面,即頁面列表的size大於1。如果可以則創建back按鈕,點擊關閉當前頁回退到上一頁
           if (Navigator.of(context).canPop()) {
               back = new GestureDetector(onTap: () => { Navigator.pop(context); },
                   child: new Text("Go Back"));
               back = new Column(mainAxisAlignment: MainAxisAlignment.center, children: new List<Widget>() {back});
           }


           return new Container(
               child: new Column(
                   children: new List<Widget>() {
                       //頂部是一個導航欄,這裏可以看到用了ConstrainedBox和DecoratedBox
                       new ConstrainedBox(constraints: new BoxConstraints(maxHeight: 80),
                           child: new DecoratedBox(
                               decoration: new BoxDecoration(color: new Color(0XFFE1ECF4)),
                               //NavigationToolbar導航工具欄,其中分爲三個區域:左側leading、中間middle和右側trailing。這裏左側放置了回退按鈕
                               child: new NavigationToolbar(leading: back,
                                   middle: new Text(this.title, textAlign: TextAlign.center)))),
                       //內容部分。這裏使用了一個Flexible,作用是可以填滿剩餘的空間,如果使用普通的container無法達到效果
                       new Flexible(child: this.body)
                   }
               )
           );
       }
   }
}

可以看到在創建WidgetsApp時,設定所有路由的路徑和實現,然後通過Navigator類切換路徑來控制頁面跳轉。

Material

UIWidgets同樣提供了Material風格的組件,通過一個demo簡單認識一下

using System.Collections.Generic;
using Unity.UIWidgets.animation;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.rendering;
using Unity.UIWidgets.service;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Image = Unity.UIWidgets.widgets.Image;

namespace UIWidgetsEx {
   public class MaterialEx : UIWidgetsPanel{

       protected override Widget createWidget()
       {
           return new MaterialApp(
               home: new MaterialThemeSampleWidget(),
               darkTheme: new ThemeData(primaryColor: Colors.black26)
           );
       }

       protected override void OnEnable()
       {
           //加載Material字體,用於icon
           FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
           base.OnEnable();
       }
   }

   public class MaterialThemeSampleWidget : StatefulWidget
   {
       public override State createState()
       {
           return new _MaterialThemeSampleWidgetState();
       }
   }

   class _MaterialThemeSampleWidgetState : State<MaterialThemeSampleWidget>
   {
       //GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>.key("lalala");
       public override Widget build(BuildContext context)
       {
           return new Theme(
               //data設置樣式
               data: new ThemeData(
                   appBarTheme: new AppBarTheme(
                       color: Colors.purple
                   ),
                   bottomAppBarTheme: new BottomAppBarTheme(
                       color: Colors.blue
                   ),
                   cardTheme: new CardTheme(
                       color: Colors.red,
                       //設置深度,即陰影大小
                       elevation: 2.0f
                   )
               ),
               //頁面根佈局是一個Scaffold,大致分爲三個區域appBar、body和bottomNavigationBar。
               child: new Scaffold(
                   //key: scaffoldKey,
                   //導航欄AppBar
                   appBar: new AppBar(
                       //設置深度,即陰影大小
                       //elevation: 25f,
                       title: new Text("Test App Bar Theme")),
                   body: new Center(
                       //組件Card,自帶陰影效果
                       child: new Card(
                           //設置深度,即陰影大小
                           //elevation: 25f,
                           //設置圓角borderRadius,還可以設置邊框side
                           shape: new RoundedRectangleBorder(
                           //side: new BorderSide(Colors.blue, 5.0f),
                               borderRadius: BorderRadius.all(5.0f)
                           ),
                           child: new Container(
                               height: 250,
                               child: new Column(
                                   children: new List<Widget> {
                                       Image.asset(
                                           "products/backpack",
                                           fit: BoxFit.cover,
                                           width: 200,
                                           height: 200
                                       ),
                                       new Text("Card Theme")
                                   }
                               )
                           )
                       )
                   ),
                   //底部是BottomAppBar
                   bottomNavigationBar: new BottomAppBar(
                       child: new Row(
                       mainAxisSize: MainAxisSize.max,
                       //設置對齊方式
                       mainAxisAlignment: MainAxisAlignment.spaceBetween,
                       children: new List<Widget> {
                           //底部是兩個IconButton,使用字體中的icon
                           new IconButton(icon: new Icon(Unity.UIWidgets.material.Icons.menu), onPressed: () => { }),
                           new IconButton(icon: new Icon(Unity.UIWidgets.material.Icons.account_balance), onPressed: () => { })
                       })
                   )
                    //,
                   //floatingActionButton: new FloatingActionButton(
                   //    child: new Text("go"),
                   //    onPressed: () => {
                   //        scaffoldKey.currentState.showSnackBar(
                   //            new SnackBar(
                   //                content: new Text("go")
                   //            )
                   //        );
                   //    }
                   //),
                   //floatingActionButtonLocation: FloatingActionButtonLocation.endDocked
               )
           );
       }
   }
}

有關Material部分跟Android基本一致,比如組件都有深度屬性elevation,比如Card就是Material中的CardView。

IconButtom使用字體Font,實際上就是5.0之後引入的SVG,即Vector Image

我們簡單補充一下:

Scaffold

實際上對應着Android中的CoordinateLayout,分爲三大區域appBar、body和bottomNavigationBar,當未設置時則隱藏該區域,body會補充填充。

除了三大區域還包括:懸浮按鈕floatingActionButton、bottomSheet、drawer、endDrawer等

其中floatingActionButton同樣保持着android中的behavior效果,下面會詳細介紹

AppBar

AppBar則包含:title、左側leading、右側擴展菜單actions

MainAxisAlignment

對齊方式,這個對齊方式相對於android更靈活一些,包括:

  • start 左(上)側對齊
  • end 右(下)側對齊
  • center 居中對齊
  • spaceBetween 每兩個item的間隔space相同,兩側的item與父佈局沒有間隔
  • spaceAround 每個item四周都有同樣的間隔,相當於margin,所以兩個item之間是兩倍間隔
  • spaceEvenly item在父佈局中均勻分佈,即所有間隔相同

FloatingActionButton

在Scaffold中floatingActionButton和floatingActionButtonLocation要搭配使用,floatingActionButtonLocation決定着按鈕的位置,包含:

  • endFloat: body的右下角
  • endDocked: bottomNavigationBar右側,中心錨點在bottomNavigationBar的頂端
  • centerDocked: bottomNavigationBar中間,中心錨點在bottomNavigationBar的頂端
  • centerFloat: body的底部中間
  • startTop:appBar左側,中心錨點appBar的底端
  • miniStartTop:與startTop類似,與左側間隔更小一點
  • endTop:appBar右側,中心錨點appBar的底端

除了上面這些我們還可以實現FloatingActionButtonLocation這個抽象類自定義位置

SnackBar

在Android中FloatingActionButton是存在一個默認behavior的,最明顯的就是與SnackBar互動。

在UIWidgets中也有SnackBar組件,使用起來比較簡單。

首先需要創建一個GlobalKey,並將它賦給Scaffold的key。這樣我們就可以使用
scaffoldKey.currentState.showSnackBar()這個函數來顯示一個SnackBar。

Animation

在前面的示例中我們使用了轉場動畫,但是並沒有體現動畫的完整應用,因爲部分邏輯比如執行動畫等封裝在底層了。下面這個例子將完整的展現如何創建並執行一個動畫。

using System.Collections.Generic;
using Unity.UIWidgets.animation;
using Unity.UIWidgets.cupertino;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.foundation;
using Unity.UIWidgets.scheduler;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;

namespace UIWidgetsSample
{
    public class AnimEx : UIWidgetsPanel
    {
        protected override void OnEnable()
        {
            base.OnEnable();
        }

        protected override Widget createWidget()
        {
            return new WidgetsApp(
                home: new ExampleApp(),
                pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) =>
                    new PageRouteBuilder(
                        settings: settings,
                        pageBuilder: (BuildContext context, Animation<float> animation,
                            Animation<float> secondaryAnimation) => builder(context)
                    )
            );
        }

        class ExampleApp : StatefulWidget
        {
            public ExampleApp(Key key = null) : base(key)
            {
            }

            public override State createState()
            {
                return new ExampleState();
            }
        }

        class ExampleState : State<ExampleApp>
        {
            //IconData iconData = Unity.UIWidgets.material.Icons.menu;
            //Curve switchCurve = new Interval(0.4f, 1.0f, curve: Curves.fastOutSlowIn);
            //TextAnim類用一個動畫組件將Image組件封裝起來,見後面
            TextAnim textAnim = new TextAnim();
            public override Widget build(BuildContext context)
            {
                return new Column(
                    children: new List<Widget> {
                        ////切換動畫
                        //new AnimatedSwitcher(
                        //    duration: new System.TimeSpan(0, 0, 1),
                        //    switchInCurve: switchCurve,
                        //    switchOutCurve: switchCurve,
                        //    child: new IconButton(
                        //        //不同的key纔會認爲是不同的組件,否則不會執行動畫
                        //        key: new ValueKey<IconData>(iconData),
                        //        icon: new Icon(icon :iconData, color: Colors.white),
                        //        onPressed: () => {
                        //            this.setState(() => {
                        //                if (iconData.Equals(Unity.UIWidgets.material.Icons.menu))
                        //                {
                        //                    iconData = Unity.UIWidgets.material.Icons.close;
                        //                }
                        //                else
                        //                {
                        //                    iconData = Unity.UIWidgets.material.Icons.menu;
                        //                }
                        //            });
                        //        }
                        //    )
                        //),
                        //這裏使用了Cupertino風格的button
                        new CupertinoButton(onPressed: () => {
                                //點擊後通過controller執行動畫
                                textAnim.controller.forward(); 
                            },
                            child: new Text("Go"),
                            color: CupertinoColors.activeBlue
                            ),
                            textAnim.build(context)
                    }
                   );
            }
        }

        //繼承SingleTickerProviderStateMixin,這是TickerProvider接口的實現類
        private class TextAnim : SingleTickerProviderStateMixin<ExampleApp>
        {
            public AnimationController controller = null;
            public override Widget build(BuildContext context)
            {
                //定一個一個AnimationController。TimeSpan(0, 0, 2)代表0小時0分2秒,也就是動畫時長是兩秒
                controller = new AnimationController(duration: new System.TimeSpan(0, 0, 2), vsync: this);
                
                //設置動畫監聽,當結束時還原
                controller.addStatusListener((status) => {
                    if(status == AnimationStatus.completed)
                    {
                        controller.reset();
                    }
                });
                
                //定義Animation,從原點下移自身兩個高度
                Animation<Offset> offset = controller.drive(new OffsetTween(
                    begin: Offset.zero,
                    end: new Offset(0.0f, 2f)
                ));
                
                //創建一個滑動動畫組件
                return new SlideTransition(
                            position: offset,
                            child: Unity.UIWidgets.widgets.Image.asset(
                                name: "test",
                                height: 100
                            )
                        );
            }
        }
    }

}

執行後點擊button,圖片會執行下移動畫,執行完回到原位置。

當然我們還可以設置動畫曲線、組合多個動畫等等。

而且我們可以看到使用了ios的Cupertino風格的button。與Material一樣,UIWidgets也有一套Cupertino風格的組件。

vsync

創建AnimationController時,需要設置一個vsync,是TickerProvider類型的,即計時器。那麼這個東西到底有什麼用?

vsync對象會綁定動畫的定時器到一個可視的widget,所以當widget不顯示時,動畫定時器將會暫停,當widget再次顯示時,動畫定時器重新恢復執行,這樣就可以避免動畫相關UI不在當前屏幕時消耗資源。

AnimatedSwitcher

切換動畫,可以同時對其新、舊子元素添加顯示、隱藏動畫。新舊子元素可以是是兩個組件,也可以利用key將一個組件同時用於新舊子元素。

Demo

github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章