mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-02 00:18:08 +00:00
perf(tv): optimize focus animations and disable native glass effect
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
import ExpoModulesCore
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
/// Observable state that SwiftUI can watch for changes without rebuilding the entire view
|
||||
class GlassPosterState: ObservableObject {
|
||||
@Published var imageUrl: String? = nil
|
||||
@Published var aspectRatio: Double = 10.0 / 15.0
|
||||
@Published var cornerRadius: Double = 24
|
||||
@Published var progress: Double = 0
|
||||
@Published var showWatchedIndicator: Bool = false
|
||||
@Published var isFocused: Bool = false
|
||||
@Published var width: Double = 260
|
||||
}
|
||||
|
||||
/// ExpoView wrapper that hosts the SwiftUI GlassPosterView
|
||||
class GlassPosterExpoView: ExpoView {
|
||||
private var hostingController: UIHostingController<GlassPosterView>?
|
||||
private var posterView: GlassPosterView
|
||||
private var hostingController: UIHostingController<GlassPosterViewWrapper>?
|
||||
private let state = GlassPosterState()
|
||||
|
||||
// Stored dimensions for intrinsic content size
|
||||
private var posterWidth: CGFloat = 260
|
||||
@@ -16,13 +28,13 @@ class GlassPosterExpoView: ExpoView {
|
||||
let onError = EventDispatcher()
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
self.posterView = GlassPosterView()
|
||||
super.init(appContext: appContext)
|
||||
setupHostingController()
|
||||
}
|
||||
|
||||
private func setupHostingController() {
|
||||
let hostingController = UIHostingController(rootView: posterView)
|
||||
let wrapper = GlassPosterViewWrapper(state: state)
|
||||
let hostingController = UIHostingController(rootView: wrapper)
|
||||
hostingController.view.backgroundColor = .clear
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@@ -38,10 +50,6 @@ class GlassPosterExpoView: ExpoView {
|
||||
self.hostingController = hostingController
|
||||
}
|
||||
|
||||
private func updateHostingController() {
|
||||
hostingController?.rootView = posterView
|
||||
}
|
||||
|
||||
// Override intrinsic content size for proper React Native layout
|
||||
override var intrinsicContentSize: CGSize {
|
||||
let height = posterWidth / posterAspectRatio
|
||||
@@ -49,43 +57,38 @@ class GlassPosterExpoView: ExpoView {
|
||||
}
|
||||
|
||||
// MARK: - Property Setters
|
||||
// These now update the observable state object directly.
|
||||
// SwiftUI observes state changes and only re-renders affected views.
|
||||
|
||||
func setImageUrl(_ url: String?) {
|
||||
posterView.imageUrl = url
|
||||
updateHostingController()
|
||||
state.imageUrl = url
|
||||
}
|
||||
|
||||
func setAspectRatio(_ ratio: Double) {
|
||||
posterView.aspectRatio = ratio
|
||||
state.aspectRatio = ratio
|
||||
posterAspectRatio = CGFloat(ratio)
|
||||
invalidateIntrinsicContentSize()
|
||||
updateHostingController()
|
||||
}
|
||||
|
||||
func setWidth(_ width: Double) {
|
||||
posterView.width = width
|
||||
state.width = width
|
||||
posterWidth = CGFloat(width)
|
||||
invalidateIntrinsicContentSize()
|
||||
updateHostingController()
|
||||
}
|
||||
|
||||
func setCornerRadius(_ radius: Double) {
|
||||
posterView.cornerRadius = radius
|
||||
updateHostingController()
|
||||
state.cornerRadius = radius
|
||||
}
|
||||
|
||||
func setProgress(_ progress: Double) {
|
||||
posterView.progress = progress
|
||||
updateHostingController()
|
||||
state.progress = progress
|
||||
}
|
||||
|
||||
func setShowWatchedIndicator(_ show: Bool) {
|
||||
posterView.showWatchedIndicator = show
|
||||
updateHostingController()
|
||||
state.showWatchedIndicator = show
|
||||
}
|
||||
|
||||
func setIsFocused(_ focused: Bool) {
|
||||
posterView.isFocused = focused
|
||||
updateHostingController()
|
||||
state.isFocused = focused
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Wrapper view that observes state changes from GlassPosterState
|
||||
/// This allows SwiftUI to efficiently update only the changed properties
|
||||
/// instead of rebuilding the entire view hierarchy on every prop change.
|
||||
struct GlassPosterViewWrapper: View {
|
||||
@ObservedObject var state: GlassPosterState
|
||||
|
||||
var body: some View {
|
||||
GlassPosterView(
|
||||
imageUrl: state.imageUrl,
|
||||
aspectRatio: state.aspectRatio,
|
||||
cornerRadius: state.cornerRadius,
|
||||
progress: state.progress,
|
||||
showWatchedIndicator: state.showWatchedIndicator,
|
||||
isFocused: state.isFocused,
|
||||
width: state.width
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// SwiftUI view with tvOS 26 Liquid Glass effect
|
||||
struct GlassPosterView: View {
|
||||
var imageUrl: String? = nil
|
||||
@@ -35,7 +54,7 @@ struct GlassPosterView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - tvOS 26+ Glass Effect
|
||||
// MARK: - tvOS 26+ Content (glass effect disabled for now)
|
||||
|
||||
#if os(tvOS)
|
||||
@available(tvOS 26.0, *)
|
||||
@@ -45,6 +64,10 @@ struct GlassPosterView: View {
|
||||
imageContent
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||
|
||||
// White border on focus
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.stroke(Color.white, lineWidth: isCurrentlyFocused ? 4 : 0)
|
||||
|
||||
// Progress bar overlay
|
||||
if progress > 0 {
|
||||
progressOverlay
|
||||
@@ -56,7 +79,6 @@ struct GlassPosterView: View {
|
||||
}
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||
.focusable()
|
||||
.focused($isInternallyFocused)
|
||||
.scaleEffect(isCurrentlyFocused ? 1.05 : 1.0)
|
||||
@@ -72,9 +94,9 @@ struct GlassPosterView: View {
|
||||
imageContent
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||
|
||||
// Subtle overlay for depth
|
||||
// White border on focus
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(.ultraThinMaterial.opacity(0.15))
|
||||
.stroke(Color.white, lineWidth: isFocused ? 4 : 0)
|
||||
|
||||
// Progress bar overlay
|
||||
if progress > 0 {
|
||||
|
||||
@@ -21,16 +21,23 @@ if (Platform.OS === "ios" && Platform.isTV) {
|
||||
|
||||
/**
|
||||
* Check if the native glass effect is available (tvOS 26+)
|
||||
* NOTE: Glass effect is currently disabled for performance reasons.
|
||||
* The native module rebuilds views on every focus change which causes lag.
|
||||
* Re-enable by uncommenting the native module check below.
|
||||
*/
|
||||
export function isGlassEffectAvailable(): boolean {
|
||||
if (!GlassPosterNativeModule) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return GlassPosterNativeModule.isGlassEffectAvailable();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// Glass effect disabled - using JS-based focus effects instead
|
||||
return false;
|
||||
|
||||
// Original implementation (re-enable when glass effect is optimized):
|
||||
// if (!GlassPosterNativeModule) {
|
||||
// return false;
|
||||
// }
|
||||
// try {
|
||||
// return GlassPosterNativeModule.isGlassEffectAvailable();
|
||||
// } catch {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
export default GlassPosterNativeModule;
|
||||
|
||||
Reference in New Issue
Block a user