目录
[TOC]
一、概述
1.UniRx作用
UniRx:Unity Reactive Extensions——响应式扩展
- 简洁优雅得实现异步逻辑
- 优雅实现MVP/MVC模式
- 对UGUI/UnityAPI提供增强
- 提高编码效率
2.基本语法格式
Observable.Timer().Subscribe().AddTo(继承自IDisposable接口的对象)
其中:
Observable
:可被观察的,UniRx中固定的开头Timer
:发布者,也被称作数据源/事件源Subscribe
:订阅者AddTo
:绑定销毁的生命周期
二、快速入门
1.计时器
(1)常规计时器
public class CommonTimerExample : MonoBehaviour
{
private float mStartTime;
private void Start()
{
mStartTime = Time.time;
}
private void Update()
{
if (Time.time - mStartTime >= 3f)
{
Debug.Log("Do Something");
mStartTime = float.MaxValue;
}
}
}
(2)Unity协程下的计时器
public class CoroutineTimerExample : MonoBehaviour
{
private void Start()
{
StartCoroutine(TimeTask(3.0f, () =>
{
Debug.Log("Do Something");
}));
}
IEnumerator TimeTask(float time, Action callback)
{
yield return new WaitForSeconds(time);
callback?.Invoke();
}
}
(3)UniRx实现计时器
Observable.Timer(TimeSpan.FromSeconds(时间)).Subscribe(_={计时任务逻辑})
public class UniRxTimerExample : MonoBehaviour
{
private void Start()
{
Observable.Timer(TimeSpan.FromSeconds(3.0f))
.Subscribe(_ =>
{
Debug.Log("Do Something");
});
}
}
2.独立的Update
UniRx比较初级的使用:使用UniRx注册事件以避免在Update中增加逻辑。
Observable.EveryUpdate().Subscribe(_=>{待注册的逻辑})
public class UpdateExample : MonoBehaviour
{
enum ButtonState
{
None,
Clicked,
}
private void Start()
{
bool mouseClicked = false;
ButtonState buttonState = ButtonState.None;
//使用UniRx注册事件以避免在Update中增加逻辑
Observable.EveryUpdate()
.Subscribe(_ =>
{
if (Input.GetMouseButtonDown(0))
{
Debug.Log("鼠标左键点击");
mouseClicked = true;
}
});
Observable.EveryUpdate()
.Subscribe(_ =>
{
if (Input.GetMouseButtonDown(1))
{
Debug.Log("鼠标右键点击");
mouseClicked = true;
}
});
Observable.EveryUpdate()
.Subscribe(_ =>
{
if (mouseClicked && buttonState == ButtonState.None)
{
buttonState = ButtonState.Clicked;
Debug.Log("按钮点击状态切换为:" + buttonState);
}
});
}
}
3.UniRx的API:AddTo
AddTo概述:
- 一个静态扩展关键字,对接口IDisposable进行了扩展;
- 只要实现了此接口,都可以使用AddTo;
- 当AddTo的目标销毁的时候,就会调用IDisposable的
OnDispose
方法。
AddTo作用:
- 将GameObject或MonoBehaviour销毁与UniRx绑定。
- 当GameObjec或MonoBehaviour被销毁的时候,UniRx的相关任务也会被销毁,防止产生空引用异常,增加UniRx使用的安全性。
代码:
Observable.Timer(TimeSpan.FromSeconds(2f))
.Subscribe()
.AddTo(this);//也可以是gameObject
当this脚本被销毁时,Timer同时被销毁,防止空引用异常。
4.操作符
UniRx中使用Observable.xxxx()
会开启一条事件流(数据源/事件源),
然后使用一系列操作符对事件流进行处理、组织、整理,
最后进行订阅和生命周期绑定。
(1)Where
作用:对数据源(发布者)进行过滤;位于发布者和订阅者之间。
public class WhereExample : MonoBehaviour
{
private void Start()
{
Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0))
.Subscribe(_ =>
{
Debug.Log("鼠标左键点击");
})
.AddTo(this);
}
}
(2)First
作用:对数据源(发布者)进行过滤;获取满足First内部条件的数据源中的第一个数据;可替代Where。
private void Start()
{
Observable.EveryUpdate()// 数据源/事件源/事件流 发布者
.First(_ => Input.GetMouseButtonDown(0))// 处理、组织、整理
.Subscribe(_ => Debug.Log("第一次点击鼠标左键"))// 订阅者
.AddTo(this);// 生命周期绑定
}
(3)Select
作用:映射事件流,将事件流中的事件/数据映射为其他值(类型可改变)
//将点击按钮A的事件映射为字符串A
btnA.OnClickAsObservable().Select(_ => "A");
(4)Merge
作用:合并事件流
public class MergeExample : MonoBehaviour
{
private void Start()
{
//开启一条鼠标点击事件流
var leftClickEventStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
//开启一条新的鼠标点击事件流
var rightClickEventStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(1));
//合并事件流
Observable.Merge(leftClickEventStream, rightClickEventStream)
.Subscribe(_ => Debug.Log("点击鼠标"));
}
}
(5)WhenAll
作用:监听所有事件流都执行完毕的时候
例子:监听每个按钮都点击一次:
//监听所有按钮点击一次
public class AllBtnClickOneTime : MonoBehaviour
{
private void Start()
{
Button btnA = transform.Find("BtnA").GetComponent<Button>();
Button btnB = transform.Find("BtnB").GetComponent<Button>();
Button btnC = transform.Find("BtnC").GetComponent<Button>();
var aStream = btnA.OnClickAsObservable().First();
var bStream = btnB.OnClickAsObservable().First();
var cStream = btnC.OnClickAsObservable().First();
Observable.WhenAll(aStream, bStream, cStream)
.Subscribe(_ => Debug.Log("按钮均点击过一次"));
}
}
5.对UGUI的支持
(1)Button
button.OnClickAsObservable()
.First()
.Subscribe(_ => Debug.Log("第一次点击按钮"));
(2)Toggle
toggle.OnValueChangedAsObservable()
.Where(on => on)
.Subscribe(_ => Debug.Log("开关为True"));
(3)Scrollbar
scrollbar.OnValueChangedAsObservable()
.Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue));
(4)ScrollRect
scrollRect.OnValueChangedAsObservable()
.Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue);
(5)Slider
slider.OnValueChangedAsObservable()
.Subscribe(sliderValue =>Debug.Log("Slider Value " + sliderValue));
(6)InputField
mInputField.OnEndEditAsObservable()
.Subscribe(result =>Debug.Log("Result :" + result));
(7)Triggers——拖拽事件
使用需导入UniRx.Triggers命名空间。具体API详见ObservableTriggerExtensions.Component.cs。
image.OnBeginDragAsObservable()
.Subscribe(_ => Debug.Log("Begin Drag"));
image.OnDragAsObservable()
.Subscribe(_ => Debug.Log("Dragging"));
image.OnEndDragAsObservable()
.Subscribe(_ => Debug.Log("End Drag"));
(8)Text:SubscribeToText
//当 inputField 的输入值改变,则会马上显示在 resultText 上
Text resultText = GetComponent<Text>();
inputField.OnValueChangedAsObservable()
.SubscribeToText(resultText);
(9)Interactable:SubscribeToInteractable
//IsDead为True时,设置atkBtn为不可交互
mEnemy.IsDead.Where(isDead => isDead)
.Select(isDead => !isDead)
.SubscribeToInteractable(atkBtn);
6.ReactiveProperty——响应式属性
很多时候,当某个变量的值改变时,内部或外部需要执行相关逻辑,通常此时会将变量用属性封装,然后使用委托或事件供内部或外部注册值改变时候的逻辑。
UniRx提供ReactiveProperty响应式属性,以减少实现上述需求的代码量。
public class ReactivePropertyExample : MonoBehaviour
{
//IntReactiveProperty:可序列化的响应式属性
public IntReactiveProperty Age = new IntReactiveProperty(25);
private void Start()
{
//注册的时候,会自动执行一次内部逻辑
Age.Subscribe(age => Debug.Log("内部订阅Age改变时候需要执行的逻辑,此时Age:" + age));
Age.Value = 26;
new PersonA().Init(this);
}
}
public class PersonA
{
public void Init(ReactivePropertyExample reactivePropertyExample)
{
//注册的时候,会自动执行一次内部逻辑
reactivePropertyExample.Age.Subscribe(age => Debug.Log("外部订阅Age改变时候需要执行的逻辑,此时Age:" + age));
reactivePropertyExample.Age.Value = 27;
}
}
运行结果:
7.简单的MVP模式实现
MV(R)P模式全称:Model—View—(Reactive)Presenter
代码:
public class MVPExample : MonoBehaviour
{
EnemyModel mEnemy;
//Unity中Hierarchy下的UI为View部分
//Start内部为Presenter部分
//EnemyModel为Model部分
private void Start()
{
Button atkBtn = transform.Find("Button")
.GetComponent<Button>();
Text hpText = transform.Find("Text")
.GetComponent<Text>();
mEnemy = new EnemyModel(100);
//将UI控件与Model绑定
atkBtn.OnClickAsObservable().Subscribe(_ =>
{
mEnemy.Hp.Value -= 40;
});
mEnemy.Hp.SubscribeToText(hpText);
mEnemy.IsDead.Where(isDead => isDead)
.Select(isDead => !isDead)
.SubscribeToInteractable(atkBtn);
}
}
public class EnemyModel
{
public IntReactiveProperty Hp;
public IReadOnlyReactiveProperty<bool> IsDead;
public EnemyModel(int initialHp)
{
Hp = new IntReactiveProperty(initialHp);
IsDead = Hp.Select(hp => hp <= 0)
.ToReactiveProperty();
}
}
8.综合以上的一个小练习:点击某一个按钮后,所有按钮不再能点击,且2秒后隐藏所有按钮
public class EventsMaskExample : MonoBehaviour
{
private void Start()
{
Button btnA = transform.Find("ButtonA").GetComponent<Button>();
Button btnB = transform.Find("ButtonB").GetComponent<Button>();
Button btnC = transform.Find("ButtonC").GetComponent<Button>();
var btnAStream = btnA.OnClickAsObservable().Select(_ => "A");
var btnBStream = btnB.OnClickAsObservable().Select(_ => "B");
var btnCStream = btnC.OnClickAsObservable().Select(_ => "C");
Observable.Merge(btnAStream, btnBStream, btnCStream)
.First()
.Subscribe(btnCode =>
{
switch (btnCode)
{
case "A":
Debug.Log("点击按钮A");
break;
case "B":
Debug.Log("点击按钮B");
break;
case "C":
Debug.Log("点击按钮C");
break;
}
Observable.Timer(TimeSpan.FromSeconds(2f))
.Subscribe(_ => gameObject.SetActive(false));
});
}
}
9.Unity生命周期
(1)EveryFixedUpdate
Observable.EveryFixedUpdate().Subscribe(_ => {});
(2)EveryEndOfFrame
Observable.EveryEndOfFrame().Subscribe(_ => {});
(3)EveryLateUpdate
Observable.EveryLateUpdate().Subscribe(_ => {});
(4)EveryAfterUpdate
Observable.EveryAfterUpdate().Subscribe(_ => {});
(5)EveryApplicationPause
Observable.EveryApplicationPause().Subscribe(paused => {});
(6)EveryApplicationFocus
Observable.EveryApplicationFocus().Subscribe(focused => {});
(7)EveryApplicationQuit
Observable.EveryApplicationQuit().Subscribe(_ => {}):
10.UniRx的Trigger
Observable.EveryUpdate()
这个 API 有的时候在某个脚本中实现,需要绑定 MonoBehaviour 的⽣命周期(主要是 OnDestroy),当然也有的时候是全局的,而且永远不会被销毁的。
需要绑定 MonoBehaviour 生命周期的 EveryUpdate。只需要一个 AddTo
就可以进行绑定了。
Observable.EveryUpdate()
.Subscribe(_ => {})
.AddTo(this);
但其实有更简洁的实现:
this.UpdateAsObservable()
.Subscribe(_ => {});
这种类型的 Observable 是Trigger,即触发器器。
触发器,字如其意,是当某个事件发生时,则会将该事件发送到Subscribe
函数中,而这个触发器器,本身是一个功能脚本,这个脚本挂在 GameObject 上,来监听 GameObject 的某个事件发生,事件发生则会回调给注册它的 Subscribe 中。
触发器的操作和其他的事件源 (Observable) 是一样的,都支持 Where、First、Merge 等操作符。
Trigger类型的 Observable 和之前所有的Observable在表现上有些不一样:
-
1.Trigger⼤大部分都是都是XXXAsObsrevable命名形式的。
-
2.在使用Trigger的GameObject上都会挂上对应的Observable XXXTrigger.cs的脚本。
AddTo()
这个 API 其实是封装了了一种Trigger: ObservableDestroyTrigger
。ObservableDestroyTrigger就是当 GameObject 销毁时获取事件的一个触发器。一般的Trigger都会配合MonoBehaviour一起使用。 ObservableDestroyTrigger的使用代码如下:
this.OnDestroyAsObservable() .Subscribe(_ => {});
不同类型的Trigger:
-
1.各种细分类型的Update
this.FixedUpdateAsObservable().Subscribe(_ => {});
this.UpdateAsObservable().Subscribe(_ => {});
this.LateUpdateAsObservable().Subscribe(_ => {});
-
2.各种碰撞的Trigger
this.OnCollisionEnterAsObservable(collision => {});
this.OnCollisionStayAsObservable(collision => {});
this.OnCollisionExitAsObservable(collision => {});
-
3.一些脚本的参数监听
this.OnEnableAsObservable().Subscribe(_ => {});
this.OnDisableAsObservable().Subscribe(_ => {});
-
4.详情可以查看ObservableTriggerExtensions.cs和ObervableTriggerExtensions.Component.cs中的API。
11.UniRx的协程
(1)Unity的Coroutine与UniRx的协程
API:.FromCoroutine()
public class CoroutineExample : MonoBehaviour
{
private void Start()
{
//Unity的Coroutine:
StartCoroutine(CoroutineA("Unity的Coroutine"));
//UniRx的
Observable.FromCoroutine(_ => CoroutineA("UniRx的Coroutine"))
.Subscribe(_ => Debug.Log("协程执行完之后,再执行此处逻辑"));
}
IEnumerator CoroutineA(string content)
{
yield return new WaitForSeconds(1f);
Debug.Log(content);
}
}
运行结果:
(2)Observable与Yield对象之间的相互转化
API:.ToYieldInstruction()
public class Observable2YieldExample : MonoBehaviour
{
private void Start()
{
StartCoroutine(CoroutineA());
}
IEnumerator CoroutineA()
{
//将Observable转化为Yield对象
yield return Observable.Timer(TimeSpan.FromSeconds(1.0f))
.ToYieldInstruction();
Debug.Log("1秒后执行");
}
}
(3)WhenAll:Coroutine的并行操作
public class WhenAllExample : MonoBehaviour
{
IEnumerator A()
{
yield return new WaitForSeconds(1.0f);
Debug.Log("A");
}
IEnumerator B()
{
yield return new WaitForSeconds(2.0f);
Debug.Log("B");
}
private void Start()
{
var aStream = Observable.FromCoroutine(A);
var bStream = Observable.FromCoroutine(B);
//WhenAll类似于Merge操作符:用于操作事件流,监听所有事件流都执行完毕的时候
Observable.WhenAll(aStream, bStream)
.Subscribe(_ => Debug.Log("When All执行"));
}
}
运行结果:
12.UniRx中Observable的生命周期
结束时候的生命周期:OnNext => OnCompleted
出错时候的生命周期:OnError
- UniRx中除了EveryUpdate的事件流,其他事件流都拥有OnNext和OnCompleted的生命周期:其中OnNext会在结束时先执行,之后执行OnCompleted。
- 在EveryUpdate中OnNext会在每帧都执行。当EveryUpdate与First操作符共同使用的时候,EveryUpdate将拥有OnNext和OnCompleted生命周期。
测试代码:
public class OnCompletesExample : MonoBehaviour
{
private void Start()
{
//Timer有OnNext、OnCompleted生命周期
Observable.Timer(TimeSpan.FromSeconds(2f))
.Subscribe(_ =>
{
Debug.Log("OnNext:Timer--after 2 second");
}, () =>
{
Debug.Log("OnCompleted:Timer");
});
//EveryUpdate没有OnCompleted生命周期
//加上First操作符后就有了OnCompleted生命周期
Observable.EveryUpdate().First()
.Subscribe(_ =>
{
Debug.Log("OnNext:EveryUpdate--First");
}, () =>
{
Debug.Log("OnCompleted:EveryUpdate--First");
});
//FromCoroutine的OnNext和OnCompleted都是在协程的yield之后执行
Observable.FromCoroutine(CoroutineA)
.Subscribe(_ =>
{
Debug.Log("OnNext:FromCoroutine--after 3 second");
}, () =>
{
Debug.Log("OnCompleted--FromCoroutine");
});
}
IEnumerator CoroutineA()
{
yield return new WaitForSeconds(3f);
Debug.Log("协程");
}
}
运行结果:
13.UniRx中的多线程
(1)Start 开启多线程
API:Observable.Start()
(2)ObserveOnMainThread 将对线程转入主线程
API:ObserveOnMainThread()
注意:各个线程中的返回值将被Subscribe按WhenAll中参数的顺序接收
测试代码:
public class ThreadExample : MonoBehaviour
{
private void Start()
{
//使用Start开启线程
var threadAStream = Observable.Start(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(5));
return "a";
});
var threadBStream = Observable.Start(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return "b";
});
//使用ObserveOnMainThread将线程转入到主线程中
//各个线程中的返回值将被Subscribe按WhenAll中参数的顺序接收
Observable.WhenAll(threadAStream, threadBStream)
.ObserveOnMainThread()
.Subscribe(results => Debug.LogFormat("threadAStream:{0};threadBStream:{1}", results[0], results[1]));
}
}
运行结果:
14.UniRx中处理网络请求操作:ObservableWWW
(1)Get
-
单个Get获取URL资源:
//使用WWW请求、获得URL上的(Text)信息 ObservableWWW.Get(@"https://huskytgame.github.io/") .Subscribe(responseText => { Debug.LogFormat("Response Text:{0}", responseText.Substring(0, 100)); }, exception => { Debug.LogErrorFormat("异常:{0}", exception); });
-
和WhenAll一起使用
var wwwAStream = ObservableWWW.Get(@"https://github.com/HuskyTGame"); var wwwBStream = ObservableWWW.Get(@"https://aaaaaahuskytgame.github.io/"); Observable.WhenAll(wwwAStream, wwwBStream) .Subscribe(responseTexts => { Debug.LogFormat("wwwAStream:{0}", responseTexts[0]); Debug.LogFormat("wwwBStream:{0}", responseTexts[1]); }, exception => { Debug.LogErrorFormat("异常:{0}", exception); });
运行结果:
(2)Post
(3)GetWWW:获取WWW
(4)PostWWW
(5)GetAnGetBytes:下载二进制数据
可以使用ScheduledNotifier(工作计划通告器)来获取下载进度信息:
private string mUrl = @"https://txxs.mahua-yongjiu.com/20191219/8388_caa7a87a/1000k/hls/88a4c76b152000000.ts";
private void Start()
{
var progressListener = new ScheduledNotifier<float>();
ObservableWWW.GetAndGetBytes(mUrl, null, progressListener)
.Subscribe(bytes =>
{
Debug.LogFormat("下载的资源:{0}", bytes[0].ToString());
});
progressListener.Subscribe(progress =>
{
Debug.LogFormat("下载进度:{0}/1", progress);
});
}
运行结果:
15.ReactiveCommand:响应式命令
ReactiveCommand内部包含有一个只读的响应式属性CanExecute,该属性决定了响应式命令是否可执行。
该属性默认为true,但可以通过在构造响应式命令时传入适当的Observable来控制CanExecute。
例子1:
//默认CanExecute一直为True
//CanExecute为只读响应式属性,只受构造时传入的Observable影响
var reactiveCommand = new ReactiveCommand();
//为响应式命令注册回调
reactiveCommand.Subscribe(_ =>
{
Debug.Log("Execute");
});
//执行命令
Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0))
.Subscribe(_ =>
{
reactiveCommand.Execute();
});
例子2:鼠标按下事件的响应:
var mouseUpStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonUp(0))
.Select(_ => false);
var mouseDownStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0))
.Select(_ => true); ;
var onMouseDownStream = Observable.Merge(mouseUpStream, mouseDownStream);
//创建响应式命令
//设置初始CanExecute属性为false
var reactiveCommand = new ReactiveCommand(onMouseDownStream, false);
//注册事件
reactiveCommand.Subscribe(_ =>
{
Debug.Log("鼠标按下");
});
//将命令放入Update中监听
Observable.EveryUpdate()
.Subscribe(_ =>
{
reactiveCommand.Execute();
});
例子3:使用Where操作符,判断奇偶数
//创建命令
var reactiveCommand = new ReactiveCommand<int>();
//使用Where操作符
reactiveCommand.Where(x => x % 2 == 0)
.Subscribe(x => Debug.LogFormat("{0}是偶数", x));
//加上时间戳
reactiveCommand.Where(x => x % 2 == 1)
.Timestamp()
.Subscribe(x => Debug.LogFormat("{0}是奇数,当前时间{1}", x.Value, x.Timestamp));
reactiveCommand.Execute(9);
reactiveCommand.Execute(10);
运行结果:
16.ReactiveCollection和ReactiveDictionary:响应式集合与响应式字典
(1)ReactiveCollection
- 添加:
ObserveAdd()
- 移除:
ObserveRemove()
- 个数改变:
ObserveCountChanged()
- 移动:
ObserveMove()
- 替换:
ObserveReplace()
- 重置:
ObserveReset()
private ReactiveCollection<string> mUrls = new ReactiveCollection<string>
{
"huskytgame.io",
"baidu.com",
"100524.com",
"mooc.cn"
};
private void Start()
{
//添加
mUrls.ObserveAdd()
.Subscribe(value => Debug.LogFormat("添加的值为:{0};添加的值的索引为:{1}", value.Value, value.Index));
//移除
mUrls.ObserveRemove()
.Subscribe(value => Debug.LogFormat("删除的值为:{0};删除的值的索引为:{1}", value.Value, value.Index));
//个数改变
mUrls.ObserveCountChanged()
.Subscribe(count => Debug.LogFormat("此时List中元素个数:{0}", count));
//移动
mUrls.ObserveMove()
.Subscribe(value => Debug.LogFormat("移动的元素为:{0},从索引{1}移动到索引{2}", value.Value, value.OldIndex, value.NewIndex));
//替换
mUrls.ObserveReplace()
.Subscribe(value => Debug.LogFormat("替换索引为{0}的元素{1}为{2}:{0}", value.Index, value.OldValue, value.NewValue));
//重置
mUrls.ObserveReset()
.Subscribe(_ => Debug.LogFormat("重置List"));
mUrls.Add("twgame.cn");
mUrls.Remove("100524.com");
mUrls.RemoveAt(1);
mUrls.Move(0, 2);
mUrls[0] = "Hare.com";
mUrls.Clear();
}
运行结果:
(2)ReactiveDictionary
用法同ReactiveCollection
- 添加:
ObserveAdd()
- 移除:
ObserveRemove()
- 个数改变:
ObserveCountChanged()
- 移动:
ObserveMove()
- 替换:
ObserveReplace()
- 重置:
ObserveReset()
17.AsyncOperation:异步操作
UniRx可以很容易监听异步加载进度。
加载Prefab:
//异步加载资源
Resources.LoadAsync<GameObject>("Prefabs/AsyncOperationTestCanvas")
.AsAsyncOperationObservable()
.Subscribe(resourcesRequest =>
{
Instantiate(resourcesRequest.asset);
});
加载场景,并监听加载进度:
//异步加载场景
var progressObservable = new ScheduledNotifier<float>();
SceneManager.LoadSceneAsync(0)
.AsAsyncOperationObservable(progressObservable)
.Subscribe(_ =>
{
Debug.Log("场景加载完毕");
});
progressObservable.Subscribe(progress =>
{
Debug.LogFormat("场景加载进度:{0}", progress);
});
运行结果:
三、TodoList实战
效果演示:
Hierarchy:
/****************************************************
文件:Model.cs
作者:HuskyT
邮箱:1005240602@qq.com
日期:2019/12/22 23:31:28
功能:TodoList的Model层
*****************************************************/
using System;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
namespace StudyUniRx.TodoList
{
[Serializable]
public class TodoItem
{
/// <summary>
/// 待办事项标识
/// </summary>
public int Id;
/// <summary>
/// 待办事项列表
/// </summary>
public StringReactiveProperty Content;
/// <summary>
/// 待办事项是否完成
/// </summary>
public BoolReactiveProperty Completed;
}
[Serializable]
public class TodoList
{
private const string SAVE_KEY = "Model";
private int mTopId = 2;
public List<TodoItem> TodoItems = new List<TodoItem>
{
new TodoItem
{
Id = 0,
Content = new StringReactiveProperty("完成UniRx学习任务"),
Completed = new BoolReactiveProperty(false),
},
new TodoItem
{
Id = 1,
Content = new StringReactiveProperty("完成UniRx的笔记整理"),
Completed = new BoolReactiveProperty(false),
}
};
public void Add(string content)
{
TodoItems.Add(new TodoItem
{
Id = mTopId,
Completed = new BoolReactiveProperty(false),
Content = new StringReactiveProperty(content),
});
mTopId %= int.MaxValue;
mTopId++;
}
public void Save()
{
PlayerPrefs.SetString(SAVE_KEY, JsonUtility.ToJson(this));
}
public static TodoList Load()
{
string content = PlayerPrefs.GetString(SAVE_KEY, string.Empty);
if (string.IsNullOrEmpty(content))
{
return new TodoList();
}
else
{
return JsonUtility.FromJson<TodoList>(content);
}
}
}
}
/****************************************************
文件:UIInputCtrl.cs
作者:HuskyT
邮箱:1005240602@qq.com
日期:2019/12/23 20:43:8
功能:输入控制部分
*****************************************************/
using UnityEngine;
using UnityEngine.UI;
using UniRx;
namespace StudyUniRx.TodoList
{
public class UIInputCtrl : MonoBehaviour
{
public enum Mode
{
/// <summary>
/// 非编辑模式
/// </summary>
NonEdit,
/// <summary>
/// 编辑模式
/// </summary>
Edit,
}
#region UI组件
private InputField mInputContent;
private Button mBtnAdd;
private Button mBtnUpdate;
private Button mBtnCancel;
#endregion
#region Model
public TodoList Model;
#endregion
public ReactiveProperty<Mode> InputMode;
private void Awake()
{
mInputContent = transform.Find("InputContent").GetComponent<InputField>();
mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
mBtnUpdate = transform.Find("BtnUpdate").GetComponent<Button>();
mBtnCancel = transform.Find("BtnCancel").GetComponent<Button>();
mBtnCancel.gameObject.SetActive(false);
InputMode = new ReactiveProperty<Mode>(Mode.NonEdit);//初始为非编辑模式
}
private void Start()
{
//输入框只有在有输入的时候,添加按钮才可交互
mInputContent.OnValueChangedAsObservable()
.Select(editContent => !string.IsNullOrEmpty(editContent))
.Subscribe(hasInput =>
{
mBtnAdd.interactable = hasInput;
mBtnCancel.gameObject.SetActive(hasInput);
InputMode.Value = hasInput ? Mode.Edit : Mode.NonEdit;//赋值输入模式
});
//只有输入框内容变化的时候,更新按钮才可以交互
mInputContent.OnValueChangedAsObservable()
.Select(editContent => mCacheTodoItem != null && editContent != mCacheTodoItem.Content.Value && !string.IsNullOrEmpty(editContent))
.SubscribeToInteractable(mBtnUpdate);
//注册Add按钮的监听
mBtnAdd.OnClickAsObservable()
.Subscribe(_ =>
{
Model.Add(mInputContent.text);
mInputContent.text = string.Empty;//输入框重置
});
//注册Update按钮的监听
mBtnUpdate.OnClickAsObservable()
.Subscribe(_ =>
{
mCacheTodoItem.Content.Value = mInputContent.text;
NonEditMode();
});
//注册Cancel按钮的监听
mBtnCancel.OnClickAsObservable()
.Subscribe(_ =>
{
NonEditMode();
});
}
public void NonEditMode()
{
mInputContent.text = string.Empty;//更新后清空输入框
mCacheTodoItem = null;//TodoItem缓存置空
mBtnAdd.gameObject.SetActive(true);
mBtnUpdate.gameObject.SetActive(false);
mBtnCancel.gameObject.SetActive(false);
}
private TodoItem mCacheTodoItem;
public void EditMode(TodoItem todoItem)
{
mCacheTodoItem = todoItem;//缓存TodoItem
mInputContent.text = todoItem.Content.Value;
mBtnAdd.gameObject.SetActive(false);
mBtnUpdate.gameObject.SetActive(true);
mBtnCancel.gameObject.SetActive(true);
}
}
}
/****************************************************
文件:UITodoList.cs
作者:HuskyT
邮箱:1005240602@qq.com
日期:2019/12/23 15:28:47
功能:管理整个TodoList(挂载在Canvas上)
*****************************************************/
using System.Linq;
using UnityEngine;
using UniRx;
using System.Collections.Generic;
namespace StudyUniRx.TodoList
{
public class UITodoList : MonoBehaviour
{
#region UI组件
[SerializeField]
private Transform mContent;//ScrollView下的Content
private UIInputCtrl mUIInputCtrl;
private GameObject mUIEventMask;//遮罩
#endregion
#region Model
private TodoList mModel = new TodoList();
#endregion
private UITodoItem mUITodoItemPrototype;
private void Awake()
{
mUITodoItemPrototype = transform.Find("UITodoItemPrototype").GetComponent<UITodoItem>();
mUIInputCtrl = transform.Find("InputCtrl").GetComponent<UIInputCtrl>();
mUIEventMask = transform.Find("EventMask").gameObject;
mModel = TodoList.Load();//加载Model
mUIInputCtrl.Model = mModel;//注入Model
}
private void Start()
{
//注册待办事项列表Count变化时的监听
mModel.TodoItems.ObserveEveryValueChanged(todoItems => todoItems.Count)
.Subscribe(_ => OnDataChanged());
//注册事件遮罩,响应输入模式的变化
mUIInputCtrl.InputMode.Subscribe(mode =>
{
switch (mode)
{
case UIInputCtrl.Mode.NonEdit:
mUIEventMask.SetActive(false);
break;
case UIInputCtrl.Mode.Edit:
mUIEventMask.SetActive(true);
break;
default:
break;
}
});
//刷新一次TodoItem
OnDataChanged();
}
/// <summary>
/// 当TodoItem的Model改变时调用
/// </summary>
private void OnDataChanged()
{
ClearAllTodoItem(mContent);
List<TodoItem> todoItems = mModel.TodoItems;
todoItems.Where(todoItem => todoItem.Completed.Value == false)
.ToList()
.ForEach(todoItem =>
{
todoItem.Completed.Subscribe(completed =>
{
//只有当完成的时候才执行OnDataChanged
if (completed)
{
OnDataChanged();
}
});
UITodoItem uiTodoItem = SpawnUITodoItem(mUITodoItemPrototype, todoItem, mContent);
uiTodoItem.BtnSelf.OnClickAsObservable()
.Subscribe(_ =>
{
mUIInputCtrl.EditMode(todoItem);
});
});
mModel.Save();//存储
}
private UITodoItem SpawnUITodoItem(UITodoItem uiTodoItemPrototype, TodoItem model, Transform parent)
{
UITodoItem uiTodoItem = Instantiate(uiTodoItemPrototype);
uiTodoItem.SetActive(true);
uiTodoItem.SetParent(parent);
uiTodoItem.ResetScale();
uiTodoItem.SetModel(model);
return uiTodoItem;
}
/// <summary>
/// 清除所有Content下的TodoItem
/// </summary>
/// <param name="content"></param>
private void ClearAllTodoItem(Transform content)
{
int itemCount = content.childCount;
for (int i = 0; i < itemCount; i++)
{
Destroy(content.GetChild(i).gameObject);
}
}
}
}
/****************************************************
文件:UITodoItem.cs
作者:HuskyT
邮箱:1005240602@qq.com
日期:2019/12/23 12:21:36
功能:TodoItem——UI组件
*****************************************************/
using UnityEngine;
using UnityEngine.UI;
using UniRx;
namespace StudyUniRx.TodoList
{
public class UITodoItem : MonoBehaviour
{
#region UI组件
private Text mTxtContent;
private Button mBtnCompleted;
public Button BtnSelf;
#endregion
#region Model
private TodoItem mModel;
#endregion
private void Awake()
{
mTxtContent = transform.Find("TxtContent").GetComponent<Text>();
mBtnCompleted = transform.Find("BtnCompleted").GetComponent<Button>();
BtnSelf = transform.GetComponent<Button>();
mBtnCompleted.OnClickAsObservable()
.Subscribe(_ => mModel.Completed.Value = true);
}
public void ResetScale()
{
transform.localScale = Vector3.one;
}
public void SetParent(Transform parent)
{
transform.SetParent(parent);
}
public void SetActive(bool activate)
{
gameObject.SetActive(activate);
}
public void SetModel(TodoItem model)
{
mModel = model;
UpdateView(model.Content.Value);
mModel.Content.Subscribe(UpdateView)
.AddTo(this);
}
#region Private Function
private void UpdateView(string content)
{
mTxtContent.text = content;
}
#endregion
}
}