'use client'; import { Platform, createSnapshotFriendlyRef } from 'expo-modules-core'; import React from 'react'; import { StyleSheet, processColor, type ImageStyle, type TextStyle, type View } from 'react-native'; import ExpoImage from './ExpoImage'; import type { ImageCacheConfig, ImageLoadOptions, ImagePrefetchOptions, ImageProps, ImageRef, ImageSource, SFSymbolEffect, SFSymbolEffectObject, } from './Image.types'; import ImageModule from './ImageModule'; import { resolveContentFit, resolveContentPosition, resolveTransition } from './utils'; import { resolveSource, resolveSources } from './utils/resolveSources'; /** * Normalizes the `sfEffect` prop to always be an array of `SFSymbolEffectObject`. * Supports: string, object, or array of strings/objects. */ function resolveSfEffect( sfEffect: SFSymbolEffect | null | undefined ): SFSymbolEffectObject[] | null { if (sfEffect == null) { return null; } // Convert to array if not already const effectsArray = Array.isArray(sfEffect) ? sfEffect : [sfEffect]; // Normalize each item to SFSymbolEffectObject return effectsArray.map((item): SFSymbolEffectObject => { if (typeof item === 'string') { return { effect: item }; } return item; }); } let loggedDefaultSourceDeprecationWarning = false; let loggedRenderingChildrenWarning = false; export class Image extends React.PureComponent { nativeViewRef: React.RefObject; containerViewRef: React.RefObject; constructor(props: ImageProps) { super(props); this.nativeViewRef = createSnapshotFriendlyRef(); this.containerViewRef = createSnapshotFriendlyRef(); } // Reanimated support on web getAnimatableRef = () => { if (Platform.OS === 'web') { return this.containerViewRef.current; } else { return this; } }; /** * @hidden */ static Image = ImageModule.Image; /** * Preloads images at the given URLs that can be later used in the image view. * Preloaded images are cached to the memory and disk by default, so make sure * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy). * @param urls - A URL string or an array of URLs of images to prefetch. * @param {ImagePrefetchOptions['cachePolicy']} cachePolicy - The cache policy for prefetched images. * @return A promise resolving to `true` as soon as all images have been * successfully prefetched. If an image fails to be prefetched, the promise * will immediately resolve to `false` regardless of whether other images have * finished prefetching. */ static async prefetch( urls: string | string[], cachePolicy?: ImagePrefetchOptions['cachePolicy'] ): Promise; /** * Preloads images at the given URLs that can be later used in the image view. * Preloaded images are cached to the memory and disk by default, so make sure * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy). * @param urls - A URL string or an array of URLs of images to prefetch. * @param options - Options for prefetching images. * @return A promise resolving to `true` as soon as all images have been * successfully prefetched. If an image fails to be prefetched, the promise * will immediately resolve to `false` regardless of whether other images have * finished prefetching. */ static async prefetch(urls: string | string[], options?: ImagePrefetchOptions): Promise; static async prefetch( urls: string | string[], options?: ImagePrefetchOptions['cachePolicy'] | ImagePrefetchOptions ): Promise { let cachePolicy: ImagePrefetchOptions['cachePolicy'] = 'memory-disk'; let headers: ImagePrefetchOptions['headers']; switch (typeof options) { case 'string': cachePolicy = options; break; case 'object': cachePolicy = options.cachePolicy ?? cachePolicy; headers = options.headers; break; } return ImageModule.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy, headers); } /** * Asynchronously clears all images stored in memory. * @platform android * @platform ios * @return A promise resolving to `true` when the operation succeeds. * It may resolve to `false` on Android when the activity is no longer available. * Resolves to `false` on Web. */ static async clearMemoryCache(): Promise { return await ImageModule.clearMemoryCache(); } /** * Asynchronously clears all images from the disk cache. * @platform android * @platform ios * @return A promise resolving to `true` when the operation succeeds. * It may resolve to `false` on Android when the activity is no longer available. * Resolves to `false` on Web. */ static async clearDiskCache(): Promise { return await ImageModule.clearDiskCache(); } /** * Asynchronously checks if an image exists in the disk cache and resolves to * the path of the cached image if it does. * @param cacheKey - The cache key for the requested image. Unless you have set * a custom cache key, this will be the source URL of the image. * @platform android * @platform ios * @return A promise resolving to the path of the cached image. It will resolve * to `null` if the image does not exist in the cache. */ static async getCachePathAsync(cacheKey: string): Promise { return await ImageModule.getCachePathAsync(cacheKey); } /** * Asynchronously writes a local image to the disk cache under the given cache key, * without fetching it over the network. Use this to seed the cache from an image you * already have on the device, for example one returned by `expo-image-picker` or * downloaded with `expo-file-system`. A later image load that uses the same `cacheKey` * in its [`source`](#imagesource) will then be served straight from the cache. * @param source - The image to cache, either a local file URI (`string`) or an [`ImageRef`](#imageref). * > **Note:** Caching an animated image (GIF, APNG, animated WebP) from an `ImageRef` flattens * > it to a single frame, because the reference holds the decoded image rather than the original * > encoded bytes. To seed an animated image losslessly, pass its local file URI instead. * @param cacheKey - The cache key to store the image under. Pass the same value in the * `cacheKey` of the [`source`](#imagesource) when you later render the image. * @platform android * @platform ios * @return A promise that resolves once the image has been written to the disk cache. */ static async writeToCacheAsync(source: string | ImageRef, cacheKey: string): Promise { return await ImageModule.writeToCacheAsync(source, cacheKey); } /** * Asynchronously reads an image stored in the cache under the given cache key and resolves to * an [`ImageRef`](#imageref) that can be passed straight to the [`source`](#imagesource) of an * image view. Resolves to `null` when no image is cached for the key. * @param cacheKey - The cache key to read the image from. Unless you have set a custom cache key, * this is the source URL of the image. * @platform android * @platform ios * @return A promise resolving to the cached image reference, or `null` if it isn't cached. */ static async readFromCacheAsync(cacheKey: string): Promise { return await ImageModule.readFromCacheAsync(cacheKey); } /** * Configures the image cache. This allows you to manage the cache eviction policy. * @param config - The cache configuration. * @platform ios */ static configureCache(config: ImageCacheConfig): void { return ImageModule.configureCache(config); } /** * Asynchronously generates a [Blurhash](https://blurha.sh) from an image. * @param source - The image source, either a URL (string) or an ImageRef * @param numberOfComponents - The number of components to encode the blurhash with. * Must be between 1 and 9. Defaults to `[4, 3]`. * @platform android * @platform ios * @return A promise resolving to the blurhash string. */ static async generateBlurhashAsync( source: string | ImageRef, numberOfComponents: [number, number] | { width: number; height: number } ): Promise { return ImageModule.generateBlurhashAsync(source, numberOfComponents); } /** * Asynchronously generates a [Thumbhash](https://evanw.github.io/thumbhash/) from an image. * @param source - The image source, either a URL (string) or an ImageRef * @platform android * @platform ios * @return A promise resolving to the thumbhash string. */ static async generateThumbhashAsync(source: string | ImageRef): Promise { return ImageModule.generateThumbhashAsync(source); } /** * Asynchronously starts playback of the view's image if it is animated. * @platform android * @platform ios */ async startAnimating(): Promise { await this.nativeViewRef.current?.startAnimating(); } /** * Asynchronously stops the playback of the view's image if it is animated. * @platform android * @platform ios */ async stopAnimating(): Promise { await this.nativeViewRef.current?.stopAnimating(); } /** * Prevents the resource from being reloaded by locking it. * @platform android * @platform ios */ async lockResourceAsync(): Promise { await this.nativeViewRef.current?.lockResourceAsync(); } /** * Releases the lock on the resource, allowing it to be reloaded. * @platform android * @platform ios */ async unlockResourceAsync(): Promise { await this.nativeViewRef.current?.unlockResourceAsync(); } /** * Reloads the resource, ignoring lock. * @platform android * @platform ios */ async reloadAsync(): Promise { await this.nativeViewRef.current?.reloadAsync(); } /** * Loads an image from the given source to memory and resolves to * an object that references the native image instance. * @platform android * @platform ios * @platform web */ static async loadAsync( source: ImageSource | string | number, options?: ImageLoadOptions ): Promise { const resolvedSource = resolveSource(source) as ImageSource; const resolvedOptions = options?.tintColor ? { ...options, tintColor: processColor(options.tintColor) as number } : options; return await ImageModule.loadAsync(resolvedSource, resolvedOptions); } render() { const { style, source, placeholder, contentFit, contentPosition, transition, fadeDuration, resizeMode: resizeModeProp, defaultSource, loadingIndicatorSource, sfEffect, ...restProps } = this.props; const { resizeMode: resizeModeStyle, fontWeight: fontWeightStyle, color: colorStyle, fontSize: fontSizeStyle, ...restStyle } = (StyleSheet.flatten(style) as ImageStyle & TextStyle) || {}; const resizeMode = resizeModeProp ?? resizeModeStyle; if ((defaultSource || loadingIndicatorSource) && !loggedDefaultSourceDeprecationWarning) { console.warn( '[expo-image]: `defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder` instead' ); loggedDefaultSourceDeprecationWarning = true; } // @ts-expect-error if (restProps.children && !loggedRenderingChildrenWarning) { console.warn( 'The component does not support children. If you want to render content on top of the image, consider using the component or absolute positioning.' ); loggedRenderingChildrenWarning = true; } // Resolve sources const resolvedSource = resolveSources(source); const isSFSymbol = Array.isArray(resolvedSource) && resolvedSource.some((s) => s?.uri?.startsWith('sf:/')); // For SF Symbols, fontSize sets both the symbol point size and container dimensions const resolvedStyle = isSFSymbol && fontSizeStyle ? { width: fontSizeStyle, height: fontSizeStyle, ...restStyle } : restStyle; return ( ); } }