diff --git a/android/src/main/java/com/turboimage/TurboImageViewManager.kt b/android/src/main/java/com/turboimage/TurboImageViewManager.kt index 900594c..d9774e9 100644 --- a/android/src/main/java/com/turboimage/TurboImageViewManager.kt +++ b/android/src/main/java/com/turboimage/TurboImageViewManager.kt @@ -13,32 +13,49 @@ import coil.load import coil.memory.MemoryCache import coil.size.Dimension import coil.size.Size +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableMap +import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.annotations.ReactProp import okhttp3.Headers import com.turboimage.decoder.APNGDecoder +import com.turboimage.events.ProgressEvent +import com.turboimage.events.interceptor.ProgressInterceptor +import com.turboimage.events.interceptor.ProgressListener +import okhttp3.OkHttpClient class TurboImageViewManager : SimpleViewManager() { override fun getName() = REACT_CLASS + override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { + return MapBuilder.of( + "onProgress", MapBuilder.of("registrationName", "onProgress") + ) + } + override fun getExportedCustomBubblingEventTypeConstants(): Map { return mapOf( - "onFailure" to mapOf( + "onStart" to mapOf( "phasedRegistrationNames" to mapOf( - "bubbled" to "onFailure" + "bubbled" to "onStart" ) - ), "onSuccess" to mapOf( + ), + "onSuccess" to mapOf( "phasedRegistrationNames" to mapOf( "bubbled" to "onSuccess" ) - ), "onStart" to mapOf( + ), + "onFailure" to mapOf( "phasedRegistrationNames" to mapOf( - "bubbled" to "onStart" + "bubbled" to "onFailure" ) - ), "onCompletion" to mapOf( + ), + "onCompletion" to mapOf( "phasedRegistrationNames" to mapOf( "bubbled" to "onCompletion" ) @@ -58,8 +75,26 @@ class TurboImageViewManager : SimpleViewManager() { CrossfadeDrawable.DEFAULT_DURATION } + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(ProgressInterceptor(object : ProgressListener { + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + val reactContext = view.context as ReactContext + UIManagerHelper.getEventDispatcher(reactContext, view.id)?.let { + val payload = Arguments.createMap().apply { + putDouble("loaded", bytesRead.toDouble()) + putDouble("total", contentLength.toDouble()) + } + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + it.dispatchEvent(ProgressEvent(surfaceId, view.id, payload)) + } + } + })) + .build() + val imageLoader = Coil.imageLoader(view.context).newBuilder() - .respectCacheHeaders(view.cachePolicy == "urlCache").build() + .respectCacheHeaders(view.cachePolicy == "urlCache") + .okHttpClient(okHttpClient) + .build() view.load(view.uri, imageLoader) { view.headers?.let { headers(it) } diff --git a/android/src/main/java/com/turboimage/events/ProgressEvent.kt b/android/src/main/java/com/turboimage/events/ProgressEvent.kt new file mode 100644 index 0000000..d90a056 --- /dev/null +++ b/android/src/main/java/com/turboimage/events/ProgressEvent.kt @@ -0,0 +1,17 @@ +package com.turboimage.events + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class ProgressEvent(surfaceId: Int, viewId: Int, private val payload: WritableMap): + Event(surfaceId, viewId) { + override fun getEventName(): String { + return EVENT_NAME + } + + override fun getEventData() = payload + + companion object { + const val EVENT_NAME = "onProgress" + } +} diff --git a/android/src/main/java/com/turboimage/events/interceptor/ProgressInterceptor.kt b/android/src/main/java/com/turboimage/events/interceptor/ProgressInterceptor.kt new file mode 100644 index 0000000..19b01fd --- /dev/null +++ b/android/src/main/java/com/turboimage/events/interceptor/ProgressInterceptor.kt @@ -0,0 +1,13 @@ +package com.turboimage.events.interceptor + +import okhttp3.Interceptor +import okhttp3.Response + +class ProgressInterceptor(private val listener: ProgressListener) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalResponse = chain.proceed(chain.request()) + return originalResponse.newBuilder() + .body(ProgressResponseBody(originalResponse.body!!, listener)) + .build() + } +} diff --git a/android/src/main/java/com/turboimage/events/interceptor/ProgressListener.kt b/android/src/main/java/com/turboimage/events/interceptor/ProgressListener.kt new file mode 100644 index 0000000..0563e89 --- /dev/null +++ b/android/src/main/java/com/turboimage/events/interceptor/ProgressListener.kt @@ -0,0 +1,5 @@ +package com.turboimage.events.interceptor + +interface ProgressListener { + fun update(bytesRead: Long, contentLength: Long, done: Boolean) +} diff --git a/android/src/main/java/com/turboimage/events/interceptor/ProgressResponseBody.kt b/android/src/main/java/com/turboimage/events/interceptor/ProgressResponseBody.kt new file mode 100644 index 0000000..14063e2 --- /dev/null +++ b/android/src/main/java/com/turboimage/events/interceptor/ProgressResponseBody.kt @@ -0,0 +1,37 @@ +package com.turboimage.events.interceptor + +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer + +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val progressListener: ProgressListener +) : ResponseBody() { + + private val bufferedSource: BufferedSource by lazy { + source(responseBody.source()).buffer() + } + + override fun contentType() = responseBody.contentType() + + override fun contentLength() = responseBody.contentLength() + + override fun source(): BufferedSource = bufferedSource + + private fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + totalBytesRead += if (bytesRead != -1L) bytesRead else 0 + progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead + } + } + } +} diff --git a/example/src/screens/events/SuccessScreen.tsx b/example/src/screens/events/SuccessScreen.tsx index b17f7a9..9a80639 100644 --- a/example/src/screens/events/SuccessScreen.tsx +++ b/example/src/screens/events/SuccessScreen.tsx @@ -5,7 +5,7 @@ import { type NativeSyntheticEvent, } from 'react-native'; import React, { useState } from 'react'; -import type { Success, TaskState } from 'react-native-turbo-image'; +import type { Progress, Success, TaskState } from 'react-native-turbo-image'; import TurboImage from 'react-native-turbo-image'; type Information = { @@ -15,6 +15,7 @@ type Information = { }; const SuccessScreen = () => { const [start, setStart] = useState(false); + const [progress, setProgress] = useState([]); const [completion, setCompletion] = useState(false); const [information, setInformation] = useState(null); @@ -32,23 +33,33 @@ const SuccessScreen = () => { setCompletion(nativeEvent.state === 'completed'); }; + const handleProgress = ({ nativeEvent }: NativeSyntheticEvent) => { + const percentage = `${( + (100 * nativeEvent.loaded) / + nativeEvent.total + ).toFixed(2)}%`; + setProgress((prev) => [...prev, percentage]); + }; + return ( {start && Start at {Date()}} + {progress.length > 0 && Progress: {progress}} {information?.width && width: {information?.width}} {information?.height && height: {information?.height}} {information?.source && source: {information?.source}} diff --git a/src/index.tsx b/src/index.tsx index 966f379..4bec931 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ export type { Format, Success, Failure, + Progress, TaskState, ResizeMode, Indicator, diff --git a/src/types.ts b/src/types.ts index f4d26c5..0d84b71 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,11 @@ export type TaskState = { state: State; }; +export type Progress = { + loaded: number; + total: number; +}; + export type Success = { width: number; height: number; @@ -65,6 +70,7 @@ export interface TurboImageProps extends AccessibilityProps, ViewProps { format?: Format; onStart?: (result: NativeSyntheticEvent) => void; onSuccess?: (result: NativeSyntheticEvent) => void; + onProgress?: (result: NativeSyntheticEvent) => void; onFailure?: (result: NativeSyntheticEvent) => void; onCompletion?: (result: NativeSyntheticEvent) => void; }