介紹
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將一個組件同時用於新舊子元素。