Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ColorValue, HostComponent, ViewProps } from 'react-native';

import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
import type { Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

export interface NativeProps extends ViewProps {
baseColor?: ColorValue;
duration?: WithDefault<Int32, 1200>;
enabled?: WithDefault<boolean, true>;
gradientColor?: ColorValue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ColorValue, HostComponent, ViewProps } from 'react-native';

import type { WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
import type { Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

export interface NativeProps extends ViewProps {
baseColor?: ColorValue;
duration?: WithDefault<Int32, 1200>;
enabled?: WithDefault<boolean, true>;
gradientColor?: ColorValue;
}
Expand Down
20 changes: 16 additions & 4 deletions package/shared-native/android/StreamShimmerFrameLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
attrs: AttributeSet? = null,
) : FrameLayout(context, attrs) {
private var baseColor: Int = DEFAULT_BASE_COLOR
private var durationMs: Long = DEFAULT_DURATION_MS
private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
private var enabled: Boolean = true

Expand All @@ -40,6 +41,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(

private var shimmerShader: LinearGradient? = null
private var shimmerTranslateX: Float = 0f
private var animatedDurationMs: Long = 0L
private var animatedViewWidth: Float = 0f
private var animator: ValueAnimator? = null

Expand All @@ -61,6 +63,14 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
invalidate()
}

fun setDuration(duration: Int) {
val normalizedDurationMs =
if (duration > 0) duration.toLong() else DEFAULT_DURATION_MS
if (durationMs == normalizedDurationMs) return
durationMs = normalizedDurationMs
updateAnimatorState()
}

fun setShimmerEnabled(enabled: Boolean) {
if (this.enabled == enabled) return
this.enabled = enabled
Expand Down Expand Up @@ -190,16 +200,17 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
private fun startShimmer() {
val viewWidth = width.toFloat()
if (viewWidth <= 0f) return
// Keep the existing animator if the same-sized shimmer is already active.
if (animator != null && animatedViewWidth == viewWidth) return
// Keep the existing animator only when size and duration still match the current request.
if (animator != null && animatedViewWidth == viewWidth && animatedDurationMs == durationMs) return

stopShimmer()

// Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
animatedViewWidth = viewWidth
animatedDurationMs = durationMs
animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
duration = SHIMMER_DURATION_MS
duration = durationMs
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener {
Expand All @@ -213,6 +224,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
private fun stopShimmer() {
animator?.cancel()
animator = null
animatedDurationMs = 0L
animatedViewWidth = 0f
}

Expand All @@ -237,8 +249,8 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(

companion object {
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
private const val DEFAULT_DURATION_MS = 1200L
private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
private const val SHIMMER_DURATION_MS = 1200L
private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f
Expand Down
4 changes: 4 additions & 0 deletions package/shared-native/android/StreamShimmerViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class StreamShimmerViewManager : ViewGroupManager<StreamShimmerFrameLayout>(),
view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
}

override fun setDuration(view: StreamShimmerFrameLayout, duration: Int) {
view.setDuration(duration)
}

override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
}
Expand Down
20 changes: 17 additions & 3 deletions package/shared-native/ios/StreamShimmerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public final class StreamShimmerView: UIView {
private static let midHighlightAlpha: CGFloat = 0.48
private static let innerHighlightAlpha: CGFloat = 0.72
private static let defaultHighlightAlpha: CGFloat = 0.35
private static let defaultShimmerDuration: CFTimeInterval = 1.2
private static let shimmerStripWidthRatio: CGFloat = 1.25
private static let shimmerDuration: CFTimeInterval = 1.2
private static let shimmerAnimationKey = "stream_shimmer_translate_x"

private let baseLayer = CALayer()
Expand All @@ -23,6 +23,8 @@ public final class StreamShimmerView: UIView {
private var baseColor: UIColor = UIColor(white: 1, alpha: 0)
private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha)
private var enabled = false
private var shimmerDuration: CFTimeInterval = defaultShimmerDuration
private var lastAnimatedDuration: CFTimeInterval = 0
private var lastAnimatedSize: CGSize = .zero
private var isAppActive = true

Expand Down Expand Up @@ -74,16 +76,19 @@ public final class StreamShimmerView: UIView {
public func apply(
baseColor: UIColor,
gradientColor: UIColor,
durationMilliseconds: Double,
enabled: Bool
) {
self.baseColor = baseColor
self.gradientColor = gradientColor
shimmerDuration = Self.normalizedDuration(milliseconds: durationMilliseconds)
self.enabled = enabled
updateLayersForCurrentState()
}

public func stopAnimation() {
shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey)
lastAnimatedDuration = 0
lastAnimatedSize = .zero
}

Expand Down Expand Up @@ -172,7 +177,10 @@ public final class StreamShimmerView: UIView {
}

// If an animation already exists for the same size, keep it running instead of restarting.
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil, lastAnimatedSize == bounds.size {
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
lastAnimatedSize == bounds.size,
lastAnimatedDuration == shimmerDuration
{
return
}

Expand All @@ -183,14 +191,20 @@ public final class StreamShimmerView: UIView {
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.fromValue = 0
animation.toValue = bounds.width + shimmerWidth
animation.duration = Self.shimmerDuration
animation.duration = shimmerDuration
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.isRemovedOnCompletion = true
shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey)
lastAnimatedDuration = shimmerDuration
lastAnimatedSize = bounds.size
}

private static func normalizedDuration(milliseconds: Double) -> CFTimeInterval {
guard milliseconds > 0 else { return defaultShimmerDuration }
return milliseconds / 1000
}

private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
// Preserve the resolved color channels and shape only alpha for smooth highlight falloff.
let resolvedColor = color.resolvedColor(with: traitCollection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &

[_shimmerView applyWithBaseColor:baseColor
gradientColor:gradientColor
durationMilliseconds:newProps.duration
enabled:newProps.enabled];

[super updateProps:props oldProps:oldProps];
Expand Down
Loading
Loading