diff --git a/ios/FastImage/FFFastImageView.h b/ios/FastImage/FFFastImageView.h index e1bedaa20..21577e823 100644 --- a/ios/FastImage/FFFastImageView.h +++ b/ios/FastImage/FFFastImageView.h @@ -20,7 +20,8 @@ @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd; @property (nonatomic, assign) RCTResizeMode resizeMode; @property (nonatomic, strong) FFFastImageSource *source; -@property (nonatomic, strong) UIImage *defaultSource; +@property (nonatomic, strong) UIImage *placeholderImage; +@property (nonatomic, strong) NSString *defaultSource; @property (nonatomic, strong) UIColor *imageColor; #ifdef RCT_NEW_ARCH_ENABLED @property(nonatomic) facebook::react::SharedViewEventEmitter eventEmitter; @@ -29,4 +30,3 @@ - (void)didSetProps:(NSArray*)changedProps; @end - diff --git a/ios/FastImage/FFFastImageView.mm b/ios/FastImage/FFFastImageView.mm index 69d06d575..28e6b304f 100644 --- a/ios/FastImage/FFFastImageView.mm +++ b/ios/FastImage/FFFastImageView.mm @@ -71,14 +71,48 @@ - (void) setImageColor: (UIColor*)imageColor { } } -- (UIImage*) makeImage: (UIImage*)image withTint: (UIColor*)color { - UIImage* newImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate]; - UIGraphicsBeginImageContextWithOptions(image.size, NO, newImage.scale); - [color set]; - [newImage drawInRect: CGRectMake(0, 0, image.size.width, newImage.size.height)]; - newImage = UIGraphicsGetImageFromCurrentImageContext(); +- (UIImage *) makeImage:(UIImage *)image withTint:(UIColor *)color { + if (!image || !color || image.size.width <= 0 || image.size.height <= 0) { + return image; + } + + // 使用 UIGraphicsImageRenderer (iOS 10+) + if (@available(iOS 10.0, *)) { + UIGraphicsImageRendererFormat *format = + [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = image.scale; + format.opaque = NO; + + UIGraphicsImageRenderer *renderer = + [[UIGraphicsImageRenderer alloc] initWithSize:image.size format:format]; + return [renderer imageWithActions:^( + UIGraphicsImageRendererContext *rendererContext) { + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + [color setFill]; + UIRectFillUsingBlendMode( + CGRectMake(0, 0, image.size.width, image.size.height), + kCGBlendModeSourceAtop); + }]; + } + + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + if (!context) { UIGraphicsEndImageContext(); - return newImage; + return image; + } + + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + + CGContextSetBlendMode(context, kCGBlendModeSourceAtop); + [color setFill]; + CGContextFillRect(context, + CGRectMake(0, 0, image.size.width, image.size.height)); + + UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return tintedImage ?: image; } - (void) setImage: (UIImage*)image { @@ -115,13 +149,68 @@ - (void) setSource: (FFFastImageSource*)source { } } -- (void) setDefaultSource: (UIImage*)defaultSource { - if (_defaultSource != defaultSource) { - _defaultSource = defaultSource; +- (void) setPlaceholderImage:(UIImage *)placeholderImage { + if (_placeholderImage != placeholderImage) { + _placeholderImage = placeholderImage; _needsReload = YES; } } +- (void) setDefaultSource:(NSString *)defaultSource { + if (defaultSource.length == 0) { + return; + } + + _defaultSource = [defaultSource copy]; + + UIImage *placeholder = nil; + + NSURL *url = [NSURL URLWithString:defaultSource]; + NSString *scheme = url.scheme.lowercaseString; + + BOOL isHTTP = (scheme.length > 0 && + ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])); + + if (!isHTTP) { + NSURL *fileURL = nil; + + if (url.isFileURL) { + fileURL = url; + } else { + fileURL = [NSURL fileURLWithPath:defaultSource]; + } + + placeholder = [UIImage imageWithContentsOfFile:fileURL.path]; + self.placeholderImage = placeholder; + return; + } + +#if DEBUG + __weak FFFastImageView *weakSelf = self; + NSURL *requestURL = url; + + [[SDWebImageManager sharedManager] loadImageWithURL:requestURL + options:0 + progress:nil + completed:^(UIImage * _Nullable image, + NSData * _Nullable data, + NSError * _Nullable error, + SDImageCacheType cacheType, + BOOL finished, + NSURL * _Nullable imageURL) { + __strong FFFastImageView *strongSelf = weakSelf; + if (!self) return; + if (!finished || error || !image) return; + + if (![strongSelf.defaultSource isEqualToString:defaultSource]) return; + + strongSelf.placeholderImage = image; + }]; +#else + self.placeholderImage = nil; +#endif +} + - (void) didSetProps: (NSArray*)changedProps { if (_needsReload) { [self reloadImage]; @@ -235,14 +324,14 @@ - (void) reloadImage { [self downloadImage: _source options: options context: context]; } else if (_defaultSource) { - [self setImage: _defaultSource]; + [self setImage: _placeholderImage]; } } - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context { __weak FFFastImageView *weakSelf = self; // Always use a weak reference to self in blocks [self sd_setImageWithURL: _source.url - placeholderImage: _defaultSource + placeholderImage: _placeholderImage options: options context: context progress: ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) { diff --git a/ios/FastImage/FFFastImageViewComponentView.mm b/ios/FastImage/FFFastImageViewComponentView.mm index bd899edf3..c87e27bf5 100644 --- a/ios/FastImage/FFFastImageViewComponentView.mm +++ b/ios/FastImage/FFFastImageViewComponentView.mm @@ -81,6 +81,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & [fastImageView setSource: imageSource]; + NSString *defaultSource = RCTNSStringFromStringNilIfEmpty(newViewProps.defaultSource); + [fastImageView setDefaultSource:defaultSource]; + RCTResizeMode resizeMode; switch (newViewProps.resizeMode) { @@ -99,7 +102,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & break; } [fastImageView setResizeMode:resizeMode]; - + fastImageView.imageColor = RCTUIColorFromSharedColor(newViewProps.tintColor); [super updateProps:props oldProps:oldProps]; diff --git a/ios/FastImage/FFFastImageViewManager.mm b/ios/FastImage/FFFastImageViewManager.mm index 84ca94e26..c73ed57af 100644 --- a/ios/FastImage/FFFastImageViewManager.mm +++ b/ios/FastImage/FFFastImageViewManager.mm @@ -13,7 +13,8 @@ - (FFFastImageView*)view { } RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource) -RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage) +RCT_EXPORT_VIEW_PROPERTY(defaultSource, NSString) +RCT_EXPORT_VIEW_PROPERTY(placeholderImage, UIImage) RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode) RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock) diff --git a/src/FastImageViewNativeComponent.tsx b/src/FastImageViewNativeComponent.tsx index 0847fed02..054513419 100644 --- a/src/FastImageViewNativeComponent.tsx +++ b/src/FastImageViewNativeComponent.tsx @@ -1,46 +1,46 @@ -import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; -import type { - ViewProps, - ColorValue, -} from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent' +import type { ViewProps, ColorValue } from 'react-native' import type { - Float, - WithDefault, - BubblingEventHandler, - Int32, -} from 'react-native/Libraries/Types/CodegenTypes'; + Float, + WithDefault, + BubblingEventHandler, + Int32, +} from 'react-native/Libraries/Types/CodegenTypes' -type Headers = ReadonlyArray>; -type Priority = WithDefault< 'low' | 'normal' | 'high', 'normal'> -type CacheControl = WithDefault< 'immutable' | 'web' | 'cacheOnly', 'web'> +type Headers = ReadonlyArray> +type Priority = WithDefault<'low' | 'normal' | 'high', 'normal'> +type CacheControl = WithDefault<'immutable' | 'web' | 'cacheOnly', 'web'> type FastImageSource = Readonly<{ - uri?: string, - headers?: Headers, - priority?: Priority, - cache?: CacheControl, + uri?: string + headers?: Headers + priority?: Priority + cache?: CacheControl }> type OnLoadEvent = Readonly<{ - width: Float, - height: Float, - }> + width: Float + height: Float +}> type OnProgressEvent = Readonly<{ - loaded: Int32, - total: Int32, - }> + loaded: Int32 + total: Int32 +}> interface NativeProps extends ViewProps { - onFastImageError?: BubblingEventHandler>, - onFastImageLoad?: BubblingEventHandler, - onFastImageLoadEnd?: BubblingEventHandler>, - onFastImageLoadStart?: BubblingEventHandler>, - onFastImageProgress?: BubblingEventHandler, - source?: FastImageSource, - defaultSource?: string | null, - resizeMode?: WithDefault<'contain' | 'cover' | 'stretch' | 'center', 'cover'>, - tintColor?: ColorValue, + onFastImageError?: BubblingEventHandler> + onFastImageLoad?: BubblingEventHandler + onFastImageLoadEnd?: BubblingEventHandler> + onFastImageLoadStart?: BubblingEventHandler> + onFastImageProgress?: BubblingEventHandler + source?: FastImageSource + defaultSource?: WithDefault + resizeMode?: WithDefault< + 'contain' | 'cover' | 'stretch' | 'center', + 'cover' + > + tintColor?: ColorValue } -export default codegenNativeComponent('FastImageView'); \ No newline at end of file +export default codegenNativeComponent('FastImageView') diff --git a/src/index.tsx b/src/index.tsx index 3e52d62c2..466741207 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,7 +16,7 @@ import { ColorValue, ImageResolvedAssetSource, } from 'react-native' -import FastImageView from './FastImageViewNativeComponent'; +import FastImageView from './FastImageViewNativeComponent' export type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center' @@ -137,21 +137,11 @@ const resolveDefaultSource = ( if (!defaultSource) { return null } - if (Platform.OS === 'android') { - // Android receives a URI string, and resolves into a Drawable using RN's methods. - const resolved = Image.resolveAssetSource( - defaultSource as ImageRequireSource, - ) - - if (resolved) { - return resolved.uri - } - return null - } - // iOS or other number mapped assets - // In iOS the number is passed, and bridged automatically into a UIImage - return defaultSource + const resolved = Image.resolveAssetSource( + defaultSource as ImageRequireSource, + ) + return resolved?.uri ?? null } function FastImageBase({ @@ -171,24 +161,48 @@ function FastImageBase({ forwardedRef, ...props }: FastImageProps & { forwardedRef: React.Ref }) { + // 参数保护 + let safeSource = source + if ( + !source || + (typeof source === 'object' && + !source.uri && + typeof source !== 'number') + ) { + safeSource = { uri: '' } + } + const validResizeModes = ['contain', 'cover', 'stretch', 'center'] + let safeResizeMode = resizeMode + if (!validResizeModes.includes(resizeMode)) { + safeResizeMode = 'cover' + } + let safeTintColor = tintColor + if ( + tintColor !== undefined && + typeof tintColor !== 'string' && + typeof tintColor !== 'number' + ) { + safeTintColor = undefined + } if (fallback) { - const cleanedSource = { ...(source as any) } + const cleanedSource = { ...(safeSource as any) } delete cleanedSource.cache const resolvedSource = Image.resolveAssetSource(cleanedSource) - return ( {children} @@ -196,35 +210,45 @@ function FastImageBase({ } // @ts-ignore non-typed property - const FABRIC_ENABLED = !!global?.nativeFabricUIManager; - - // this type differs based on the `source` prop passed - const resolvedSource = Image.resolveAssetSource(source as any) as ImageResolvedAssetSource & {headers: any} - if (resolvedSource?.headers && (FABRIC_ENABLED || Platform.OS === 'android')) { - // we do it like that to trick codegen - const headersArray: {name: string, value: string}[] = []; - Object.keys(resolvedSource.headers).forEach(key => { - headersArray.push({name: key, value: resolvedSource.headers[key]}); + const FABRIC_ENABLED = !!global?.nativeFabricUIManager + const resolvedSource = Image.resolveAssetSource( + safeSource as any, + ) as ImageResolvedAssetSource & { headers: any } + if ( + resolvedSource?.headers && + (FABRIC_ENABLED || Platform.OS === 'android') + ) { + const headersArray: { name: string; value: string }[] = [] + Object.keys(resolvedSource.headers).forEach((key) => { + headersArray.push({ name: key, value: resolvedSource.headers[key] }) }) - resolvedSource.headers = headersArray; + resolvedSource.headers = headersArray } const resolvedDefaultSource = resolveDefaultSource(defaultSource) - const resolvedDefaultSourceAsString = resolvedDefaultSource !== null ? String(resolvedDefaultSource) : null; + const resolvedDefaultSourceAsString = + resolvedDefaultSource !== null ? String(resolvedDefaultSource) : null + const isUriInvalid = resolvedSource?.uri == null + const safeResolvedSource = { + ...resolvedSource, + uri: isUriInvalid ? '' : resolvedSource.uri, + } return ( {children}