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

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