赞
踩
UIWidgets(https://github.com/UnityTech/UIWidgets)是Unity编辑器的一个插件包,可帮助开发人员通过Unity引擎来创建、调试和部署高效的跨平台应用。
UIWidgets主要来自Flutter。但UIWidgets通过使用强大的Unity引擎为开发人员提供了许多新功能,显著地改进他们开发的应用性能和工作流程。
来看看效果图:
可以看到通过UIWidgets可以实现应用效果,UIWidgets的优势如下:
通过使用最新的Unity渲染SDK,UIWidgets应用可以非常快速地运行并且大多数时间保持大于60fps的速度。
与任何其他Unity项目一样,UIWidgets应用可以直接部署在各种平台上,包括PC,移动设备和网页等。
除了基本的2D UI之外,开发人员还能够将3D模型,音频,粒子系统添加到UIWidgets应用中。
官方文档只是简单介绍,代码中没有任何中英文注释,部分demo还未完成。
不过由于基于Flutter,大部分API与Flutter一致,所以可以参考Flutter文档,或者先学习一下Flutter。
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组件
将资源图片放入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
)
参考官方示例中的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类切换路径来控制页面跳转。
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
我们简单补充一下:
实际上对应着Android中的CoordinateLayout,分为三大区域appBar、body和bottomNavigationBar,当未设置时则隐藏该区域,body会补充填充。
除了三大区域还包括:悬浮按钮floatingActionButton、bottomSheet、drawer、endDrawer等
其中floatingActionButton同样保持着android中的behavior效果,下面会详细介绍
AppBar则包含:title、左侧leading、右侧扩展菜单actions
对齐方式,这个对齐方式相对于android更灵活一些,包括:
在Scaffold中floatingActionButton和floatingActionButtonLocation要搭配使用,floatingActionButtonLocation决定着按钮的位置,包含:
除了上面这些我们还可以实现FloatingActionButtonLocation这个抽象类自定义位置
在Android中FloatingActionButton是存在一个默认behavior的,最明显的就是与SnackBar互动。
在UIWidgets中也有SnackBar组件,使用起来比较简单。
首先需要创建一个GlobalKey,并将它赋给Scaffold的key。这样我们就可以使用
scaffoldKey.currentState.showSnackBar()这个函数来显示一个SnackBar。
在前面的示例中我们使用了转场动画,但是并没有体现动画的完整应用,因为部分逻辑比如执行动画等封装在底层了。下面这个例子将完整的展现如何创建并执行一个动画。
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风格的组件。
创建AnimationController时,需要设置一个vsync,是TickerProvider类型的,即计时器。那么这个东西到底有什么用?
vsync对象会绑定动画的定时器到一个可视的widget,所以当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在当前屏幕时消耗资源。
切换动画,可以同时对其新、旧子元素添加显示、隐藏动画。新旧子元素可以是是两个组件,也可以利用key将一个组件同时用于新旧子元素。
通过上面介绍可以看到,UIWidgets实际上就是Unity上的Flutter,使用起来与Flutter也非常类似。关于Flutter我推荐一下自己写的几篇文章:
【Flutter混合开发】在Android项目中如何启动Flutter
【Flutter进阶】与Native进行交互的三种方式
【Flutter进阶】聊一聊组件中的生命周期、状态管理及局部重绘
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。