Skip to content

Commit 8b9f7ee

Browse files
committed
feat: initial pass at implementing gradient shimmer on android
1 parent 48240e3 commit 8b9f7ee

File tree

10 files changed

+411
-13
lines changed

10 files changed

+411
-13
lines changed

package/native-package/android/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ buildscript {
66

77
dependencies {
88
classpath "com.android.tools.build:gradle:7.2.1"
9-
9+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${project.properties['ImageResizer_kotlinVersion']}"
1010
}
1111
}
1212

@@ -15,6 +15,7 @@ def isNewArchitectureEnabled() {
1515
}
1616

1717
apply plugin: "com.android.library"
18+
apply plugin: "kotlin-android"
1819

1920

2021
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
@@ -65,6 +66,10 @@ android {
6566
targetCompatibility JavaVersion.VERSION_1_8
6667
}
6768

69+
kotlinOptions {
70+
jvmTarget = "17"
71+
}
72+
6873
sourceSets {
6974
main {
7075
if (isNewArchitectureEnabled()) {

package/native-package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
import com.facebook.react.module.model.ReactModuleInfo;
77
import com.facebook.react.module.model.ReactModuleInfoProvider;
88
import com.facebook.react.TurboReactPackage;
9+
import com.facebook.react.uimanager.ViewManager;
910

1011

1112
import java.util.HashMap;
13+
import java.util.Collections;
14+
import java.util.List;
1215
import java.util.Map;
1316

1417
public class StreamChatReactNativePackage extends TurboReactPackage {
@@ -42,4 +45,9 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() {
4245
return moduleInfos;
4346
};
4447
}
48+
49+
@Override
50+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
51+
return Collections.<ViewManager>singletonList(new StreamShimmerViewManager());
52+
}
4553
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.streamchatreactnative
2+
3+
import android.animation.ValueAnimator
4+
import android.content.Context
5+
import android.graphics.Canvas
6+
import android.graphics.Color
7+
import android.graphics.LinearGradient
8+
import android.graphics.Matrix
9+
import android.graphics.Paint
10+
import android.graphics.Shader
11+
import android.util.AttributeSet
12+
import android.view.animation.LinearInterpolator
13+
import android.widget.FrameLayout
14+
import kotlin.math.roundToInt
15+
16+
class StreamShimmerFrameLayout @JvmOverloads constructor(
17+
context: Context,
18+
attrs: AttributeSet? = null,
19+
) : FrameLayout(context, attrs) {
20+
private var baseColor: Int = DEFAULT_BASE_COLOR
21+
private var highlightColor: Int = DEFAULT_HIGHLIGHT_COLOR
22+
private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
23+
private var gradientWidth: Float = 0f
24+
private var gradientHeight: Float = 0f
25+
private var enabled: Boolean = true
26+
27+
private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
28+
private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
29+
private val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
30+
private val shimmerMatrix = Matrix()
31+
32+
private var shimmerShader: LinearGradient? = null
33+
private var shimmerTranslateX: Float = 0f
34+
private var animator: ValueAnimator? = null
35+
36+
init {
37+
setWillNotDraw(false)
38+
}
39+
40+
fun setBaseColor(color: Int) {
41+
if (baseColor == color) return
42+
baseColor = color
43+
rebuildShimmerShader()
44+
invalidate()
45+
}
46+
47+
fun setHighlightColor(color: Int) {
48+
if (highlightColor == color) return
49+
highlightColor = color
50+
rebuildShimmerShader()
51+
invalidate()
52+
}
53+
54+
fun setGradientColor(color: Int) {
55+
if (gradientColor == color) return
56+
gradientColor = color
57+
invalidate()
58+
}
59+
60+
fun setGradientWidth(widthPx: Float) {
61+
if (gradientWidth == widthPx) return
62+
gradientWidth = widthPx
63+
invalidate()
64+
}
65+
66+
fun setGradientHeight(heightPx: Float) {
67+
if (gradientHeight == heightPx) return
68+
gradientHeight = heightPx
69+
invalidate()
70+
}
71+
72+
fun setShimmerEnabled(enabled: Boolean) {
73+
if (this.enabled == enabled) return
74+
this.enabled = enabled
75+
updateAnimatorState()
76+
invalidate()
77+
}
78+
79+
fun updateAnimatorState() {
80+
if (enabled && isAttachedToWindow && width > 0) {
81+
startShimmer()
82+
} else {
83+
stopShimmer()
84+
}
85+
}
86+
87+
override fun onAttachedToWindow() {
88+
super.onAttachedToWindow()
89+
updateAnimatorState()
90+
}
91+
92+
override fun onDetachedFromWindow() {
93+
stopShimmer()
94+
super.onDetachedFromWindow()
95+
}
96+
97+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
98+
super.onSizeChanged(w, h, oldw, oldh)
99+
rebuildShimmerShader()
100+
updateAnimatorState()
101+
}
102+
103+
override fun dispatchDraw(canvas: Canvas) {
104+
val viewWidth = width.toFloat()
105+
val viewHeight = height.toFloat()
106+
if (viewWidth <= 0f || viewHeight <= 0f) {
107+
super.dispatchDraw(canvas)
108+
return
109+
}
110+
111+
basePaint.color = baseColor
112+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint)
113+
114+
drawShimmer(canvas, viewWidth, viewHeight)
115+
drawGradient(canvas, viewWidth, viewHeight)
116+
117+
super.dispatchDraw(canvas)
118+
}
119+
120+
private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
121+
if (!enabled) return
122+
123+
val shader = shimmerShader ?: return
124+
125+
shimmerMatrix.setTranslate(shimmerTranslateX, 0f)
126+
shader.setLocalMatrix(shimmerMatrix)
127+
shimmerPaint.shader = shader
128+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint)
129+
shimmerPaint.shader = null
130+
}
131+
132+
private fun drawGradient(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
133+
if (gradientWidth <= 0f || gradientHeight <= 0f) return
134+
135+
val left = (viewWidth - gradientWidth) / 2f
136+
val top = (viewHeight - gradientHeight) / 2f
137+
val right = left + gradientWidth
138+
val bottom = top + gradientHeight
139+
val gradient = LinearGradient(
140+
left,
141+
top,
142+
right,
143+
top,
144+
intArrayOf(
145+
colorWithAlpha(gradientColor, 0f),
146+
colorWithAlpha(gradientColor, GRADIENT_CENTER_ALPHA),
147+
colorWithAlpha(gradientColor, 0f),
148+
),
149+
floatArrayOf(0f, 0.5f, 1f),
150+
Shader.TileMode.CLAMP,
151+
)
152+
gradientPaint.shader = gradient
153+
canvas.drawRect(left, top, right, bottom, gradientPaint)
154+
gradientPaint.shader = null
155+
}
156+
157+
private fun rebuildShimmerShader() {
158+
val viewWidth = width.toFloat()
159+
if (viewWidth <= 0f) {
160+
shimmerShader = null
161+
return
162+
}
163+
164+
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
165+
shimmerShader = LinearGradient(
166+
0f,
167+
0f,
168+
shimmerWidth,
169+
0f,
170+
intArrayOf(baseColor, highlightColor, baseColor),
171+
floatArrayOf(0f, 0.5f, 1f),
172+
Shader.TileMode.CLAMP,
173+
)
174+
}
175+
176+
private fun startShimmer() {
177+
if (animator != null) return
178+
179+
val viewWidth = width.toFloat()
180+
if (viewWidth <= 0f) return
181+
182+
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
183+
animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
184+
duration = SHIMMER_DURATION_MS
185+
repeatCount = ValueAnimator.INFINITE
186+
interpolator = LinearInterpolator()
187+
addUpdateListener {
188+
shimmerTranslateX = it.animatedValue as Float
189+
invalidate()
190+
}
191+
start()
192+
}
193+
}
194+
195+
private fun stopShimmer() {
196+
animator?.cancel()
197+
animator = null
198+
}
199+
200+
private fun colorWithAlpha(color: Int, alphaFactor: Float): Int {
201+
val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255)
202+
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
203+
}
204+
205+
companion object {
206+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
207+
private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF
208+
private const val DEFAULT_GRADIENT_COLOR = Color.WHITE
209+
private const val SHIMMER_DURATION_MS = 1200L
210+
private const val SHIMMER_STRIP_WIDTH_RATIO = 0.35f
211+
private const val GRADIENT_CENTER_ALPHA = 0.35f
212+
}
213+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.streamchatreactnative
2+
3+
import androidx.annotation.NonNull
4+
import com.facebook.react.uimanager.ViewManagerDelegate
5+
import com.facebook.react.uimanager.PixelUtil
6+
import com.facebook.react.uimanager.ThemedReactContext
7+
import com.facebook.react.uimanager.ViewGroupManager
8+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate
9+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface
10+
11+
class StreamShimmerViewManager : ViewGroupManager<StreamShimmerFrameLayout>(),
12+
StreamShimmerViewManagerInterface<StreamShimmerFrameLayout> {
13+
private val delegate = StreamShimmerViewManagerDelegate(this)
14+
15+
override fun getName(): String = REACT_CLASS
16+
17+
@NonNull
18+
override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout {
19+
val layout = StreamShimmerFrameLayout(reactContext)
20+
layout.updateAnimatorState()
21+
return layout
22+
}
23+
24+
override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) {
25+
super.onAfterUpdateTransaction(view)
26+
view.updateAnimatorState()
27+
}
28+
29+
override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) {
30+
parent.addView(child, index)
31+
}
32+
33+
override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View {
34+
return parent.getChildAt(index)
35+
}
36+
37+
override fun getChildCount(parent: StreamShimmerFrameLayout): Int {
38+
return parent.childCount
39+
}
40+
41+
override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) {
42+
parent.removeViewAt(index)
43+
}
44+
45+
override fun getDelegate(): ViewManagerDelegate<StreamShimmerFrameLayout> = delegate
46+
47+
override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) {
48+
view.setShimmerEnabled(enabled)
49+
}
50+
51+
override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) {
52+
view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
53+
}
54+
55+
override fun setHighlightColor(view: StreamShimmerFrameLayout, color: Int?) {
56+
view.setHighlightColor(color ?: DEFAULT_HIGHLIGHT_COLOR)
57+
}
58+
59+
override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
60+
view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
61+
}
62+
63+
override fun setGradientWidth(view: StreamShimmerFrameLayout, widthDp: Double) {
64+
view.setGradientWidth(PixelUtil.toPixelFromDIP(widthDp.toFloat()))
65+
}
66+
67+
override fun setGradientHeight(view: StreamShimmerFrameLayout, heightDp: Double) {
68+
view.setGradientHeight(PixelUtil.toPixelFromDIP(heightDp.toFloat()))
69+
}
70+
71+
override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) {
72+
super.onDropViewInstance(view)
73+
view.setShimmerEnabled(false)
74+
}
75+
76+
companion object {
77+
const val REACT_CLASS = "StreamShimmerView"
78+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
79+
private const val DEFAULT_HIGHLIGHT_COLOR = 0x59FFFFFF
80+
private const val DEFAULT_GRADIENT_COLOR = 0xFFFFFFFF.toInt()
81+
}
82+
}

package/native-package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
},
8787
"codegenConfig": {
8888
"name": "StreamChatReactNativeSpec",
89-
"type": "modules",
89+
"type": "all",
9090
"jsSrcsDir": "src/native"
9191
},
9292
"resolutions": {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ColorValue, HostComponent, ViewProps } from 'react-native';
2+
3+
import type { Double } from 'react-native/Libraries/Types/CodegenTypes';
4+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
5+
6+
export interface NativeProps extends ViewProps {
7+
baseColor?: ColorValue;
8+
enabled?: boolean;
9+
gradientColor?: ColorValue;
10+
gradientHeight?: Double;
11+
gradientWidth?: Double;
12+
highlightColor?: ColorValue;
13+
}
14+
15+
export default codegenNativeComponent<NativeProps>(
16+
'StreamShimmerView',
17+
) as HostComponent<NativeProps>;

0 commit comments

Comments
 (0)