mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-26 09:14:15 +08:00
[add] Image support for blurRadius, tintColor, and shadows
Use CSS filters to implement React Native image styles. Ref #362 Ref #548
This commit is contained in:
@@ -22,11 +22,11 @@ const ImageStylePropTypes = {
|
||||
backgroundColor: ColorPropType,
|
||||
opacity: number,
|
||||
resizeMode: oneOf(Object.keys(ImageResizeMode)),
|
||||
tintColor: ColorPropType,
|
||||
/**
|
||||
* @platform unsupported
|
||||
*/
|
||||
overlayColor: string,
|
||||
tintColor: ColorPropType,
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/Image prop "blurRadius" 1`] = `"blur(5px)"`;
|
||||
|
||||
exports[`components/Image prop "defaultSource" sets background image when value is a string 1`] = `"url(\\"https://google.com/favicon.ico\\")"`;
|
||||
|
||||
exports[`components/Image prop "defaultSource" sets background image when value is an object 1`] = `"url(\\"https://google.com/favicon.ico\\")"`;
|
||||
@@ -14,4 +16,6 @@ exports[`components/Image prop "resizeMode" value "stretch" 1`] = `"100% 100%"`;
|
||||
|
||||
exports[`components/Image prop "resizeMode" value "undefined" 1`] = `"cover"`;
|
||||
|
||||
exports[`components/Image prop "style" correctly supports "resizeMode" property 1`] = `"contain"`;
|
||||
exports[`components/Image prop "style" supports "resizeMode" property 1`] = `"contain"`;
|
||||
|
||||
exports[`components/Image prop "style" supports "shadow" properties (convert to filter) 1`] = `"drop-shadow(1px 1px 0px rgba(255,0,0,1.00))"`;
|
||||
|
||||
@@ -36,6 +36,12 @@ describe('components/Image', () => {
|
||||
expect(component.prop('accessible')).toBe(false);
|
||||
});
|
||||
|
||||
test('prop "blurRadius"', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' };
|
||||
const component = shallow(<Image blurRadius={5} defaultSource={defaultSource} />);
|
||||
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('prop "defaultSource"', () => {
|
||||
test('sets background image when value is an object', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' };
|
||||
@@ -207,11 +213,29 @@ describe('components/Image', () => {
|
||||
});
|
||||
|
||||
describe('prop "style"', () => {
|
||||
test('correctly supports "resizeMode" property', () => {
|
||||
test('supports "resizeMode" property', () => {
|
||||
const component = shallow(<Image style={{ resizeMode: Image.resizeMode.contain }} />);
|
||||
expect(findImageSurfaceStyle(component).backgroundSize).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('supports "shadow" properties (convert to filter)', () => {
|
||||
const component = shallow(
|
||||
<Image style={{ shadowColor: 'red', shadowOffset: { width: 1, height: 1 } }} />
|
||||
);
|
||||
expect(findImageSurfaceStyle(component).filter).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('supports "tintcolor" property (convert to filter)', () => {
|
||||
const defaultSource = { uri: 'https://google.com/favicon.ico' };
|
||||
const component = shallow(
|
||||
<Image defaultSource={defaultSource} style={{ tintColor: 'red' }} />
|
||||
);
|
||||
// filter
|
||||
expect(findImageSurfaceStyle(component).filter).toContain('url(#tint-');
|
||||
// svg
|
||||
expect(component.childAt(2).type()).toBe('svg');
|
||||
});
|
||||
|
||||
test('removes other unsupported View styles', () => {
|
||||
const component = shallow(<Image style={{ overlayColor: 'red', tintColor: 'blue' }} />);
|
||||
expect(component.props().style.overlayColor).toBeUndefined();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods';
|
||||
import createElement from '../createElement';
|
||||
import { getAssetByID } from '../../modules/AssetRegistry';
|
||||
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
|
||||
import ImageLoader from '../../modules/ImageLoader';
|
||||
import ImageResizeMode from './ImageResizeMode';
|
||||
import ImageSourcePropType from './ImageSourcePropType';
|
||||
@@ -73,6 +74,20 @@ const resolveAssetUri = source => {
|
||||
return uri;
|
||||
};
|
||||
|
||||
let filterId = 0;
|
||||
|
||||
const createTintColorSVG = (tintColor, id) =>
|
||||
tintColor && id != null ? (
|
||||
<svg style={{ position: 'absolute', height: 0, visibility: 'hidden', width: 0 }}>
|
||||
<defs>
|
||||
<filter id={`tint-${id}`}>
|
||||
<feFlood floodColor={`${tintColor}`} />
|
||||
<feComposite in2="SourceAlpha" operator="atop" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
) : null;
|
||||
|
||||
type State = {
|
||||
shouldDisplaySource: boolean
|
||||
};
|
||||
@@ -86,6 +101,7 @@ class Image extends Component<*, State> {
|
||||
|
||||
static propTypes = {
|
||||
...ViewPropTypes,
|
||||
blurRadius: number,
|
||||
defaultSource: ImageSourcePropType,
|
||||
draggable: bool,
|
||||
onError: func,
|
||||
@@ -98,7 +114,6 @@ class Image extends Component<*, State> {
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
// compatibility with React Native
|
||||
/* eslint-disable react/sort-prop-types */
|
||||
blurRadius: number,
|
||||
capInsets: shape({ top: number, left: number, bottom: number, right: number }),
|
||||
resizeMethod: oneOf(['auto', 'resize', 'scale'])
|
||||
/* eslint-enable react/sort-prop-types */
|
||||
@@ -118,6 +133,7 @@ class Image extends Component<*, State> {
|
||||
|
||||
static resizeMode = ImageResizeMode;
|
||||
|
||||
_filterId = 0;
|
||||
_imageRef = null;
|
||||
_imageRequestId = null;
|
||||
_imageState = null;
|
||||
@@ -130,6 +146,8 @@ class Image extends Component<*, State> {
|
||||
const shouldDisplaySource = ImageUriCache.has(uri);
|
||||
this.state = { shouldDisplaySource };
|
||||
this._imageState = getImageState(uri, shouldDisplaySource);
|
||||
this._filterId = filterId;
|
||||
filterId++;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -170,13 +188,13 @@ class Image extends Component<*, State> {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessible,
|
||||
blurRadius,
|
||||
defaultSource,
|
||||
draggable,
|
||||
onLayout,
|
||||
source,
|
||||
testID,
|
||||
/* eslint-disable */
|
||||
blurRadius,
|
||||
capInsets,
|
||||
onError,
|
||||
onLoad,
|
||||
@@ -206,10 +224,35 @@ class Image extends Component<*, State> {
|
||||
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
|
||||
const flatStyle = { ...StyleSheet.flatten(this.props.style) };
|
||||
const finalResizeMode = resizeMode || flatStyle.resizeMode || ImageResizeMode.cover;
|
||||
// View doesn't support these styles
|
||||
|
||||
// CSS filters
|
||||
const filters = [];
|
||||
const tintColor = flatStyle.tintColor;
|
||||
if (flatStyle.filter) {
|
||||
filters.push(flatStyle.filter);
|
||||
}
|
||||
if (blurRadius) {
|
||||
filters.push(`blur(${blurRadius}px)`);
|
||||
}
|
||||
if (flatStyle.shadowOffset) {
|
||||
const shadowString = resolveShadowValue(flatStyle);
|
||||
if (shadowString) {
|
||||
filters.push(`drop-shadow(${shadowString})`);
|
||||
}
|
||||
}
|
||||
if (flatStyle.tintColor) {
|
||||
filters.push(`url(#tint-${this._filterId})`);
|
||||
}
|
||||
|
||||
// these styles were converted to filters
|
||||
delete flatStyle.shadowColor;
|
||||
delete flatStyle.shadowOpacity;
|
||||
delete flatStyle.shadowOffset;
|
||||
delete flatStyle.shadowRadius;
|
||||
delete flatStyle.tintColor;
|
||||
// these styles are not supported on View
|
||||
delete flatStyle.overlayColor;
|
||||
delete flatStyle.resizeMode;
|
||||
delete flatStyle.tintColor;
|
||||
|
||||
// Accessibility image allows users to trigger the browser's image context menu
|
||||
const hiddenImage = displayImageUri
|
||||
@@ -240,10 +283,12 @@ class Image extends Component<*, State> {
|
||||
style={[
|
||||
styles.image,
|
||||
resizeModeStyles[finalResizeMode],
|
||||
backgroundImage && { backgroundImage }
|
||||
backgroundImage && { backgroundImage },
|
||||
filters.length > 0 && { filter: filters.join(' ') }
|
||||
]}
|
||||
/>
|
||||
{hiddenImage}
|
||||
{createTintColorSVG(tintColor, this._filterId)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import normalizeColor from '../../modules/normalizeColor';
|
||||
import normalizeValue from './normalizeValue';
|
||||
import resolveShadowValue from './resolveShadowValue';
|
||||
|
||||
/**
|
||||
* The browser implements the CSS cascade, where the order of properties is a
|
||||
@@ -82,16 +83,9 @@ const defaultOffset = { height: 0, width: 0 };
|
||||
* Shadow
|
||||
*/
|
||||
|
||||
// TODO: add inset and spread support
|
||||
const resolveShadow = (resolvedStyle, style) => {
|
||||
const { boxShadow, shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
|
||||
const { height, width } = shadowOffset || defaultOffset;
|
||||
const offsetX = normalizeValue(null, width);
|
||||
const offsetY = normalizeValue(null, height);
|
||||
const blurRadius = normalizeValue(null, shadowRadius || 0);
|
||||
const color = normalizeColor(shadowColor || 'black', shadowOpacity);
|
||||
|
||||
const shadow = `${offsetX} ${offsetY} ${blurRadius} ${color}`;
|
||||
const { boxShadow } = style;
|
||||
const shadow = resolveShadowValue(style);
|
||||
resolvedStyle.boxShadow = boxShadow ? `${boxShadow}, ${shadow}` : shadow;
|
||||
};
|
||||
|
||||
|
||||
27
packages/react-native-web/src/exports/StyleSheet/resolveShadowValue.js
vendored
Normal file
27
packages/react-native-web/src/exports/StyleSheet/resolveShadowValue.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2018-present, Nicolas Gallagher.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import normalizeColor from '../../modules/normalizeColor';
|
||||
import normalizeValue from './normalizeValue';
|
||||
|
||||
const defaultOffset = { height: 0, width: 0 };
|
||||
|
||||
const resolveShadowValue = (style: Object) => {
|
||||
const { shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
|
||||
const { height, width } = shadowOffset || defaultOffset;
|
||||
const offsetX = normalizeValue(null, width);
|
||||
const offsetY = normalizeValue(null, height);
|
||||
const blurRadius = normalizeValue(null, shadowRadius || 0);
|
||||
const color = normalizeColor(shadowColor || 'black', shadowOpacity);
|
||||
if (color) {
|
||||
return `${offsetX} ${offsetY} ${blurRadius} ${color}`;
|
||||
}
|
||||
};
|
||||
|
||||
export default resolveShadowValue;
|
||||
@@ -21,6 +21,7 @@ import UIExplorer, {
|
||||
Description,
|
||||
DocItem,
|
||||
Section,
|
||||
StyleList,
|
||||
storiesOf
|
||||
} from '../../ui-explorer';
|
||||
|
||||
@@ -34,6 +35,12 @@ const ImageScreen = () => (
|
||||
<Section title="Props">
|
||||
<DocItem name="...View props" />
|
||||
|
||||
<DocItem
|
||||
name="blurRadius"
|
||||
typeInfo="?number"
|
||||
description="The blur radius of the blur filter added to the image"
|
||||
/>
|
||||
|
||||
<DocItem
|
||||
name="defaultSource"
|
||||
typeInfo="?object"
|
||||
@@ -122,7 +129,11 @@ const ImageScreen = () => (
|
||||
}}
|
||||
/>
|
||||
|
||||
<DocItem name="style" typeInfo="?style" />
|
||||
<DocItem
|
||||
name="style"
|
||||
typeInfo="?style"
|
||||
description={<StyleList stylePropTypes={stylePropTypes} />}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section title="Properties">
|
||||
@@ -168,4 +179,17 @@ const ImageScreen = () => (
|
||||
</UIExplorer>
|
||||
);
|
||||
|
||||
const stylePropTypes = [
|
||||
{
|
||||
name: '...View#style'
|
||||
},
|
||||
{
|
||||
name: 'resizeMode'
|
||||
},
|
||||
{
|
||||
name: 'tintColor',
|
||||
typeInfo: 'color'
|
||||
}
|
||||
];
|
||||
|
||||
storiesOf('Components', module).add('Image', ImageScreen);
|
||||
|
||||
Reference in New Issue
Block a user