stream-chat-expo 9.1.2 → 9.1.3-beta.2

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.
@@ -1,16 +1,16 @@
1
1
  package com.streamchatreactnative
2
2
 
3
- import android.animation.ValueAnimator
4
3
  import android.content.Context
5
4
  import android.graphics.Canvas
6
5
  import android.graphics.Color
7
6
  import android.graphics.LinearGradient
8
7
  import android.graphics.Matrix
9
8
  import android.graphics.Paint
9
+ import android.graphics.Rect
10
10
  import android.graphics.Shader
11
11
  import android.util.AttributeSet
12
+ import android.view.Choreographer
12
13
  import android.view.View
13
- import android.view.animation.LinearInterpolator
14
14
  import android.widget.FrameLayout
15
15
  import kotlin.math.roundToInt
16
16
 
@@ -38,12 +38,12 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
38
38
  isDither = true
39
39
  }
40
40
  private val shimmerMatrix = Matrix()
41
+ private val visibleViewportRect = Rect()
41
42
 
42
43
  private var shimmerShader: LinearGradient? = null
43
44
  private var shimmerTranslateX: Float = 0f
44
- private var animatedDurationMs: Long = 0L
45
- private var animatedViewWidth: Float = 0f
46
- private var animator: ValueAnimator? = null
45
+ private var isRegisteredForShimmerFrames: Boolean = false
46
+ private var shimmerStartTimeNanos: Long = UNSET_FRAME_TIME_NANOS
47
47
 
48
48
  init {
49
49
  setWillNotDraw(false)
@@ -68,6 +68,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
68
68
  if (duration > 0) duration.toLong() else DEFAULT_DURATION_MS
69
69
  if (durationMs == normalizedDurationMs) return
70
70
  durationMs = normalizedDurationMs
71
+ shimmerStartTimeNanos = UNSET_FRAME_TIME_NANOS
71
72
  updateAnimatorState()
72
73
  }
73
74
 
@@ -79,8 +80,8 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
79
80
  }
80
81
 
81
82
  fun updateAnimatorState() {
82
- // Centralized lifecycle gate for animation start/stop. This keeps shimmer off for detached or
83
- // hidden views to avoid wasting UI-thread work in long lists.
83
+ // Centralized lifecycle gate for the shared frame clock. This keeps shimmer off for detached or
84
+ // hidden views and prevents every mounted shimmer from owning a separate ValueAnimator.
84
85
  if (shouldAnimateShimmer()) {
85
86
  startShimmer()
86
87
  } else {
@@ -96,7 +97,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
96
97
  }
97
98
 
98
99
  override fun onDetachedFromWindow() {
99
- // Detached views are not drawable; stop and clear animator so a future attach starts cleanly.
100
+ // Detached views are not drawable; unregister so a future attach starts cleanly.
100
101
  stopShimmer()
101
102
  super.onDetachedFromWindow()
102
103
  }
@@ -114,9 +115,7 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
114
115
 
115
116
  override fun onVisibilityChanged(changedView: View, visibility: Int) {
116
117
  super.onVisibilityChanged(changedView, visibility)
117
- if (changedView === this) {
118
- updateAnimatorState()
119
- }
118
+ updateAnimatorState()
120
119
  }
121
120
 
122
121
  override fun dispatchDraw(canvas: Canvas) {
@@ -155,13 +154,10 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
155
154
  return
156
155
  }
157
156
 
158
- // Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look.
157
+ // Match iOS CAGradientLayer shimmer stops so both platforms have the same visual falloff.
159
158
  val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
160
159
  val transparentHighlight = colorWithAlpha(gradientColor, 0f)
161
- val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR)
162
160
  val softBase = colorWithAlpha(gradientColor, SOFT_HIGHLIGHT_ALPHA_FACTOR)
163
- val mediumBase = colorWithAlpha(gradientColor, MID_HIGHLIGHT_ALPHA_FACTOR)
164
- val innerBase = colorWithAlpha(gradientColor, INNER_HIGHLIGHT_ALPHA_FACTOR)
165
161
  shimmerShader = LinearGradient(
166
162
  0f,
167
163
  0f,
@@ -169,28 +165,16 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
169
165
  0f,
170
166
  intArrayOf(
171
167
  transparentHighlight,
172
- edgeBase,
173
168
  softBase,
174
- mediumBase,
175
- innerBase,
176
169
  gradientColor,
177
- innerBase,
178
- mediumBase,
179
170
  softBase,
180
- edgeBase,
181
171
  transparentHighlight,
182
172
  ),
183
173
  floatArrayOf(
184
174
  0f,
185
- 0.08f,
186
- 0.2f,
187
- 0.32f,
188
- 0.4f,
175
+ 0.35f,
189
176
  0.5f,
190
- 0.6f,
191
- 0.68f,
192
- 0.8f,
193
- 0.92f,
177
+ 0.65f,
194
178
  1f,
195
179
  ),
196
180
  Shader.TileMode.CLAMP,
@@ -198,34 +182,59 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
198
182
  }
199
183
 
