diff --git a/CMakeLists.txt b/CMakeLists.txt
index 889bd439a0..1008c5566e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -955,6 +955,8 @@ add_library(native STATIC
ext/native/ui/ui_context.h
ext/native/ui/ui_screen.cpp
ext/native/ui/ui_screen.h
+ ext/native/ui/ui_tween.cpp
+ ext/native/ui/ui_tween.h
ext/native/ui/view.cpp
ext/native/ui/view.h
ext/native/ui/viewgroup.cpp
diff --git a/UWP/NativeUWP/NativeUWP.vcxproj b/UWP/NativeUWP/NativeUWP.vcxproj
index f107dbabb7..9db58ee9a8 100644
--- a/UWP/NativeUWP/NativeUWP.vcxproj
+++ b/UWP/NativeUWP/NativeUWP.vcxproj
@@ -376,6 +376,7 @@
+
@@ -1290,6 +1291,7 @@
+
diff --git a/UWP/NativeUWP/NativeUWP.vcxproj.filters b/UWP/NativeUWP/NativeUWP.vcxproj.filters
index c7e48c5f7a..d0abe95c90 100644
--- a/UWP/NativeUWP/NativeUWP.vcxproj.filters
+++ b/UWP/NativeUWP/NativeUWP.vcxproj.filters
@@ -154,6 +154,9 @@
ui
+
+ ui
+
ui
@@ -590,6 +593,9 @@
ui
+
+ ui
+
ui
diff --git a/ext/native/Android.mk b/ext/native/Android.mk
index ad1271daba..955fd84d87 100644
--- a/ext/native/Android.mk
+++ b/ext/native/Android.mk
@@ -91,6 +91,7 @@ LOCAL_SRC_FILES :=\
ui/viewgroup.cpp \
ui/ui.cpp \
ui/ui_screen.cpp \
+ ui/ui_tween.cpp \
ui/ui_context.cpp \
ui/screen.cpp \
util/text/utf8.cpp \
diff --git a/ext/native/native.vcxproj b/ext/native/native.vcxproj
index aa3b268a19..751ab22e99 100644
--- a/ext/native/native.vcxproj
+++ b/ext/native/native.vcxproj
@@ -285,6 +285,7 @@
+
@@ -752,6 +753,7 @@
+
diff --git a/ext/native/native.vcxproj.filters b/ext/native/native.vcxproj.filters
index 6996479a53..73e468f8d9 100644
--- a/ext/native/native.vcxproj.filters
+++ b/ext/native/native.vcxproj.filters
@@ -329,6 +329,9 @@
thin3d
+
+ ui
+
@@ -796,6 +799,9 @@
thin3d
+
+ ui
+
diff --git a/ext/native/ui/ui_tween.cpp b/ext/native/ui/ui_tween.cpp
new file mode 100644
index 0000000000..49be6ba10d
--- /dev/null
+++ b/ext/native/ui/ui_tween.cpp
@@ -0,0 +1,31 @@
+#include "base/colorutil.h"
+#include "ui/ui_tween.h"
+#include "ui/view.h"
+
+namespace UI {
+
+uint32_t ColorTween::Current() {
+ return colorBlend(to_, from_, Position());
+}
+
+void TextColorTween::Apply(View *view) {
+ // TODO: No validation without RTTI?
+ TextView *tv = (TextView *)view;
+ tv->SetTextColor(Current());
+}
+
+void VisibilityTween::Apply(View *view) {
+ view->SetVisibility(Current());
+}
+
+Visibility VisibilityTween::Current() {
+ // Prefer V_VISIBLE over V_GONE/V_INVISIBLE.
+ float p = Position();
+ if (from_ == V_VISIBLE && p < 1.0f)
+ return from_;
+ if (to_ == V_VISIBLE && p > 0.0f)
+ return to_;
+ return p >= 1.0f ? to_ : from_;
+}
+
+} // namespace
diff --git a/ext/native/ui/ui_tween.h b/ext/native/ui/ui_tween.h
new file mode 100644
index 0000000000..70d1ad852c
--- /dev/null
+++ b/ext/native/ui/ui_tween.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include
+#include
+#include "base/timeutil.h"
+#include "ui/view.h"
+
+namespace UI {
+
+// This is the class to use in Update().
+class Tween {
+public:
+ Tween(float duration, float (*curve)(float)) : duration_(duration), curve_(curve) {
+ start_ = time_now();
+ }
+ virtual ~Tween() {
+ }
+
+ // Actually apply the tween to a view.
+ virtual void Apply(View *view) = 0;
+
+ bool Finished() {
+ return time_now() >= start_ + duration_;
+ }
+
+protected:
+ float DurationOffset() {
+ return time_now() - start_;
+ }
+
+ float Position() {
+ return curve_(std::min(1.0f, DurationOffset() / duration_));
+ }
+
+ float start_;
+ float duration_;
+ float (*curve_)(float);
+};
+
+// This is the class all tweens inherit from. Shouldn't be used directly, see below.
+template
+class TweenBase: public Tween {
+public:
+ TweenBase(Value from, Value to, float duration, float (*curve)(float) = [](float f) { return f; })
+ : Tween(duration, curve), from_(from), to_(to) {
+ }
+
+ // Use this to change the destination value.
+ // Useful when a state flips while the tween is half-way through.
+ void Divert(const Value &newTo) {
+ const Value newFrom = Current();
+
+ // Are we already part way through another transition?
+ if (!Finished()) {
+ if (newTo == to_) {
+ // Already on course. Don't change.
+ } else if (newTo == from_) {
+ // Reversing, adjust start_ to be smooth from the current value.
+ float newOffset = duration_ - DurationOffset();
+ start_ = time_now() - newOffset;
+ } else {
+ // Otherwise, start over.
+ start_ = time_now();
+ }
+ } else {
+ // Already finished, so restart.
+ start_ = time_now();
+ }
+
+ from_ = newFrom;
+ to_ = newTo;
+ }
+
+ // Stop animating the value.
+ void Stop() {
+ Reset(Current());
+ }
+
+ // Use when the value is explicitly reset. Implicitly stops the tween.
+ void Reset(const Value &newFrom) {
+ from_ = newFrom;
+ to_ = newFrom;
+ }
+
+protected:
+ virtual Value Current() = 0;
+
+ Value from_;
+ Value to_;
+};
+
+// Generic - subclass this for specific color handling.
+class ColorTween : public TweenBase {
+public:
+ using TweenBase::TweenBase;
+
+protected:
+ uint32_t Current() override;
+};
+
+class TextColorTween : public ColorTween {
+public:
+ using ColorTween::ColorTween;
+
+ void Apply(View *view) override;
+};
+
+class VisibilityTween : public TweenBase {
+public:
+ using TweenBase::TweenBase;
+
+ void Apply(View *view) override;
+
+protected:
+ Visibility Current() override;
+};
+
+} // namespace