UIWidgetsの機能を軽く確かめた備忘録(uGUIのように使う)

# はじめに
この記事はK3 Advent calendar 18日目のものです。
こういう機会がないとなかなか試すこともないため，ちょうどいい機会なのでUIWidgetsの機能を軽く試していきたいと思います。
今年(2019年)7月に投稿した[UIWidgetsアセットの記事](https://qiita.com/NWaka_1415/items/d74f7a6d4198e70e4b0d)の続き(?)的なものになりますかね。
UIWidgetsに関する細かいことは上記記事を読んでいただければ雰囲気はつかめるかなと思います。
僕はFlutterなんもわからんな人なので，コードはあくまで参考程度にお願いします。
# 環境
Unity 2019.2.12f1
UIWidgets 1.5.4-preview.0

# 画像の表示
Flutterの画像の表示にはいくつか種類がありますが，今回は`Image.network()`と`Image.asset()`の使用方法を見ていきます。
## ネットワークの画像を表示(`Image.network()`)
こちらはFlutterと何も変わりません。

```.cs
// using Image = Unity.UIWidgets.widgets.Image;
Image.network(src: "ネットワーク先のパス")
```

ホントに何も変わらないですね。Unityだからこその注意点としては，uGUIの`UnityEngine.UI.Image`が別に存在している点ですね。
実際に以下のコードを使用して表示してみました。
画像はFlutter.devのものをお借りしました。
※前回のコードを流用しているので所々ガバいと思います。

```Test.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Image = Unity.UIWidgets.widgets.Image;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new MyStatelessWidget()
        );
    }
}

public class MyStatelessWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: Image.network(
                        src: "https://i.ytimg.com/vi/fq4N0hgOWzU/maxresdefault.jpg"
                    )
                )
            )
        );
    }
}
```

実行結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/25c18042-637b-79e3-bbdb-0de3f44b1036.png)
真ん中にドドーンと読み込まれた画像が表示できているのがわかります。

ちなみにネットワーク上にあるgif画像も普通に表示できます。使用方法は上記と全く一緒です。
ただ引数変更しただけですが，一応試したコードを載せます。MyStatelessWidgetクラスを以下のようにすればgif再生がされるはずです。

```Test.cs
// 省略
public class MyStatelessWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: Image.network("https://cdn.appllio.com/sites/default/files/Taco_Party_0.gif")
                )
            )
        );
    }
}
```

## アプリケーション内(アセット内)静止画像を表示(`Image.asset()`)
続いてアプリケーション内の画像を表示する方法。
[こちら](https://qiita.com/yu124choco/items/a2710ec004d3425a2a0b)にもある通り，Flutterであればyamlを編集したうえで`Image.asset()`を使用します。また，拡張子も忘れてはいけません。
しかし，UIWidgetsではこのようなことは不要です。

1. Unity利用者にはおなじみの`Resources`フォルダを用意。
1. 表示したい画像を`Resources`フォルダに配置。
1. コードに`Image.asset(パス)`と記載。ただしパスは.pngなどが不要。

で表示できます。

こちらも実際に試してみました。
png画像を用意して，ディレクトリ構造は以下のようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/08cc914b-49c8-e126-9596-c124575ff0e3.png)

コードは以下の通り。

```Test.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Image = Unity.UIWidgets.widgets.Image;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new MyStatelessWidget()
        );
    }
}

public class MyStatelessWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: Image.asset("Images/image1")
                )
            )
        );
    }
}
```

実行結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/98f1a8f8-4086-417b-e270-c0678d91b34f.png)

こちらはUnityに慣れていればファイルパスなどは問題なさそうですね。Flutterでは必要な.pngなどの拡張子があると逆に表示されないので，そこだけは注意してください。

##アプリケーション内(アセット内)gif画像表示(`Image.asset()`)
なんと！gifももちろん対応しています！
基本的には静止画像のときと変わりません。ただし，少し気を付けることがあります。

1. gifファイルをリネームし，拡張子(.gif)のあとに`.bytes`とつけます。
1. `Image.asset(パス)`で表示できます。ただし，パスには`.gif`拡張子(すでにネームの一部みたいなもんだけど)をつけること

実際に試してみました。
Unity上だと拡張子の方までリネームができないのでエクスプローラー上で行いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/5ea0ef74-d3a7-61e0-4167-45e11494ef14.png)

