craft-native 0.0.234 → 0.0.236
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -8,6 +8,7 @@ import android.content.Context
|
|
|
8
8
|
import android.content.pm.PackageManager
|
|
9
9
|
import android.content.res.ColorStateList
|
|
10
10
|
import android.content.res.Configuration
|
|
11
|
+
import android.content.res.Resources
|
|
11
12
|
import android.database.ContentObserver
|
|
12
13
|
import android.content.Intent
|
|
13
14
|
import android.app.PictureInPictureParams
|
|
@@ -865,8 +866,146 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
865
866
|
private fun color(value: String): Int = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
|
|
866
867
|
}
|
|
867
868
|
|
|
869
|
+
/** A finite celebration owned by a View, never by the JavaScript frame loop. */
|
|
870
|
+
internal class CraftNativeCelebration(private val view: View) {
|
|
871
|
+
private var request: Pair<Double, Boolean>? = null
|
|
872
|
+
private var pending = false
|
|
873
|
+
private var animator: ValueAnimator? = null
|
|
874
|
+
private var decoration: Drawable? = null
|
|
875
|
+
private var root: View? = null
|
|
876
|
+
private var radius = 0f
|
|
877
|
+
var starts = 0
|
|
878
|
+
private set
|
|
879
|
+
val isRunning: Boolean get() = animator != null
|
|
880
|
+
|
|
881
|
+
fun configure(raw: JSONObject?, radius: Float) {
|
|
882
|
+
val replay = craftNativeNumber(raw?.opt("replay"))?.takeIf { it >= 0 }
|
|
883
|
+
val next = replay?.let { it to (raw?.opt("enabled") != false) }
|
|
884
|
+
this.radius = radius
|
|
885
|
+
if (next == request) return
|
|
886
|
+
stop()
|
|
887
|
+
request = next
|
|
888
|
+
pending = next?.second == true
|
|
889
|
+
view.requestLayout()
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
fun stop() {
|
|
893
|
+
pending = false
|
|
894
|
+
animator?.removeAllUpdateListeners()
|
|
895
|
+
animator?.removeAllListeners()
|
|
896
|
+
animator?.cancel()
|
|
897
|
+
animator = null
|
|
898
|
+
decoration?.let { root?.overlay?.remove(it) }
|
|
899
|
+
decoration = null
|
|
900
|
+
root = null
|
|
901
|
+
view.clearAnimation()
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
fun layout(animationsEnabled: Boolean = ValueAnimator.areAnimatorsEnabled()) {
|
|
905
|
+
if (!pending || !view.isAttachedToWindow || !view.isShown || view.width <= 0 || view.height <= 0) return
|
|
906
|
+
pending = false
|
|
907
|
+
if (!animationsEnabled) return
|
|
908
|
+
starts += 1
|
|
909
|
+
val root = view.rootView
|
|
910
|
+
this.root = root
|
|
911
|
+
val at = IntArray(2)
|
|
912
|
+
val origin = IntArray(2)
|
|
913
|
+
view.getLocationInWindow(at)
|
|
914
|
+
root.getLocationInWindow(origin)
|
|
915
|
+
val frame = RectF((at[0] - origin[0]).toFloat(), (at[1] - origin[1]).toFloat(), (at[0] - origin[0] + view.width).toFloat(), (at[1] - origin[1] + view.height).toFloat())
|
|
916
|
+
val density = view.resources.displayMetrics.density
|
|
917
|
+
var progress = 0f
|
|
918
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
919
|
+
val colors = intArrayOf(Color.rgb(59, 130, 246), Color.rgb(153, 246, 228), Color.rgb(230, 196, 140), Color.WHITE)
|
|
920
|
+
val decoration = object : Drawable() {
|
|
921
|
+
override fun draw(canvas: Canvas) {
|
|
922
|
+
for (index in 0 until 52) {
|
|
923
|
+
val side = if (index % 2 == 0) -1f else 1f
|
|
924
|
+
val spread = (index * 37 % 101) / 100f
|
|
925
|
+
val t = ((progress * 3f - (index % 7) * .018f) / (2.2f + spread * .45f)).coerceIn(0f, 1f)
|
|
926
|
+
if (t <= 0 || t >= 1) continue
|
|
927
|
+
val startX = frame.centerX() + side * frame.width() * .42f
|
|
928
|
+
val startY = frame.top + 28 * density
|
|
929
|
+
val endX = frame.centerX() + side * (35 * density + spread * root.width * .7f)
|
|
930
|
+
val endY = root.height + 30 * density
|
|
931
|
+
val x = startX + (endX - startX) * t
|
|
932
|
+
val y = startY + (endY - startY) * t * t - (70 + spread * 100) * density * 4 * t * (1 - t)
|
|
933
|
+
val saved = canvas.save()
|
|
934
|
+
canvas.translate(x, y)
|
|
935
|
+
canvas.rotate(side * (index * 30f + t * (400 + spread * 600)))
|
|
936
|
+
paint.shader = null
|
|
937
|
+
paint.color = colors[index % colors.size]
|
|
938
|
+
paint.alpha = (255 * min(1f, min(t / .04f, (1 - t) / .3f))).roundToInt()
|
|
939
|
+
canvas.drawRoundRect(RectF(-3 * density, -5 * density, (if (index % 3 == 0) 1 else 4) * density, 6 * density), density, density, paint)
|
|
940
|
+
canvas.restoreToCount(saved)
|
|
941
|
+
}
|
|
942
|
+
val light = (progress * 3f - .55f) / 1.15f
|
|
943
|
+
if (light > 0 && light < 1) {
|
|
944
|
+
val saved = canvas.save()
|
|
945
|
+
val mask = Path().apply { addRoundRect(frame, radius, radius, Path.Direction.CW) }
|
|
946
|
+
canvas.clipPath(mask)
|
|
947
|
+
val x = frame.left - frame.width() + light * frame.width() * 3
|
|
948
|
+
paint.alpha = 255
|
|
949
|
+
paint.shader = LinearGradient(x - frame.width() * .25f, frame.top, x + frame.width() * .25f, frame.bottom, intArrayOf(Color.TRANSPARENT, 0x33ffffff, Color.TRANSPARENT), null, Shader.TileMode.CLAMP)
|
|
950
|
+
canvas.drawRect(frame, paint)
|
|
951
|
+
paint.shader = null
|
|
952
|
+
canvas.restoreToCount(saved)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
override fun setAlpha(alpha: Int) {}
|
|
956
|
+
override fun setColorFilter(colorFilter: ColorFilter?) {}
|
|
957
|
+
@Deprecated("Deprecated in Java")
|
|
958
|
+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
|
959
|
+
}
|
|
960
|
+
decoration.setBounds(0, 0, root.width, root.height)
|
|
961
|
+
this.decoration = decoration
|
|
962
|
+
root.overlay.add(decoration)
|
|
963
|
+
// Classic view animations leave authored transform properties intact.
|
|
964
|
+
val entrance = android.view.animation.AnimationSet(true).apply {
|
|
965
|
+
addAnimation(android.view.animation.ScaleAnimation(.86f, 1f, .86f, 1f, android.view.animation.Animation.RELATIVE_TO_SELF, .5f, android.view.animation.Animation.RELATIVE_TO_SELF, .5f))
|
|
966
|
+
addAnimation(android.view.animation.TranslateAnimation(0f, 0f, 34 * density, 0f))
|
|
967
|
+
addAnimation(android.view.animation.RotateAnimation(-2.6f, 0f, android.view.animation.Animation.RELATIVE_TO_SELF, .5f, android.view.animation.Animation.RELATIVE_TO_SELF, .5f))
|
|
968
|
+
duration = 950
|
|
969
|
+
interpolator = android.view.animation.OvershootInterpolator(.65f)
|
|
970
|
+
}
|
|
971
|
+
view.startAnimation(entrance)
|
|
972
|
+
val clock = ValueAnimator.ofFloat(0f, 1f)
|
|
973
|
+
animator = clock
|
|
974
|
+
clock.apply {
|
|
975
|
+
duration = 3000
|
|
976
|
+
interpolator = LinearInterpolator()
|
|
977
|
+
addUpdateListener {
|
|
978
|
+
if (!ValueAnimator.areAnimatorsEnabled()) { stop(); return@addUpdateListener }
|
|
979
|
+
progress = it.animatedValue as Float
|
|
980
|
+
decoration.invalidateSelf()
|
|
981
|
+
}
|
|
982
|
+
addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
983
|
+
override fun onAnimationEnd(animation: android.animation.Animator) { stop() }
|
|
984
|
+
})
|
|
985
|
+
start()
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
868
990
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
869
991
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
992
|
+
private var celebration: CraftNativeCelebration? = null
|
|
993
|
+
fun configureCelebration(raw: JSONObject?, radius: Float) {
|
|
994
|
+
if (raw != null && celebration == null) celebration = CraftNativeCelebration(this)
|
|
995
|
+
celebration?.configure(raw, radius)
|
|
996
|
+
}
|
|
997
|
+
override fun onDetachedFromWindow() {
|
|
998
|
+
celebration?.stop()
|
|
999
|
+
super.onDetachedFromWindow()
|
|
1000
|
+
}
|
|
1001
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
1002
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
1003
|
+
if (visibility != View.VISIBLE) celebration?.stop()
|
|
1004
|
+
}
|
|
1005
|
+
override fun onWindowVisibilityChanged(visibility: Int) {
|
|
1006
|
+
super.onWindowVisibilityChanged(visibility)
|
|
1007
|
+
if (visibility != View.VISIBLE) celebration?.stop()
|
|
1008
|
+
}
|
|
870
1009
|
var onLayoutChanged: (() -> Unit)? = null
|
|
871
1010
|
private val styles = mutableMapOf<View, CraftNativeLayoutStyle>()
|
|
872
1011
|
private var wrap = false
|
|
@@ -1769,6 +1908,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1769
1908
|
}
|
|
1770
1909
|
|
|
1771
1910
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
1911
|
+
celebration?.layout()
|
|
1772
1912
|
widthDefinite = true
|
|
1773
1913
|
if (grid) {
|
|
1774
1914
|
val layout = gridLayout(width, height)
|
|
@@ -3631,6 +3771,34 @@ private class CraftNativeSideBorderDrawable(
|
|
|
3631
3771
|
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
|
3632
3772
|
}
|
|
3633
3773
|
|
|
3774
|
+
/** Protects only the native three-button region; content keeps its authored background. */
|
|
3775
|
+
private class CraftNativeNavigationBarProtection : Drawable() {
|
|
3776
|
+
private val paint = Paint()
|
|
3777
|
+
private var left = 0
|
|
3778
|
+
private var right = 0
|
|
3779
|
+
private var bottom = 0
|
|
3780
|
+
|
|
3781
|
+
fun update(color: Int, left: Int, right: Int, bottom: Int) {
|
|
3782
|
+
if (paint.color == color && this.left == left && this.right == right && this.bottom == bottom) return
|
|
3783
|
+
paint.color = color
|
|
3784
|
+
this.left = left
|
|
3785
|
+
this.right = right
|
|
3786
|
+
this.bottom = bottom
|
|
3787
|
+
invalidateSelf()
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
override fun draw(canvas: Canvas) {
|
|
3791
|
+
if (left > 0) canvas.drawRect(bounds.left.toFloat(), bounds.top.toFloat(), (bounds.left + left).toFloat(), bounds.bottom.toFloat(), paint)
|
|
3792
|
+
if (right > 0) canvas.drawRect((bounds.right - right).toFloat(), bounds.top.toFloat(), bounds.right.toFloat(), bounds.bottom.toFloat(), paint)
|
|
3793
|
+
if (bottom > 0) canvas.drawRect(bounds.left.toFloat(), (bounds.bottom - bottom).toFloat(), bounds.right.toFloat(), bounds.bottom.toFloat(), paint)
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
override fun setAlpha(alpha: Int) { paint.alpha = alpha; invalidateSelf() }
|
|
3797
|
+
override fun setColorFilter(filter: ColorFilter?) { paint.colorFilter = filter; invalidateSelf() }
|
|
3798
|
+
@Suppress("DEPRECATION")
|
|
3799
|
+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3634
3802
|
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
3635
3803
|
class MainActivity : AppCompatActivity() {
|
|
3636
3804
|
private class Screen(
|
|
@@ -3805,6 +3973,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3805
3973
|
private var initialDeepLinkClaimed = false
|
|
3806
3974
|
private var defaultTextColor = Color.WHITE
|
|
3807
3975
|
private var windowSafeInsets = intArrayOf(0, 0, 0, 0)
|
|
3976
|
+
private var navigationButtonInsets = intArrayOf(0, 0, 0)
|
|
3977
|
+
private val navigationBarProtection = CraftNativeNavigationBarProtection()
|
|
3808
3978
|
private var useMessagePorts = false
|
|
3809
3979
|
private val imageExecutor = Executors.newFixedThreadPool(2)
|
|
3810
3980
|
private val imageCache by lazy { CraftNativeImageCache(File(cacheDir, "craft-native-images")) }
|
|
@@ -3844,10 +4014,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
3844
4014
|
if (savedInstanceState == null) pendingNotificationTap = notificationTap(intent)
|
|
3845
4015
|
setContentView(R.layout.activity_main)
|
|
3846
4016
|
root = findViewById(R.id.native_root)
|
|
4017
|
+
root.foreground = navigationBarProtection
|
|
3847
4018
|
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
3848
4019
|
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
3849
4020
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
|
3850
4021
|
windowSafeInsets = intArrayOf(safe.left, safe.top, safe.right, safe.bottom)
|
|
4022
|
+
val tappable = insets.getInsets(WindowInsetsCompat.Type.tappableElement())
|
|
4023
|
+
val buttons = intArrayOf(tappable.left, tappable.right, tappable.bottom)
|
|
4024
|
+
if (!navigationButtonInsets.contentEquals(buttons)) {
|
|
4025
|
+
navigationButtonInsets = buttons
|
|
4026
|
+
visibleScreen?.let(::applyScreenBackground)
|
|
4027
|
+
}
|
|
3851
4028
|
keyboardInset = max(0, ime.bottom - safe.bottom)
|
|
3852
4029
|
updateSafeAreaViews()
|
|
3853
4030
|
updateKeyboardAvoidingViews()
|
|
@@ -4576,8 +4753,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
4576
4753
|
// only the visible one may change the colour behind system bars.
|
|
4577
4754
|
if (visibleScreen === screen) {
|
|
4578
4755
|
root.setBackgroundColor(background)
|
|
4756
|
+
// Android 15's launcher taskbar can retain system-themed button
|
|
4757
|
+
// colours even when the app requests another icon appearance.
|
|
4758
|
+
// Match that palette only behind tappable navigation buttons.
|
|
4759
|
+
// Gesture navigation and authored screen/status backgrounds stay
|
|
4760
|
+
// edge-to-edge; no global theme or system setting is changed.
|
|
4761
|
+
val protectButtons = Build.VERSION.SDK_INT >= 35 && navigationButtonInsets.any { it > 0 }
|
|
4762
|
+
val systemNight = Resources.getSystem().configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
4763
|
+
val navigationBackground = if (protectButtons) {
|
|
4764
|
+
getColor(if (systemNight) android.R.color.system_neutral1_900 else android.R.color.system_neutral1_50)
|
|
4765
|
+
} else background
|
|
4766
|
+
navigationBarProtection.update(navigationBackground,
|
|
4767
|
+
if (protectButtons) navigationButtonInsets[0] else 0,
|
|
4768
|
+
if (protectButtons) navigationButtonInsets[1] else 0,
|
|
4769
|
+
if (protectButtons) navigationButtonInsets[2] else 0)
|
|
4579
4770
|
WindowCompat.getInsetsController(window, window.decorView)
|
|
4580
|
-
.isAppearanceLightNavigationBars = Color.luminance(
|
|
4771
|
+
.isAppearanceLightNavigationBars = Color.luminance(navigationBackground) >= 0.5f
|
|
4581
4772
|
// Android 15+ draws the root behind transparent system bars.
|
|
4582
4773
|
// Older releases still paint separate window bar surfaces.
|
|
4583
4774
|
if (Build.VERSION.SDK_INT < 35) {
|
|
@@ -5697,6 +5888,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5697
5888
|
}
|
|
5698
5889
|
}
|
|
5699
5890
|
if (control is CraftNativeFlexLayout) {
|
|
5891
|
+
control.configureCelebration(props.optJSONObject("celebration"), ((craftNativeNumber(style.opt("borderRadius")) ?: 0.0) * resources.displayMetrics.density).toFloat())
|
|
5700
5892
|
control.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
5701
5893
|
} else if (control is CraftNativeKeyboardAvoidingView) {
|
|
5702
5894
|
control.content.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.236";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -1834,7 +1834,172 @@ extension UIView {
|
|
|
1834
1834
|
}
|
|
1835
1835
|
}
|
|
1836
1836
|
|
|
1837
|
+
/// A finite, opt-in native celebration. Reconciliation with the same replay
|
|
1838
|
+
/// value never restarts it; Reduce Motion always leaves the complete card still.
|
|
1839
|
+
struct CraftNativeCelebrationSpec: Equatable {
|
|
1840
|
+
let replay: Double
|
|
1841
|
+
let enabled: Bool
|
|
1842
|
+
|
|
1843
|
+
init?(_ raw: Any?) {
|
|
1844
|
+
guard let value = raw as? [String: Any],
|
|
1845
|
+
let replay = value["replay"] as? NSNumber,
|
|
1846
|
+
CFGetTypeID(replay) != CFBooleanGetTypeID(),
|
|
1847
|
+
replay.doubleValue.isFinite, replay.doubleValue >= 0 else { return nil }
|
|
1848
|
+
self.replay = replay.doubleValue
|
|
1849
|
+
enabled = value["enabled"] as? Bool != false
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
final class CraftNativeCelebration {
|
|
1854
|
+
private weak var view: UIView?
|
|
1855
|
+
private var request: CraftNativeCelebrationSpec?
|
|
1856
|
+
private var pending = false
|
|
1857
|
+
private var overlay: CALayer?
|
|
1858
|
+
private var sheen: CALayer?
|
|
1859
|
+
private var cleanup: DispatchWorkItem?
|
|
1860
|
+
private var observers: [NSObjectProtocol] = []
|
|
1861
|
+
private(set) var starts = 0
|
|
1862
|
+
var isRunning: Bool { overlay != nil }
|
|
1863
|
+
|
|
1864
|
+
init(view: UIView) {
|
|
1865
|
+
self.view = view
|
|
1866
|
+
for name in [UIApplication.didEnterBackgroundNotification, UIAccessibility.reduceMotionStatusDidChangeNotification] {
|
|
1867
|
+
observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in self?.stop() })
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
deinit {
|
|
1872
|
+
cleanup?.cancel()
|
|
1873
|
+
overlay?.removeFromSuperlayer()
|
|
1874
|
+
sheen?.removeFromSuperlayer()
|
|
1875
|
+
observers.forEach(NotificationCenter.default.removeObserver)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
func configure(_ raw: Any?) {
|
|
1879
|
+
let next = CraftNativeCelebrationSpec(raw)
|
|
1880
|
+
guard next != request else { return }
|
|
1881
|
+
stop()
|
|
1882
|
+
request = next
|
|
1883
|
+
pending = next?.enabled == true
|
|
1884
|
+
view?.setNeedsLayout()
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
func stop() {
|
|
1888
|
+
pending = false
|
|
1889
|
+
cleanup?.cancel()
|
|
1890
|
+
cleanup = nil
|
|
1891
|
+
view?.layer.removeAnimation(forKey: "craft.celebration.entrance")
|
|
1892
|
+
overlay?.removeFromSuperlayer()
|
|
1893
|
+
overlay = nil
|
|
1894
|
+
sheen?.removeFromSuperlayer()
|
|
1895
|
+
sheen = nil
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
func layout(reduceMotion: Bool = UIAccessibility.isReduceMotionEnabled) {
|
|
1899
|
+
guard pending, let view, let window = view.window,
|
|
1900
|
+
view.bounds.width > 0, view.bounds.height > 0, !view.isHidden else { return }
|
|
1901
|
+
var ancestor: UIView? = view.superview
|
|
1902
|
+
while let parent = ancestor {
|
|
1903
|
+
if parent.isHidden { return }
|
|
1904
|
+
ancestor = parent.superview
|
|
1905
|
+
}
|
|
1906
|
+
pending = false
|
|
1907
|
+
guard !reduceMotion else { return }
|
|
1908
|
+
starts += 1
|
|
1909
|
+
let frame = view.convert(view.bounds, to: window)
|
|
1910
|
+
let overlay = CALayer()
|
|
1911
|
+
overlay.name = "craft.celebration.confetti"
|
|
1912
|
+
overlay.frame = window.bounds
|
|
1913
|
+
window.layer.addSublayer(overlay)
|
|
1914
|
+
self.overlay = overlay
|
|
1915
|
+
|
|
1916
|
+
// Core Animation owns every frame. No script timers, per-frame bridge
|
|
1917
|
+
// messages, bitmap assets or touch-intercepting overlay views.
|
|
1918
|
+
let colors: [UIColor] = [.init(red: 0.23, green: 0.51, blue: 0.96, alpha: 1), .init(red: 0.6, green: 0.96, blue: 0.89, alpha: 1), .init(red: 0.9, green: 0.77, blue: 0.55, alpha: 1), .white]
|
|
1919
|
+
let now = CACurrentMediaTime()
|
|
1920
|
+
for index in 0..<52 {
|
|
1921
|
+
let side: CGFloat = index % 2 == 0 ? -1 : 1
|
|
1922
|
+
let spread = CGFloat((index * 37) % 101) / 100
|
|
1923
|
+
let start = CGPoint(x: frame.midX + side * frame.width * 0.42, y: frame.minY + 28)
|
|
1924
|
+
let end = CGPoint(x: frame.midX + side * (35 + spread * window.bounds.width * 0.7), y: window.bounds.height + 30)
|
|
1925
|
+
let piece = CALayer()
|
|
1926
|
+
piece.bounds = CGRect(x: 0, y: 0, width: index % 3 == 0 ? 4 : 7, height: 11)
|
|
1927
|
+
piece.cornerRadius = 1
|
|
1928
|
+
piece.backgroundColor = colors[index % colors.count].cgColor
|
|
1929
|
+
piece.position = end
|
|
1930
|
+
piece.opacity = 0
|
|
1931
|
+
overlay.addSublayer(piece)
|
|
1932
|
+
let path = CAKeyframeAnimation(keyPath: "position")
|
|
1933
|
+
path.values = [NSValue(cgPoint: start), NSValue(cgPoint: CGPoint(x: start.x + side * (45 + spread * 65), y: max(0, start.y - 70 - spread * 100))), NSValue(cgPoint: end)]
|
|
1934
|
+
path.keyTimes = [0, 0.28, 1]
|
|
1935
|
+
path.calculationMode = .cubic
|
|
1936
|
+
let tumble = CABasicAnimation(keyPath: "transform.rotation")
|
|
1937
|
+
tumble.fromValue = side * Double(index)
|
|
1938
|
+
tumble.toValue = side * (Double(index) + 7 + Double(spread) * 10)
|
|
1939
|
+
let fade = CAKeyframeAnimation(keyPath: "opacity")
|
|
1940
|
+
fade.values = [0, 1, 1, 0]
|
|
1941
|
+
fade.keyTimes = [0, 0.04, 0.7, 1]
|
|
1942
|
+
let group = CAAnimationGroup()
|
|
1943
|
+
group.animations = [path, tumble, fade]
|
|
1944
|
+
group.duration = 2.2 + Double(spread) * 0.45
|
|
1945
|
+
group.beginTime = now + Double(index % 7) * 0.018
|
|
1946
|
+
group.fillMode = .backwards
|
|
1947
|
+
piece.add(group, forKey: "craft.celebration.particle")
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
let entrance = CASpringAnimation(keyPath: "transform")
|
|
1951
|
+
var from = CATransform3DMakeTranslation(0, 34, 0)
|
|
1952
|
+
from = CATransform3DRotate(from, -0.045, 0, 0, 1)
|
|
1953
|
+
from = CATransform3DScale(from, 0.86, 0.86, 1)
|
|
1954
|
+
entrance.fromValue = NSValue(caTransform3D: CATransform3DConcat(view.layer.transform, from))
|
|
1955
|
+
entrance.toValue = NSValue(caTransform3D: view.layer.transform)
|
|
1956
|
+
entrance.damping = 19
|
|
1957
|
+
entrance.stiffness = 160
|
|
1958
|
+
entrance.mass = 1
|
|
1959
|
+
entrance.duration = 0.95
|
|
1960
|
+
view.layer.add(entrance, forKey: "craft.celebration.entrance")
|
|
1961
|
+
|
|
1962
|
+
let sheen = CAGradientLayer()
|
|
1963
|
+
sheen.name = "craft.celebration.sheen"
|
|
1964
|
+
sheen.frame = view.bounds
|
|
1965
|
+
sheen.colors = [UIColor.clear.cgColor, UIColor.white.withAlphaComponent(0.2).cgColor, UIColor.clear.cgColor]
|
|
1966
|
+
sheen.locations = [0.25, 0.5, 0.75]
|
|
1967
|
+
sheen.startPoint = CGPoint(x: 0, y: 0.3)
|
|
1968
|
+
sheen.endPoint = CGPoint(x: 1, y: 0.7)
|
|
1969
|
+
sheen.opacity = 0
|
|
1970
|
+
let mask = CAShapeLayer()
|
|
1971
|
+
mask.path = UIBezierPath(roundedRect: view.bounds, cornerRadius: view.layer.cornerRadius).cgPath
|
|
1972
|
+
sheen.mask = mask
|
|
1973
|
+
view.layer.addSublayer(sheen)
|
|
1974
|
+
self.sheen = sheen
|
|
1975
|
+
let sweep = CABasicAnimation(keyPath: "transform.translation.x")
|
|
1976
|
+
sweep.fromValue = -view.bounds.width
|
|
1977
|
+
sweep.toValue = view.bounds.width
|
|
1978
|
+
let light = CAKeyframeAnimation(keyPath: "opacity")
|
|
1979
|
+
light.values = [0, 1, 1, 0]
|
|
1980
|
+
light.keyTimes = [0, 0.15, 0.8, 1]
|
|
1981
|
+
let shine = CAAnimationGroup()
|
|
1982
|
+
shine.animations = [sweep, light]
|
|
1983
|
+
shine.duration = 1.15
|
|
1984
|
+
shine.beginTime = now + 0.55
|
|
1985
|
+
sheen.add(shine, forKey: "craft.celebration.sweep")
|
|
1986
|
+
let cleanup = DispatchWorkItem { [weak self] in self?.stop() }
|
|
1987
|
+
self.cleanup = cleanup
|
|
1988
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: cleanup)
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1837
1992
|
private class CraftNativeFlowView: UIStackView {
|
|
1993
|
+
var celebration: CraftNativeCelebration?
|
|
1994
|
+
func configureCelebration(_ raw: Any?) {
|
|
1995
|
+
if raw != nil && celebration == nil { celebration = CraftNativeCelebration(view: self) }
|
|
1996
|
+
celebration?.configure(raw)
|
|
1997
|
+
}
|
|
1998
|
+
override func didMoveToWindow() {
|
|
1999
|
+
super.didMoveToWindow()
|
|
2000
|
+
if window == nil { celebration?.stop() }
|
|
2001
|
+
else { setNeedsLayout() }
|
|
2002
|
+
}
|
|
1838
2003
|
var pressActiveOpacity: CGFloat?
|
|
1839
2004
|
var pressBaseOpacity: CGFloat = 1
|
|
1840
2005
|
var pressUnderlayColor: UIColor?
|
|
@@ -2051,6 +2216,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2051
2216
|
override func layoutSubviews() {
|
|
2052
2217
|
super.layoutSubviews()
|
|
2053
2218
|
craftApplyCornerRadius()
|
|
2219
|
+
celebration?.layout()
|
|
2054
2220
|
let scale = window?.screen.scale ?? traitCollection.displayScale
|
|
2055
2221
|
let result = layout(width: bounds.width, height: bounds.height, maxWidth: bounds.width, maxHeight: bounds.height, place: true)
|
|
2056
2222
|
var moved = false
|
|
@@ -4134,6 +4300,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4134
4300
|
|
|
4135
4301
|
override func viewWillDisappear(_ animated: Bool) {
|
|
4136
4302
|
super.viewWillDisappear(animated)
|
|
4303
|
+
stopCelebrations(in: view)
|
|
4137
4304
|
// The accessory belongs to the visible native screen. Clear the
|
|
4138
4305
|
// shared shell state before a hybrid/web route appears, otherwise a
|
|
4139
4306
|
// destination from the outgoing screen remains tappable on top of it.
|
|
@@ -4188,6 +4355,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4188
4355
|
/// `SCREEN_FOCUS { focused: true }`, and going away is `focused: false`.
|
|
4189
4356
|
func screenVisibilityChanged(_ visible: Bool) {
|
|
4190
4357
|
guard !runtimeDisposed else { return }
|
|
4358
|
+
if !visible, isViewLoaded { stopCelebrations(in: view) }
|
|
4191
4359
|
// Hidden tab stacks receive no UIKit appearance callback. A cancelled
|
|
4192
4360
|
// preview also restores a screen that never lost focus, so restore its
|
|
4193
4361
|
// chrome even when no new SCREEN_FOCUS event is needed.
|
|
@@ -6217,9 +6385,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6217
6385
|
if disabled { result.isUserInteractionEnabled = false }
|
|
6218
6386
|
}
|
|
6219
6387
|
applyAccessibility(props, type: type, to: result)
|
|
6388
|
+
(result as? CraftNativeFlowView)?.configureCelebration(props["celebration"])
|
|
6220
6389
|
return current
|
|
6221
6390
|
}
|
|
6222
6391
|
|
|
6392
|
+
private func stopCelebrations(in root: UIView) {
|
|
6393
|
+
(root as? CraftNativeFlowView)?.celebration?.stop()
|
|
6394
|
+
root.subviews.forEach { stopCelebrations(in: $0) }
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6223
6397
|
private func makeView(_ type: String, props: [String: Any]) -> UIView {
|
|
6224
6398
|
switch type {
|
|
6225
6399
|
case "Text":
|