stream-chat-react-native 8.13.7 → 9.0.0-beta.10
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.
- package/android/build.gradle +55 -1
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNative.java +0 -153
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativeModule.java +2 -15
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +8 -1
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerFrameLayout.kt +260 -0
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerViewManager.kt +85 -0
- package/android/src/oldarch/com/streamchatreactnative/StreamChatReactNative.java +1 -1
- package/ios/StreamChatReactNative.mm +16 -148
- package/ios/shared/StreamShimmerView.swift +249 -0
- package/ios/shared/StreamShimmerViewComponentView.h +22 -0
- package/ios/shared/StreamShimmerViewComponentView.mm +108 -0
- package/package.json +16 -7
- package/src/handlers/compressImage.ts +0 -1
- package/src/index.js +2 -0
- package/src/native/NativeStreamChatReactNative.ts +0 -1
- package/src/native/StreamShimmerViewNativeComponent.ts +15 -0
- package/src/native/index.tsx +0 -2
- package/src/optionalDependencies/NativeShimmerView.ts +3 -0
- package/src/optionalDependencies/Video.tsx +14 -1
- package/src/optionalDependencies/index.ts +1 -0
- package/stream-chat-react-native.podspec +10 -27
package/android/build.gradle
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
def kotlinVersion =
|
|
2
|
+
rootProject.ext.has("kotlinVersion")
|
|
3
|
+
? rootProject.ext.get("kotlinVersion")
|
|
4
|
+
: project.properties["ImageResizer_kotlinVersion"]
|
|
5
|
+
|
|
1
6
|
buildscript {
|
|
2
7
|
repositories {
|
|
3
8
|
google()
|
|
@@ -6,7 +11,7 @@ buildscript {
|
|
|
6
11
|
|
|
7
12
|
dependencies {
|
|
8
13
|
classpath "com.android.tools.build:gradle:7.2.1"
|
|
9
|
-
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
|
10
15
|
}
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -15,6 +20,7 @@ def isNewArchitectureEnabled() {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
apply plugin: "com.android.library"
|
|
23
|
+
apply plugin: "kotlin-android"
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
|
|
@@ -30,6 +36,11 @@ def getExtOrDefault(name) {
|
|
|
30
36
|
def getExtOrIntegerDefault(name) {
|
|
31
37
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
|
|
32
38
|
}
|
|
39
|
+
def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
|
|
40
|
+
def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
|
|
41
|
+
def hasNativeSources = { File dir ->
|
|
42
|
+
dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
android {
|
|
35
46
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
@@ -65,11 +76,16 @@ android {
|
|
|
65
76
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
kotlinOptions {
|
|
80
|
+
jvmTarget = "17"
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
sourceSets {
|
|
69
84
|
main {
|
|
70
85
|
if (isNewArchitectureEnabled()) {
|
|
71
86
|
java.srcDirs += [
|
|
72
87
|
"src/newarch",
|
|
88
|
+
"src/main/java/com/streamchatreactnative/shared",
|
|
73
89
|
// This is needed to build Kotlin project with NewArch enabled
|
|
74
90
|
"${project.buildDir}/generated/source/codegen/java"
|
|
75
91
|
]
|
|
@@ -80,6 +96,44 @@ android {
|
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
tasks.register("syncSharedShimmerSources") {
|
|
100
|
+
outputs.dir(localSharedNativeRootDir)
|
|
101
|
+
outputs.upToDateWhen { false }
|
|
102
|
+
doLast {
|
|
103
|
+
def sourceRootDir = null
|
|
104
|
+
if (hasNativeSources(localSharedNativeRootDir)) {
|
|
105
|
+
sourceRootDir = localSharedNativeRootDir
|
|
106
|
+
} else if (hasNativeSources(sharedNativeRootDir)) {
|
|
107
|
+
sourceRootDir = sharedNativeRootDir
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (sourceRootDir == null) {
|
|
111
|
+
throw new GradleException(
|
|
112
|
+
"Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " +
|
|
113
|
+
"or ../../shared-native/android/**/*.{kt,java}."
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (sourceRootDir != localSharedNativeRootDir) {
|
|
118
|
+
project.delete(localSharedNativeRootDir)
|
|
119
|
+
project.copy {
|
|
120
|
+
from(sourceRootDir)
|
|
121
|
+
into(localSharedNativeRootDir)
|
|
122
|
+
}
|
|
123
|
+
} else if (!hasNativeSources(localSharedNativeRootDir)) {
|
|
124
|
+
throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
tasks.matching { it.name == "preBuild" }.configureEach {
|
|
130
|
+
dependsOn("syncSharedShimmerSources")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
|
134
|
+
dependsOn("syncSharedShimmerSources")
|
|
135
|
+
}
|
|
136
|
+
|
|
83
137
|
repositories {
|
|
84
138
|
mavenCentral()
|
|
85
139
|
google()
|
|
@@ -11,7 +11,6 @@ import android.net.Uri;
|
|
|
11
11
|
import android.os.Build;
|
|
12
12
|
import android.provider.MediaStore;
|
|
13
13
|
import android.util.Base64;
|
|
14
|
-
import android.util.Log;
|
|
15
14
|
|
|
16
15
|
import java.io.ByteArrayOutputStream;
|
|
17
16
|
import java.io.File;
|
|
@@ -33,108 +32,6 @@ public class StreamChatReactNative {
|
|
|
33
32
|
private final static String SCHEME_FILE = "file";
|
|
34
33
|
private final static String SCHEME_HTTP = "http";
|
|
35
34
|
private final static String SCHEME_HTTPS = "https";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// List of known EXIF tags we will be copying.
|
|
39
|
-
// Orientation, width, height, and some others are ignored
|
|
40
|
-
// TODO: Find any missing tag that might be useful
|
|
41
|
-
private final static String[] EXIF_TO_COPY_ROTATED = new String[]
|
|
42
|
-
{
|
|
43
|
-
ExifInterface.TAG_APERTURE_VALUE,
|
|
44
|
-
ExifInterface.TAG_MAX_APERTURE_VALUE,
|
|
45
|
-
ExifInterface.TAG_METERING_MODE,
|
|
46
|
-
ExifInterface.TAG_ARTIST,
|
|
47
|
-
ExifInterface.TAG_BITS_PER_SAMPLE,
|
|
48
|
-
ExifInterface.TAG_COMPRESSION,
|
|
49
|
-
ExifInterface.TAG_BODY_SERIAL_NUMBER,
|
|
50
|
-
ExifInterface.TAG_BRIGHTNESS_VALUE,
|
|
51
|
-
ExifInterface.TAG_CONTRAST,
|
|
52
|
-
ExifInterface.TAG_CAMERA_OWNER_NAME,
|
|
53
|
-
ExifInterface.TAG_COLOR_SPACE,
|
|
54
|
-
ExifInterface.TAG_COPYRIGHT,
|
|
55
|
-
ExifInterface.TAG_DATETIME,
|
|
56
|
-
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
57
|
-
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
58
|
-
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
|
|
59
|
-
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
|
|
60
|
-
ExifInterface.TAG_EXIF_VERSION,
|
|
61
|
-
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
|
|
62
|
-
ExifInterface.TAG_EXPOSURE_INDEX,
|
|
63
|
-
ExifInterface.TAG_EXPOSURE_MODE,
|
|
64
|
-
ExifInterface.TAG_EXPOSURE_TIME,
|
|
65
|
-
ExifInterface.TAG_EXPOSURE_PROGRAM,
|
|
66
|
-
ExifInterface.TAG_FLASH,
|
|
67
|
-
ExifInterface.TAG_FLASH_ENERGY,
|
|
68
|
-
ExifInterface.TAG_FOCAL_LENGTH,
|
|
69
|
-
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
|
|
70
|
-
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
71
|
-
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
|
|
72
|
-
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
|
|
73
|
-
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
|
|
74
|
-
ExifInterface.TAG_PLANAR_CONFIGURATION,
|
|
75
|
-
ExifInterface.TAG_F_NUMBER,
|
|
76
|
-
ExifInterface.TAG_GAIN_CONTROL,
|
|
77
|
-
ExifInterface.TAG_GAMMA,
|
|
78
|
-
ExifInterface.TAG_GPS_ALTITUDE,
|
|
79
|
-
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
|
80
|
-
ExifInterface.TAG_GPS_AREA_INFORMATION,
|
|
81
|
-
ExifInterface.TAG_GPS_DATESTAMP,
|
|
82
|
-
ExifInterface.TAG_GPS_DOP,
|
|
83
|
-
ExifInterface.TAG_GPS_LATITUDE,
|
|
84
|
-
ExifInterface.TAG_GPS_LATITUDE_REF,
|
|
85
|
-
ExifInterface.TAG_GPS_LONGITUDE,
|
|
86
|
-
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
|
87
|
-
ExifInterface.TAG_GPS_STATUS,
|
|
88
|
-
ExifInterface.TAG_GPS_DEST_BEARING,
|
|
89
|
-
ExifInterface.TAG_GPS_DEST_BEARING_REF,
|
|
90
|
-
ExifInterface.TAG_GPS_DEST_DISTANCE,
|
|
91
|
-
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
|
|
92
|
-
ExifInterface.TAG_GPS_DEST_LATITUDE,
|
|
93
|
-
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
|
|
94
|
-
ExifInterface.TAG_GPS_DEST_LONGITUDE,
|
|
95
|
-
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
|
|
96
|
-
ExifInterface.TAG_GPS_DIFFERENTIAL,
|
|
97
|
-
ExifInterface.TAG_GPS_IMG_DIRECTION,
|
|
98
|
-
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
|
|
99
|
-
ExifInterface.TAG_GPS_MAP_DATUM,
|
|
100
|
-
ExifInterface.TAG_GPS_MEASURE_MODE,
|
|
101
|
-
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
|
102
|
-
ExifInterface.TAG_GPS_SATELLITES,
|
|
103
|
-
ExifInterface.TAG_GPS_SPEED,
|
|
104
|
-
ExifInterface.TAG_GPS_SPEED_REF,
|
|
105
|
-
ExifInterface.TAG_GPS_STATUS,
|
|
106
|
-
ExifInterface.TAG_GPS_TIMESTAMP,
|
|
107
|
-
ExifInterface.TAG_GPS_TRACK,
|
|
108
|
-
ExifInterface.TAG_GPS_TRACK_REF,
|
|
109
|
-
ExifInterface.TAG_GPS_VERSION_ID,
|
|
110
|
-
ExifInterface.TAG_IMAGE_DESCRIPTION,
|
|
111
|
-
ExifInterface.TAG_IMAGE_UNIQUE_ID,
|
|
112
|
-
ExifInterface.TAG_ISO_SPEED,
|
|
113
|
-
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
|
|
114
|
-
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
|
|
115
|
-
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
116
|
-
ExifInterface.TAG_LENS_MAKE,
|
|
117
|
-
ExifInterface.TAG_LENS_MODEL,
|
|
118
|
-
ExifInterface.TAG_LENS_SERIAL_NUMBER,
|
|
119
|
-
ExifInterface.TAG_LENS_SPECIFICATION,
|
|
120
|
-
ExifInterface.TAG_LIGHT_SOURCE,
|
|
121
|
-
ExifInterface.TAG_MAKE,
|
|
122
|
-
ExifInterface.TAG_MAKER_NOTE,
|
|
123
|
-
ExifInterface.TAG_MODEL,
|
|
124
|
-
// ExifInterface.TAG_ORIENTATION, // removed
|
|
125
|
-
ExifInterface.TAG_SATURATION,
|
|
126
|
-
ExifInterface.TAG_SHARPNESS,
|
|
127
|
-
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
|
|
128
|
-
ExifInterface.TAG_SOFTWARE,
|
|
129
|
-
ExifInterface.TAG_SUBJECT_DISTANCE,
|
|
130
|
-
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
|
|
131
|
-
ExifInterface.TAG_SUBJECT_LOCATION,
|
|
132
|
-
ExifInterface.TAG_USER_COMMENT,
|
|
133
|
-
ExifInterface.TAG_WHITE_BALANCE
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
35
|
/**
|
|
139
36
|
* Resize the specified bitmap.
|
|
140
37
|
*/
|
|
@@ -263,55 +160,6 @@ public class StreamChatReactNative {
|
|
|
263
160
|
return file;
|
|
264
161
|
}
|
|
265
162
|
|
|
266
|
-
/**
|
|
267
|
-
* Attempts to copy exif info from one file to another. Note: orientation, width, and height
|
|
268
|
-
exif attributes are not copied since those are lost after image rotation.
|
|
269
|
-
|
|
270
|
-
* imageUri: original image URI as provided from JS
|
|
271
|
-
* dstPath: final image output path
|
|
272
|
-
* Returns true if copy was successful, false otherwise.
|
|
273
|
-
*/
|
|
274
|
-
public static boolean copyExif(Context context, Uri imageUri, String dstPath){
|
|
275
|
-
ExifInterface src = null;
|
|
276
|
-
ExifInterface dst = null;
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
|
|
280
|
-
File file = getFileFromUri(context, imageUri);
|
|
281
|
-
if (!file.exists()) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
src = new ExifInterface(file.getAbsolutePath());
|
|
286
|
-
dst = new ExifInterface(dstPath);
|
|
287
|
-
|
|
288
|
-
} catch (Exception ignored) {
|
|
289
|
-
Log.e("StreamChatReactNative::copyExif", "EXIF read failed", ignored);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if(src == null || dst == null){
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try{
|
|
297
|
-
|
|
298
|
-
for (String attr : EXIF_TO_COPY_ROTATED)
|
|
299
|
-
{
|
|
300
|
-
String value = src.getAttribute(attr);
|
|
301
|
-
if (value != null){
|
|
302
|
-
dst.setAttribute(attr, value);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
dst.saveAttributes();
|
|
306
|
-
|
|
307
|
-
} catch (Exception ignored) {
|
|
308
|
-
Log.e("StreamChatReactNative::copyExif", "EXIF copy failed", ignored);
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
163
|
/**
|
|
316
164
|
* Get orientation by reading Image metadata
|
|
317
165
|
*/
|
|
@@ -591,4 +439,3 @@ public class StreamChatReactNative {
|
|
|
591
439
|
return scaledImage;
|
|
592
440
|
}
|
|
593
441
|
}
|
|
594
|
-
|
|
@@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
|
|
|
4
4
|
import android.graphics.Bitmap;
|
|
5
5
|
import android.net.Uri;
|
|
6
6
|
import android.os.AsyncTask;
|
|
7
|
-
import android.util.Log;
|
|
8
|
-
|
|
9
7
|
import androidx.annotation.Nullable;
|
|
10
8
|
import androidx.annotation.NonNull;
|
|
11
9
|
|
|
@@ -36,7 +34,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
@ReactMethod
|
|
39
|
-
public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath,
|
|
37
|
+
public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise) {
|
|
40
38
|
WritableMap options = Arguments.createMap();
|
|
41
39
|
options.putString("mode", mode);
|
|
42
40
|
options.putBoolean("onlyScaleDown", onlyScaleDown);
|
|
@@ -46,7 +44,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
46
44
|
@Override
|
|
47
45
|
protected void doInBackgroundGuarded(Void... params) {
|
|
48
46
|
try {
|
|
49
|
-
Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath,
|
|
47
|
+
Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, options);
|
|
50
48
|
promise.resolve(response);
|
|
51
49
|
}
|
|
52
50
|
catch (IOException e) {
|
|
@@ -59,7 +57,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
59
57
|
@SuppressLint("LongLogTag")
|
|
60
58
|
private Object createResizedImageWithExceptions(String imagePath, int newWidth, int newHeight,
|
|
61
59
|
String compressFormatString, int quality, int rotation, String outputPath,
|
|
62
|
-
final boolean keepMeta,
|
|
63
60
|
final ReadableMap options) throws IOException {
|
|
64
61
|
|
|
65
62
|
Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
|
|
@@ -89,16 +86,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
|
|
|
89
86
|
response.putDouble("size", resizedImage.length());
|
|
90
87
|
response.putDouble("width", scaledImage.getWidth());
|
|
91
88
|
response.putDouble("height", scaledImage.getHeight());
|
|
92
|
-
|
|
93
|
-
// Copy file's metadata/exif info if required
|
|
94
|
-
if(keepMeta){
|
|
95
|
-
try{
|
|
96
|
-
StreamChatReactNative.copyExif(this.getReactApplicationContext(), imageUri, resizedImage.getAbsolutePath());
|
|
97
|
-
}
|
|
98
|
-
catch(Exception ignored){
|
|
99
|
-
Log.e("StreamChatReactNative::createResizedImageWithExceptions", "EXIF copy failed", ignored);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
89
|
} else {
|
|
103
90
|
throw new IOException("Error getting resized image path");
|
|
104
91
|
}
|
|
@@ -6,9 +6,11 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
6
6
|
import com.facebook.react.module.model.ReactModuleInfo;
|
|
7
7
|
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
8
8
|
import com.facebook.react.TurboReactPackage;
|
|
9
|
-
|
|
9
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
10
10
|
|
|
11
11
|
import java.util.HashMap;
|
|
12
|
+
import java.util.Collections;
|
|
13
|
+
import java.util.List;
|
|
12
14
|
import java.util.Map;
|
|
13
15
|
|
|
14
16
|
public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
@@ -42,4 +44,9 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
|
|
|
42
44
|
return moduleInfos;
|
|
43
45
|
};
|
|
44
46
|
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
50
|
+
return Collections.<ViewManager>singletonList(new StreamShimmerViewManager());
|
|
51
|
+
}
|
|
45
52
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
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.View
|
|
13
|
+
import android.view.animation.LinearInterpolator
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Native shimmer container used by `StreamShimmerView`.
|
|
19
|
+
*
|
|
20
|
+
* This view draws a base color plus a moving highlight strip directly on canvas and still behaves
|
|
21
|
+
* like a regular container for React children. The animation runs fully on the native side so it
|
|
22
|
+
* does not depend on JS-driven frame updates. It automatically stops animating when the view is
|
|
23
|
+
* detached or not visible, rebuilds its shader when size or colors change, and waits for valid
|
|
24
|
+
* dimensions before starting animation to avoid invalid draw/animation states.
|
|
25
|
+
*/
|
|
26
|
+
class StreamShimmerFrameLayout @JvmOverloads constructor(
|
|
27
|
+
context: Context,
|
|
28
|
+
attrs: AttributeSet? = null,
|
|
29
|
+
) : FrameLayout(context, attrs) {
|
|
30
|
+
private var baseColor: Int = DEFAULT_BASE_COLOR
|
|
31
|
+
private var durationMs: Long = DEFAULT_DURATION_MS
|
|
32
|
+
private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
|
|
33
|
+
private var enabled: Boolean = true
|
|
34
|
+
|
|
35
|
+
private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
|
|
36
|
+
private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
37
|
+
style = Paint.Style.FILL
|
|
38
|
+
isDither = true
|
|
39
|
+
}
|
|
40
|
+
private val shimmerMatrix = Matrix()
|
|
41
|
+
|
|
42
|
+
private var shimmerShader: LinearGradient? = null
|
|
43
|
+
private var shimmerTranslateX: Float = 0f
|
|
44
|
+
private var animatedDurationMs: Long = 0L
|
|
45
|
+
private var animatedViewWidth: Float = 0f
|
|
46
|
+
private var animator: ValueAnimator? = null
|
|
47
|
+
|
|
48
|
+
init {
|
|
49
|
+
setWillNotDraw(false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun setBaseColor(color: Int) {
|
|
53
|
+
if (baseColor == color) return
|
|
54
|
+
baseColor = color
|
|
55
|
+
rebuildShimmerShader()
|
|
56
|
+
invalidate()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun setGradientColor(color: Int) {
|
|
60
|
+
if (gradientColor == color) return
|
|
61
|
+
gradientColor = color
|
|
62
|
+
rebuildShimmerShader()
|
|
63
|
+
invalidate()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun setDuration(duration: Int) {
|
|
67
|
+
val normalizedDurationMs =
|
|
68
|
+
if (duration > 0) duration.toLong() else DEFAULT_DURATION_MS
|
|
69
|
+
if (durationMs == normalizedDurationMs) return
|
|
70
|
+
durationMs = normalizedDurationMs
|
|
71
|
+
updateAnimatorState()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fun setShimmerEnabled(enabled: Boolean) {
|
|
75
|
+
if (this.enabled == enabled) return
|
|
76
|
+
this.enabled = enabled
|
|
77
|
+
updateAnimatorState()
|
|
78
|
+
invalidate()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
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.
|
|
84
|
+
if (shouldAnimateShimmer()) {
|
|
85
|
+
startShimmer()
|
|
86
|
+
} else {
|
|
87
|
+
stopShimmer()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override fun onAttachedToWindow() {
|
|
92
|
+
super.onAttachedToWindow()
|
|
93
|
+
// Reattachment (including reparenting) should recheck visibility state and restart only if
|
|
94
|
+
// this instance is eligible to animate.
|
|
95
|
+
updateAnimatorState()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onDetachedFromWindow() {
|
|
99
|
+
// Detached views are not drawable; stop and clear animator so a future attach starts cleanly.
|
|
100
|
+
stopShimmer()
|
|
101
|
+
super.onDetachedFromWindow()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
105
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
106
|
+
rebuildShimmerShader()
|
|
107
|
+
updateAnimatorState()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun onWindowVisibilityChanged(visibility: Int) {
|
|
111
|
+
super.onWindowVisibilityChanged(visibility)
|
|
112
|
+
updateAnimatorState()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
116
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
117
|
+
if (changedView === this) {
|
|
118
|
+
updateAnimatorState()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override fun dispatchDraw(canvas: Canvas) {
|
|
123
|
+
val viewWidth = width.toFloat()
|
|
124
|
+
val viewHeight = height.toFloat()
|
|
125
|
+
if (viewWidth <= 0f || viewHeight <= 0f) {
|
|
126
|
+
super.dispatchDraw(canvas)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
basePaint.color = baseColor
|
|
131
|
+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint)
|
|
132
|
+
|
|
133
|
+
drawShimmer(canvas, viewWidth, viewHeight)
|
|
134
|
+
super.dispatchDraw(canvas)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
|
|
138
|
+
if (!enabled) return
|
|
139
|
+
|
|
140
|
+
val shader = shimmerShader ?: return
|
|
141
|
+
|
|
142
|
+
shimmerMatrix.setTranslate(shimmerTranslateX, 0f)
|
|
143
|
+
shader.setLocalMatrix(shimmerMatrix)
|
|
144
|
+
shimmerPaint.shader = shader
|
|
145
|
+
canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint)
|
|
146
|
+
shimmerPaint.shader = null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun rebuildShimmerShader() {
|
|
150
|
+
// Recreates the shimmer gradient for the current width/colors. This allocates shader state,
|
|
151
|
+
// so keep calls tied to real changes (size or color updates), not per frame execution.
|
|
152
|
+
val viewWidth = width.toFloat()
|
|
153
|
+
if (viewWidth <= 0f) {
|
|
154
|
+
shimmerShader = null
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look.
|
|
159
|
+
val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
|
|
160
|
+
val transparentHighlight = colorWithAlpha(gradientColor, 0f)
|
|
161
|
+
val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR)
|
|
162
|
+
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
|
+
shimmerShader = LinearGradient(
|
|
166
|
+
0f,
|
|
167
|
+
0f,
|
|
168
|
+
shimmerWidth,
|
|
169
|
+
0f,
|
|
170
|
+
intArrayOf(
|
|
171
|
+
transparentHighlight,
|
|
172
|
+
edgeBase,
|
|
173
|
+
softBase,
|
|
174
|
+
mediumBase,
|
|
175
|
+
innerBase,
|
|
176
|
+
gradientColor,
|
|
177
|
+
innerBase,
|
|
178
|
+
mediumBase,
|
|
179
|
+
softBase,
|
|
180
|
+
edgeBase,
|
|
181
|
+
transparentHighlight,
|
|
182
|
+
),
|
|
183
|
+
floatArrayOf(
|
|
184
|
+
0f,
|
|
185
|
+
0.08f,
|
|
186
|
+
0.2f,
|
|
187
|
+
0.32f,
|
|
188
|
+
0.4f,
|
|
189
|
+
0.5f,
|
|
190
|
+
0.6f,
|
|
191
|
+
0.68f,
|
|
192
|
+
0.8f,
|
|
193
|
+
0.92f,
|
|
194
|
+
1f,
|
|
195
|
+
),
|
|
196
|
+
Shader.TileMode.CLAMP,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun startShimmer() {
|
|
201
|
+
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
|
|
205
|
+
|
|
206
|
+
stopShimmer()
|
|
207
|
+
|
|
208
|
+
// Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
|
|
209
|
+
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
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private fun stopShimmer() {
|
|
225
|
+
animator?.cancel()
|
|
226
|
+
animator = null
|
|
227
|
+
animatedDurationMs = 0L
|
|
228
|
+
animatedViewWidth = 0f
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private fun shouldAnimateShimmer(): Boolean {
|
|
232
|
+
// `isShown` and explicit visibility/window checks cover different hide paths in nested
|
|
233
|
+
// hierarchies. Keeping them all prevents animations running when not visible to the user.
|
|
234
|
+
return enabled &&
|
|
235
|
+
isAttachedToWindow &&
|
|
236
|
+
width > 0 &&
|
|
237
|
+
height > 0 &&
|
|
238
|
+
visibility == View.VISIBLE &&
|
|
239
|
+
windowVisibility == View.VISIBLE &&
|
|
240
|
+
isShown &&
|
|
241
|
+
alpha > 0f
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun colorWithAlpha(color: Int, alphaFactor: Float): Int {
|
|
245
|
+
// Preserve RGB while shaping only alpha; used for symmetric highlight falloff in gradient stops.
|
|
246
|
+
val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255)
|
|
247
|
+
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
companion object {
|
|
251
|
+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
|
|
252
|
+
private const val DEFAULT_DURATION_MS = 1200L
|
|
253
|
+
private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
|
|
254
|
+
private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
|
|
255
|
+
private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
|
|
256
|
+
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
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.streamchatreactnative
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull
|
|
4
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
7
|
+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fabric manager for StreamShimmerView.
|
|
12
|
+
*
|
|
13
|
+
* It creates the native shimmer layout, maps React props to native setters, and exposes child
|
|
14
|
+
* management methods so Fabric can mount and unmount children correctly inside this container.
|
|
15
|
+
* The manager rechecks animation state after prop transactions and disables shimmer when a view
|
|
16
|
+
* instance is dropped as a defensive cleanup step for recycled or unmounted views. Because the
|
|
17
|
+
* shimmer view wraps React children, this must remain a real ViewGroupManager as using a non-group
|
|
18
|
+
* manager can fail in Fabric mounting paths at runtime.
|
|
19
|
+
*/
|
|
20
|
+
class StreamShimmerViewManager : ViewGroupManager<StreamShimmerFrameLayout>(),
|
|
21
|
+
StreamShimmerViewManagerInterface<StreamShimmerFrameLayout> {
|
|
22
|
+
private val delegate = StreamShimmerViewManagerDelegate(this)
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = REACT_CLASS
|
|
25
|
+
|
|
26
|
+
@NonNull
|
|
27
|
+
override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout {
|
|
28
|
+
val layout = StreamShimmerFrameLayout(reactContext)
|
|
29
|
+
layout.updateAnimatorState()
|
|
30
|
+
return layout
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) {
|
|
34
|
+
super.onAfterUpdateTransaction(view)
|
|
35
|
+
// Prop batches can change visibility/enabled/colors together, so we re-evaluate the animator once
|
|
36
|
+
// after every transaction to keep state consistent and avoid duplicate start/stop churn.
|
|
37
|
+
view.updateAnimatorState()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) {
|
|
41
|
+
parent.addView(child, index)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View {
|
|
45
|
+
return parent.getChildAt(index)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun getChildCount(parent: StreamShimmerFrameLayout): Int {
|
|
49
|
+
return parent.childCount
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) {
|
|
53
|
+
parent.removeViewAt(index)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun getDelegate(): ViewManagerDelegate<StreamShimmerFrameLayout> = delegate
|
|
57
|
+
|
|
58
|
+
override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) {
|
|
59
|
+
view.setShimmerEnabled(enabled)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) {
|
|
63
|
+
view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun setDuration(view: StreamShimmerFrameLayout, duration: Int) {
|
|
67
|
+
view.setDuration(duration)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
|
|
71
|
+
view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) {
|
|
75
|
+
super.onDropViewInstance(view)
|
|
76
|
+
// Defensive shutdown for recycled/unmounted views; avoids animator leaks in list-heavy screens.
|
|
77
|
+
view.setShimmerEnabled(false)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
companion object {
|
|
81
|
+
const val REACT_CLASS = "StreamShimmerView"
|
|
82
|
+
private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
|
|
83
|
+
private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -12,5 +12,5 @@ abstract class StreamChatReactNativeSpec extends ReactContextBaseJavaModule {
|
|
|
12
12
|
super(context);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath,
|
|
15
|
+
public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise);
|
|
16
16
|
}
|