先ほどの静止画像と同じ配置にし，以下のコードを実行しました。

```Test.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Image = Unity.UIWidgets.widgets.Image;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new MyStatelessWidget()
        );
    }
}

public class MyStatelessWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: Image.asset("Images/gif_test.gif")
                )
            )
        );
    }
}
```

実行結果はこちら(静止画像ですみません，ホントは動いてます)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/0392098b-aab3-2ec4-4b23-2bd87219ad99.png)

Unityではgifをそのまま表示することはできませんのでこれは便利ですね(gif表示を可能にするアセットは存在しているので，この用途のみにUIWidgetsを使用するのも微妙ですが…)。注意点としては`.bytes`を付け足してリネームしないといけない点と`asset()`に渡す引数には`.gif`拡張子もしっかりと記載しないといけない点くらですかね。

# uGUIの一部としてUIWidgetsを使用してみる
ふと思うと今まで画面いっぱいに広げてザ・アプリみたいな見た目でやってきました。
しかし，よく考えたらAppBarとかってつけなくても問題ないじゃないか！

ということでスマホアプリみたいな画面いっぱいの見た目ではなく，uGUIと一緒に――というよりはuGUIのように――UIWidgetsを使用してみたいと思います。
## Gif表示uGUIモドキの作成
gif表示ができることが前節でわかりました。
もちろんgif表示のためだけにUIWidgetsを導入するのは明らかに不要です。もっと特化したアセットを入れるべきでしょう。
しかしながら，うるせぇ！知らねぇ！Final(ry

ということでuGUIのようにUIWidgetsを使用してみましょう。
とりあえず以下のようなコードを作成。

```GifImage.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.widgets;
using Image = Unity.UIWidgets.widgets.Image;

public class GifImage : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new GifImageWidget()
        );
    }
}

class GifImageWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return Image.asset("Images/gif_test.gif");
    }
}
```

元PanelオブジェクトゆえCanvasいっぱいに広がっています。そこに先ほどと同じようにgifだけ表示してくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/d6cee66f-8db1-c30b-5d10-d809b8fd6dc8.png)

元Panelとは言え，サイズや配置は自由に変えられますね？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/b80c6e2a-b974-5fc6-82db-34f649fd5280.png)
つまりこれだけでもうgifを表示させるuGUIオブジェクトみたいなもんですよ！アハハ！

## そのほかのUI
### Button
gifがいけたのなら，ボタンなどもいけるはず！
ボタンのデザインは[[Flutter]コピペで使える！ボタンのデザイン16種類をまとめました](https://qiita.com/coka__01/items/30716f42e4a909334c9f)を参考にしました。

```MaterialButton.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.widgets;
using Color = Unity.UIWidgets.ui.Color;

namespace NWakaTest
{
    public class MaterialButton : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new RaisedButton(
                    child: new Text(data: "Button", style: new TextStyle(color: Color.white, fontSize: 32f)),
                    color: Colors.cyan,
                    shape: new RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(10.0f)
                    ),
                    onPressed: () => { }
                )
            );
        }
    }
}
```

実行せずとも反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/0a3bf6c3-8845-44da-9fee-452ad2260967.png)

サイズ調整も自由自在です。普通のuGUIとなんら変わりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/d2fed57d-ac8b-8630-34aa-ea0dbda63d49.png)

他のデザインも試してみました(アイコン付き)。

```MaterialButton.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Color = Unity.UIWidgets.ui.Color;

namespace NWakaTest
{
    public class MaterialButton : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: RaisedButton.icon(
                    icon: new Icon(
                        Icons.tag_faces,
                        color: Color.white
                    ),
                    label: new Text("Button"),
                    onPressed: () => { },
                    color: Colors.green,
                    textColor: Colors.white
                )
            );
        }

        protected override void OnEnable()
        {
            base.OnEnable();
            FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
        }
    }
}
```

こんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/2de567a9-f1a8-bfad-f0a3-dfc9df554308.png)

### ScrollView

```ScrollView.cs
using System.Collections.Generic;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.widgets;

namespace NWakaTest
{
    public class ScrollView : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new ScrollViewWidgets()
            );
        }
    }

    class ScrollViewWidgets : StatelessWidget
    {
        public override Widget build(BuildContext context)
        {
            return new SingleChildScrollView(
                child: new Column(
                    children: new List<Widget>
                    {
                        new Container(
                            height: 200,
                            color: Colors.green
                        ),
                        new Container(
                            height: 200,
                            color: Colors.black
                        ),
                        new Container(
                            height: 200,
                            color: Colors.red
                        )
                    }
                )
            );
        }
    }
}
```