200
184
  private fun startShimmer() {
185
+ if (isRegisteredForShimmerFrames) return
201
186
  val viewWidth = width.toFloat()
202
- if (viewWidth <= 0f) return
203
- // Keep the existing animator only when size and duration still match the current request.
204
- if (animator != null && animatedViewWidth == viewWidth && animatedDurationMs == durationMs) return
187
+ shimmerStartTimeNanos = UNSET_FRAME_TIME_NANOS
188
+ if (viewWidth > 0f) {
189
+ shimmerTranslateX = -(viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
190
+ }
191
+ isRegisteredForShimmerFrames = true
192
+ StreamShimmerFrameClock.register(this)
193
+ }
205
194
 
206
- stopShimmer()
195
+ private fun stopShimmer() {
196
+ if (isRegisteredForShimmerFrames) {
197
+ isRegisteredForShimmerFrames = false
198
+ StreamShimmerFrameClock.unregister(this)
199
+ }
200
+ resetShimmerFrameState()
201
+ }
202
+
203
+ internal fun onSharedShimmerFrame(frameTimeNanos: Long) {
204
+ val viewWidth = width.toFloat()
205
+ if (viewWidth <= 0f || !hasVisibleViewport()) return
206
+
207
+ if (shimmerStartTimeNanos == UNSET_FRAME_TIME_NANOS) {
208
+ shimmerStartTimeNanos = frameTimeNanos
209
+ }
207
210
 
208
211
  // Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
209
212
  val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
210
- animatedViewWidth = viewWidth
211
- animatedDurationMs = durationMs
212
- animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
213
- duration = durationMs
214
- repeatCount = ValueAnimator.INFINITE
215
- interpolator = LinearInterpolator()
216
- addUpdateListener {
217
- shimmerTranslateX = it.animatedValue as Float
218
- invalidate()
219
- }
220
- start()
221
- }
213
+ val durationNanos = (durationMs * NANOS_PER_MILLISECOND).coerceAtLeast(1L)
214
+ val elapsedNanos = (frameTimeNanos - shimmerStartTimeNanos).coerceAtLeast(0L)
215
+ val progress = (elapsedNanos % durationNanos).toFloat() / durationNanos.toFloat()
216
+
217
+ shimmerTranslateX = -shimmerWidth + ((viewWidth + shimmerWidth) * progress)
218
+ invalidate()
222
219
  }
223
220
 
224
- private fun stopShimmer() {
225
- animator?.cancel()
226
- animator = null
227
- animatedDurationMs = 0L
228
- animatedViewWidth = 0f
221
+ internal fun onRemovedFromSharedFrameClock() {
222
+ isRegisteredForShimmerFrames = false
223
+ resetShimmerFrameState()
224
+ }
225
+
226
+ internal fun shouldRunSharedShimmerFrame(): Boolean {
227
+ return shouldAnimateShimmer()
228
+ }
229
+
230
+ private fun hasVisibleViewport(): Boolean {
231
+ visibleViewportRect.setEmpty()
232
+ return getGlobalVisibleRect(visibleViewportRect) && !visibleViewportRect.isEmpty
233
+ }
234
+
235
+ private fun resetShimmerFrameState() {
236
+ shimmerStartTimeNanos = UNSET_FRAME_TIME_NANOS
237
+ shimmerTranslateX = 0f
229
238
  }
230
239
 
231
240
  private fun shouldAnimateShimmer(): Boolean {
@@ -251,10 +260,51 @@ class StreamShimmerFrameLayout @JvmOverloads constructor(
251
260
  private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
252
261
  private const val DEFAULT_DURATION_MS = 1200L
253
262
  private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
263
+ private const val NANOS_PER_MILLISECOND = 1_000_000L
254
264
  private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
255
- private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
256
265
  private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f
257
- private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.48f
258
- private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.72f
266
+ private const val UNSET_FRAME_TIME_NANOS = -1L
267
+ }
268
+ }
269
+
270
+ private object StreamShimmerFrameClock : Choreographer.FrameCallback {
271
+ private val activeViews = LinkedHashSet<StreamShimmerFrameLayout>()
272
+ private var frameScheduled = false
273
+
274
+ fun register(view: StreamShimmerFrameLayout) {
275
+ activeViews.add(view)
276
+ scheduleNextFrame()
277
+ }
278
+
279
+ fun unregister(view: StreamShimmerFrameLayout) {
280
+ activeViews.remove(view)
281
+ if (activeViews.isEmpty() && frameScheduled) {
282
+ Choreographer.getInstance().removeFrameCallback(this)
283
+ frameScheduled = false
284
+ }
285
+ }
286
+
287
+ override fun doFrame(frameTimeNanos: Long) {
288
+ frameScheduled = false
289
+ if (activeViews.isEmpty()) return
290
+
291
+ val iterator = activeViews.iterator()
292
+ while (iterator.hasNext()) {
293
+ val view = iterator.next()
294
+ if (view.shouldRunSharedShimmerFrame()) {
295
+ view.onSharedShimmerFrame(frameTimeNanos)
296
+ } else {
297
+ iterator.remove()
298
+ view.onRemovedFromSharedFrameClock()
299
+ }
300
+ }
301
+
302
+ scheduleNextFrame()
303
+ }
304
+
305
+ private fun scheduleNextFrame() {
306
+ if (frameScheduled || activeViews.isEmpty()) return
307
+ Choreographer.getInstance().postFrameCallback(this)
308
+ frameScheduled = true
259
309
  }
260
310
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-expo",
3
3
  "description": "The official Expo SDK for Stream Chat, a service for building chat applications",
4
- "version": "9.1.2",
4
+ "version": "9.1.3-beta.2",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -27,7 +27,7 @@
27
27
  "types": "types/index.d.ts",
28
28
  "dependencies": {
29
29
  "mime": "^4.0.7",
30
- "stream-chat-react-native-core": "9.1.2"
30
+ "stream-chat-react-native-core": "9.1.3-beta.2"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "expo": ">=52.0.0",