先ほどまでのシーンに追加しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/ca14fced-11b0-ea58-fc68-65be0f42de8c.png)

実行すると上下に動かすことができます(気が向いたのでgifです)。
![result.gif](https://i.imgur.com/7H9p1IB.gif)
今回はただの色付きContainerですが，ちゃんとすればとても使い勝手がよさそうです。個人的にuGUIのScrollViewは使い慣れないので今度からUIWidgetsのものを使おうかな…
それほど簡単に実装できます。

### ToggleSwitch(Switch)
続いてトグルスイッチ(スイッチ)です。
コードは以下の通り。

```ToggleSwitch.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.widgets;

namespace NWakaTest
{
    public class ToggleSwitch : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new ToggleSwitchWidget()
            );
        }
    }

    class ToggleSwitchWidget : StatefulWidget
    {
        public override State createState()
        {
            return new ToggleState();
        }
    }

    class ToggleState : State<ToggleSwitchWidget>
    {
        private bool _switchFlag = true;

        public override Widget build(BuildContext context)
        {
            return new Switch(
                value: _switchFlag,
                activeColor: Colors.orange,
                onChanged: (value =>
                {
                    setState(() =>
                        {
                            if (value != null)
                            {
                                _switchFlag = (bool) value;
                                TestManager.instance.SetActiveObjects((bool) value);
                            }
                        }
                    );
                })
            );
        }
    }
}
```

TestManagerクラスも用意しました。クソくだらないコードですが許してください。

```TestManager.cs
using System;
using UnityEngine;

namespace NWakaTest
{
    public class TestManager : MonoBehaviour
    {
        [SerializeField] private GameObject gifImage = null;
        [SerializeField] private GameObject scrollView = null;
        [SerializeField] private GameObject button = null;

        private static TestManager _instance = null;

        public static TestManager instance => _instance;

        private void Awake()
        {
            if (_instance == null) _instance = this;
            else if (_instance != this) Destroy(gameObject);
        }

        public void SetActiveObjects(bool value)
        {
            gifImage.SetActive(value);
            scrollView.SetActive(value);
            button.SetActive(value);
        }
    }
}
```

スイッチがオフになると他のゲームオブジェクトを停止させ，オンになるとアクティブになるという実に単純なプログラムです。
![result2](https://i.imgur.com/yC1HRCl.gif)

### Tab
TabBarおよびTabBarViewを用いたTab表現。
まずはコード。

```TabView.cs
using System.Collections.Generic;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.foundation;
using Unity.UIWidgets.material;
using Unity.UIWidgets.scheduler;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;

namespace NWakaTest
{
    public class TabView : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new TabWidgets()
            );
        }
    }

    class TabWidgets : StatelessWidget
    {
        public override Widget build(BuildContext context)
        {
            return new DefaultTabController(
                length: 3,
                child: new Scaffold(
                    backgroundColor: Colors.transparent,
                    appBar: new AppBar(
                        backgroundColor: Colors.transparent,
                        bottom: new TabBar(
                            indicatorColor: Colors.purple,
                            tabs: new List<Widget>
                            {
                                new Tab(child: new Text("Content1")),
                                new Tab(child: new Text("Content2")),
                                new Tab(child: new Text("Content3"))
                            }
                        )
                    ),
                    body: new TabBarView(
                        children: new List<Widget>
                        {
                            new Container(color: Colors.transparent),
                            new Container(color: Colors.blue),
                            new Container(color: Colors.orange)
                        }
                    )
                )
            );
        }
    }
}
```

実行結果はこちら。
![result_tab](https://i.imgur.com/FhfUP4d.gif)
(見れない場合はhttps://i.imgur.com/FhfUP4d.mp4)
こればっかりはAppBarとかを使用しない方法がよくわからなくてuGUIっぽさは出し切れませんでした。
uGUIにはそもそもTabなどというものが存在せず，自作もしくはアセット(あるのかは知らんけど)に頼るしかありません。実際現在個人製作中のカジュアルゲームではタブビューは自作しています。
なので，これもuGUIに溶け込んで実装できれば最高だったのですが…現時点ではこれ以上は無理でした。。
AppBarに関しては`elevation`を`0.0f`にすることによって，完全に透過させることが可能です。

```.cs
///一部抜粋
return new DefaultTabController(
                length: 3,
                child: new Scaffold(
                    backgroundColor: Colors.transparent,
                    appBar: new AppBar(
                        backgroundColor: Colors.transparent,
                        elevation: 0.0f,
                        bottom: new TabBar(
                            indicatorColor: Colors.purple,
                            tabs: new List<Widget>
                            {
                                new Tab(child: new Text("Content1")),
                                new Tab(child: new Text("Content2")),
                                new Tab(child: new Text("Content3"))
                            }
                        )
                    ),
```

こんな感じになります。
![result_tab2](https://i.imgur.com/4w7hrmm.gif)

透明にすることでまだギリギリuGUIに溶け込めるかな…？

### DropDown
それでは最後にドロップダウンボタンです。
コードは以下の通り。[Flutter公式のリファレンスドキュメント](https://api.flutter.dev/flutter/material/DropdownButton-class.html)を参考にしました。`List.map`の代用として`Linq`の`Select`を使用しています。そのままだと`IEnumerable`で返ってくるので`ToList()`しています。

```DropDown.cs
﻿using System.Collections.Generic;
using System.Linq;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;

namespace NWakaTest
{
    public class DropDown : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new DropDownWidgets()
            );
        }

        protected override void OnEnable()
        {
            FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
            base.OnEnable();
        }
    }

    class DropDownWidgets : StatefulWidget
    {
        public override State createState()
        {
            return new DropDownState();
        }
    }

    class DropDownState : State<DropDownWidgets>
    {
        private string _dropdownValue = "One";

        public override Widget build(BuildContext context)
        {
            return new DropdownButton<string>(
                value: _dropdownValue,
                icon: new Icon(Icons.arrow_downward),
                iconSize: 24,
                elevation: 16,
                style: new TextStyle(color: Colors.deepPurple),
                underline: new Container(
                    height: 2,
                    color: Colors.deepPurpleAccent
                ),
                onChanged: (newValue =>
                {
                    setState(
                        (() => _dropdownValue = newValue)
                    );
                }),
                items: new List<string>
                {
                    "One", "Two", "Three", "Four"
                }.Select(selectValue =>
                    new DropdownMenuItem<string>(
                        value: selectValue,
                        child: new Text(selectValue)
                    )
                ).ToList()
            );
        }
    }
}
```

実行結果は[こちら](https://i.imgur.com/exve7Gu.mp4)。
サイズ調整を誤ると選択肢が表示できなくなってしまいます。また，オブジェクトのどこを押しても反応するため，ふいにドロップダウンがオープンしかねません。これは当然と言えば当然。オブジェクト全体がドロップダウンボタンと化しているので，何もない透明な部分でもボタンなわけですね。
うーん，さすがにこれでは使いにくい。ということで，DropDwonクラスを以下のように書き換えます。

```DropDown.cs
    public class DropDown : UIWidgetsPanel
    {
        protected override Widget createWidget()
        {
            return new MaterialApp(
                home: new Center(
                    child: new DropDownWidgets()
                )
            );
        }

        protected override void OnEnable()
        {
            FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
            base.OnEnable();
        }
    }
```

実行結果は[こちら](https://i.imgur.com/yUvklqp.mp4)。
Centerの子としただけですが，とても使い勝手が変わります。ボタンっぽく見える部分しか反応しなくなりました。
Center以外のものでもいいと思いますが，他のものと組み合わせてレイアウトを考えてあげるとうまく使っていけそうですね。

# まとめ

Flutter製アプリをUnityでも制作できるのはもちろん，FlutterのマテリアルUIパーツをあたかもuGUIのように使うことでよりUnityのUIの幅が広がりそうですね！
ただ少し残念なのは，タッチorマウス前提という点ですかね。今回のようにただFlutterのように実装しただけではコントローラやキーボード操作はできません。コントローラからの入力に応答させたい場合は自分で記載する必要があるでしょう。

画像表示を確認して，そこでのgif表示からuGUIに混ぜて使うということを思いついて，なんとなく一通りのUI素材を試すというほんとに思いつきの塊みたいな記事ですが，まだまだ活用例や情報の少ない__UIWidgets__に少しでも興味を持ってもらえればと思います。