mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-30 09:19:21 +00:00
[change] modernize Image
Rewrite Image to use function components and hooks. Fix #1322
This commit is contained in:
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
const dataUriPattern = /^data:/;
|
|
||||||
|
|
||||||
export default class ImageUriCache {
|
|
||||||
static _maximumEntries: number = 256;
|
|
||||||
static _entries = {};
|
|
||||||
|
|
||||||
static has(uri: string) {
|
|
||||||
const entries = ImageUriCache._entries;
|
|
||||||
const isDataUri = dataUriPattern.test(uri);
|
|
||||||
return isDataUri || Boolean(entries[uri]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static add(uri: string) {
|
|
||||||
const entries = ImageUriCache._entries;
|
|
||||||
const lastUsedTimestamp = Date.now();
|
|
||||||
if (entries[uri]) {
|
|
||||||
entries[uri].lastUsedTimestamp = lastUsedTimestamp;
|
|
||||||
entries[uri].refCount += 1;
|
|
||||||
} else {
|
|
||||||
entries[uri] = {
|
|
||||||
lastUsedTimestamp,
|
|
||||||
refCount: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static remove(uri: string) {
|
|
||||||
const entries = ImageUriCache._entries;
|
|
||||||
if (entries[uri]) {
|
|
||||||
entries[uri].refCount -= 1;
|
|
||||||
}
|
|
||||||
// Free up entries when the cache is "full"
|
|
||||||
ImageUriCache._cleanUpIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
static _cleanUpIfNeeded() {
|
|
||||||
const entries = ImageUriCache._entries;
|
|
||||||
const imageUris = Object.keys(entries);
|
|
||||||
|
|
||||||
if (imageUris.length + 1 > ImageUriCache._maximumEntries) {
|
|
||||||
let leastRecentlyUsedKey;
|
|
||||||
let leastRecentlyUsedEntry;
|
|
||||||
|
|
||||||
imageUris.forEach(uri => {
|
|
||||||
const entry = entries[uri];
|
|
||||||
if (
|
|
||||||
(!leastRecentlyUsedEntry ||
|
|
||||||
entry.lastUsedTimestamp < leastRecentlyUsedEntry.lastUsedTimestamp) &&
|
|
||||||
entry.refCount === 0
|
|
||||||
) {
|
|
||||||
leastRecentlyUsedKey = uri;
|
|
||||||
leastRecentlyUsedEntry = entry;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (leastRecentlyUsedKey) {
|
|
||||||
delete entries[leastRecentlyUsedKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-4
@@ -317,14 +317,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`]
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-view-1dbjc4n r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-backgroundSize-4gszlv r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw"
|
class="css-view-1dbjc4n r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-backgroundSize-4gszlv r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw"
|
||||||
style="filter: url(#tint-33);"
|
style="filter: url(#tint-53);"
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
|
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<filter
|
<filter
|
||||||
id="tint-33"
|
id="tint-53"
|
||||||
>
|
>
|
||||||
<feflood
|
<feflood
|
||||||
flood-color="blue"
|
flood-color="blue"
|
||||||
@@ -366,7 +366,7 @@ exports[`components/Image prop "style" supports "tintcolor" property (convert to
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-view-1dbjc4n r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-backgroundSize-4gszlv r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw"
|
class="css-view-1dbjc4n r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-backgroundSize-4gszlv r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw"
|
||||||
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-32);"
|
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-52);"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
@@ -379,7 +379,7 @@ exports[`components/Image prop "style" supports "tintcolor" property (convert to
|
|||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<filter
|
<filter
|
||||||
id="tint-32"
|
id="tint-52"
|
||||||
>
|
>
|
||||||
<feflood
|
<feflood
|
||||||
flood-color="red"
|
flood-color="red"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-env jasmine, jest */
|
/* eslint-env jasmine, jest */
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
|
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import * as AssetRegistry from '../../../modules/AssetRegistry';
|
import * as AssetRegistry from '../../../modules/AssetRegistry';
|
||||||
import Image from '../';
|
import Image from '../';
|
||||||
import ImageLoader from '../../../modules/ImageLoader';
|
import ImageLoader, { ImageUriCache } from '../../../modules/ImageLoader';
|
||||||
import ImageUriCache from '../ImageUriCache';
|
|
||||||
import PixelRatio from '../../PixelRatio';
|
import PixelRatio from '../../PixelRatio';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
@@ -89,10 +89,18 @@ describe('components/Image', () => {
|
|||||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||||
onLoad();
|
onLoad();
|
||||||
});
|
});
|
||||||
|
const onLoadStartStub = jest.fn();
|
||||||
const onLoadStub = jest.fn();
|
const onLoadStub = jest.fn();
|
||||||
render(<Image onLoad={onLoadStub} source="https://test.com/img.jpg" />);
|
const onLoadEndStub = jest.fn();
|
||||||
|
render(
|
||||||
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source="https://test.com/img.jpg"
|
||||||
|
/>
|
||||||
|
);
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(ImageLoader.load).toBeCalled();
|
|
||||||
expect(onLoadStub).toBeCalled();
|
expect(onLoadStub).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,32 +109,74 @@ describe('components/Image', () => {
|
|||||||
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
|
||||||
onLoad();
|
onLoad();
|
||||||
});
|
});
|
||||||
|
const onLoadStartStub = jest.fn();
|
||||||
const onLoadStub = jest.fn();
|
const onLoadStub = jest.fn();
|
||||||
|
const onLoadEndStub = jest.fn();
|
||||||
const uri = 'https://test.com/img.jpg';
|
const uri = 'https://test.com/img.jpg';
|
||||||
ImageUriCache.add(uri);
|
ImageUriCache.add(uri);
|
||||||
render(<Image onLoad={onLoadStub} source={uri} />);
|
render(
|
||||||
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source={uri}
|
||||||
|
/>
|
||||||
|
);
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(ImageLoader.load).not.toBeCalled();
|
|
||||||
expect(onLoadStub).toBeCalled();
|
expect(onLoadStub).toBeCalled();
|
||||||
ImageUriCache.remove(uri);
|
ImageUriCache.remove(uri);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('is called on update if "uri" is different', () => {
|
test('is called on update if "uri" is different', () => {
|
||||||
|
const onLoadStartStub = jest.fn();
|
||||||
const onLoadStub = jest.fn();
|
const onLoadStub = jest.fn();
|
||||||
|
const onLoadEndStub = jest.fn();
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source={'https://test.com/img.jpg'}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
rerender(<Image onLoad={onLoadStub} source={'https://blah.com/img.png'} />);
|
act(() => {
|
||||||
|
rerender(
|
||||||
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source={'https://blah.com/img.png'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
expect(onLoadStub.mock.calls.length).toBe(2);
|
expect(onLoadStub.mock.calls.length).toBe(2);
|
||||||
|
expect(onLoadEndStub.mock.calls.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('is not called on update if "uri" is the same', () => {
|
test('is not called on update if "uri" is the same', () => {
|
||||||
|
const onLoadStartStub = jest.fn();
|
||||||
const onLoadStub = jest.fn();
|
const onLoadStub = jest.fn();
|
||||||
|
const onLoadEndStub = jest.fn();
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source={'https://test.com/img.jpg'}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
rerender(<Image onLoad={onLoadStub} source={'https://test.com/img.jpg'} />);
|
act(() => {
|
||||||
|
rerender(
|
||||||
|
<Image
|
||||||
|
onLoad={onLoadStub}
|
||||||
|
onLoadEnd={onLoadEndStub}
|
||||||
|
onLoadStart={onLoadStartStub}
|
||||||
|
source={'https://test.com/img.jpg'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
expect(onLoadStub.mock.calls.length).toBe(1);
|
expect(onLoadStub.mock.calls.length).toBe(1);
|
||||||
|
expect(onLoadEndStub.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,15 +228,19 @@ describe('components/Image', () => {
|
|||||||
ImageUriCache.remove(uriOne);
|
ImageUriCache.remove(uriOne);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
// props update
|
// props update
|
||||||
rerender(<Image source={{ uri: uriTwo }} />);
|
act(() => {
|
||||||
ImageUriCache.remove(uriTwo);
|
rerender(<Image source={{ uri: uriTwo }} />);
|
||||||
|
ImageUriCache.remove(uriTwo);
|
||||||
|
});
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('is correctly updated when missing in initial render', () => {
|
test('is correctly updated when missing in initial render', () => {
|
||||||
const uri = 'https://testing.com/img.jpg';
|
const uri = 'https://testing.com/img.jpg';
|
||||||
const { container, rerender } = render(<Image />);
|
const { container, rerender } = render(<Image />);
|
||||||
rerender(<Image source={{ uri }} />);
|
act(() => {
|
||||||
|
rerender(<Image source={{ uri }} />);
|
||||||
|
});
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +253,9 @@ describe('components/Image', () => {
|
|||||||
});
|
});
|
||||||
const { container } = render(<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />);
|
const { container } = render(<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
loadCallback();
|
act(() => {
|
||||||
|
loadCallback();
|
||||||
|
});
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,8 +271,10 @@ describe('components/Image', () => {
|
|||||||
let { container } = render(<Image source={1} />);
|
let { container } = render(<Image source={1} />);
|
||||||
expect(container.querySelector('img').src).toBe('http://localhost/static/img.png');
|
expect(container.querySelector('img').src).toBe('http://localhost/static/img.png');
|
||||||
|
|
||||||
PixelRatio.get = jest.fn(() => 2.2);
|
act(() => {
|
||||||
({ container } = render(<Image source={1} />));
|
PixelRatio.get = jest.fn(() => 2.2);
|
||||||
|
({ container } = render(<Image source={1} />));
|
||||||
|
});
|
||||||
expect(container.querySelector('img').src).toBe('http://localhost/static/img@2x.png');
|
expect(container.querySelector('img').src).toBe('http://localhost/static/img@2x.png');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+236
-301
@@ -11,18 +11,16 @@
|
|||||||
import type { ViewProps } from '../View';
|
import type { ViewProps } from '../View';
|
||||||
import type { ResizeMode, Source, Style } from './types';
|
import type { ResizeMode, Source, Style } from './types';
|
||||||
|
|
||||||
import applyNativeMethods from '../../modules/applyNativeMethods';
|
|
||||||
import createElement from '../createElement';
|
import createElement from '../createElement';
|
||||||
import css from '../StyleSheet/css';
|
import css from '../StyleSheet/css';
|
||||||
import { getAssetByID } from '../../modules/AssetRegistry';
|
import { getAssetByID } from '../../modules/AssetRegistry';
|
||||||
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
|
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
|
||||||
import ImageLoader from '../../modules/ImageLoader';
|
import ImageLoader from '../../modules/ImageLoader';
|
||||||
import ImageUriCache from './ImageUriCache';
|
|
||||||
import PixelRatio from '../PixelRatio';
|
import PixelRatio from '../PixelRatio';
|
||||||
import StyleSheet from '../StyleSheet';
|
import StyleSheet from '../StyleSheet';
|
||||||
import TextAncestorContext from '../Text/TextAncestorContext';
|
import TextAncestorContext from '../Text/TextAncestorContext';
|
||||||
import View from '../View';
|
import View from '../View';
|
||||||
import React from 'react';
|
import React, { forwardRef, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
export type ImageProps = {
|
export type ImageProps = {
|
||||||
...ViewProps,
|
...ViewProps,
|
||||||
@@ -40,34 +38,84 @@ export type ImageProps = {
|
|||||||
style?: Style
|
style?: Style
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
const ERRORED = 'ERRORED';
|
||||||
layout: Object,
|
const LOADED = 'LOADED';
|
||||||
shouldDisplaySource: boolean
|
const LOADING = 'LOADING';
|
||||||
};
|
const IDLE = 'IDLE';
|
||||||
|
|
||||||
const STATUS_ERRORED = 'ERRORED';
|
let _filterId = 0;
|
||||||
const STATUS_LOADED = 'LOADED';
|
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
|
||||||
const STATUS_LOADING = 'LOADING';
|
|
||||||
const STATUS_PENDING = 'PENDING';
|
|
||||||
const STATUS_IDLE = 'IDLE';
|
|
||||||
|
|
||||||
const getImageState = (uri, shouldDisplaySource) => {
|
function createTintColorSVG(tintColor, id) {
|
||||||
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
|
return 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;
|
||||||
|
}
|
||||||
|
|
||||||
const resolveAssetDimensions = source => {
|
function getFlatStyle(style, blurRadius, filterId) {
|
||||||
|
const flatStyle = { ...StyleSheet.flatten(style) };
|
||||||
|
|
||||||
|
const { filter, resizeMode, shadowOffset, tintColor } = flatStyle;
|
||||||
|
|
||||||
|
// Add CSS filters
|
||||||
|
// React Native exposes these features as props and proprietary styles
|
||||||
|
const filters = [];
|
||||||
|
let _filter = null;
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
filters.push(filter);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if (blurRadius) {
|
||||||
|
filters.push(`blur(${blurRadius}px)`);
|
||||||
|
}
|
||||||
|
if (shadowOffset) {
|
||||||
|
const shadowString = resolveShadowValue(flatStyle);
|
||||||
|
if (shadowString) {
|
||||||
|
filters.push(`drop-shadow(${shadowString})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tintColor && filterId != null) {
|
||||||
|
filters.push(`url(#tint-${filterId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.length > 0) {
|
||||||
|
_filter = filters.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// These styles are converted to CSS filters applied to the
|
||||||
|
// element displaying the background image.
|
||||||
|
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;
|
||||||
|
|
||||||
|
return [flatStyle, resizeMode, _filter, tintColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAssetDimensions(source) {
|
||||||
if (typeof source === 'number') {
|
if (typeof source === 'number') {
|
||||||
const { height, width } = getAssetByID(source);
|
const { height, width } = getAssetByID(source);
|
||||||
return { height, width };
|
return { height, width };
|
||||||
} else if (typeof source === 'object') {
|
} else if (source != null && typeof source === 'object') {
|
||||||
const { height, width } = source;
|
const { height, width } = source;
|
||||||
return { height, width };
|
return { height, width };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
|
function resolveAssetUri(source): ?string {
|
||||||
const resolveAssetUri = source => {
|
let uri = null;
|
||||||
let uri = '';
|
|
||||||
if (typeof source === 'number') {
|
if (typeof source === 'number') {
|
||||||
// get the URI from the packager
|
// get the URI from the packager
|
||||||
const asset = getAssetByID(source);
|
const asset = getAssetByID(source);
|
||||||
@@ -98,306 +146,193 @@ const resolveAssetUri = source => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return uri;
|
return uri;
|
||||||
};
|
}
|
||||||
|
|
||||||
let filterId = 0;
|
const Image = forwardRef<ImageProps, *>((props, ref) => {
|
||||||
|
const {
|
||||||
|
accessibilityLabel,
|
||||||
|
accessibilityRelationship,
|
||||||
|
accessibilityRole,
|
||||||
|
accessibilityState,
|
||||||
|
accessible,
|
||||||
|
blurRadius,
|
||||||
|
defaultSource,
|
||||||
|
draggable,
|
||||||
|
importantForAccessibility,
|
||||||
|
nativeID,
|
||||||
|
onError,
|
||||||
|
onLayout,
|
||||||
|
onLoad,
|
||||||
|
onLoadEnd,
|
||||||
|
onLoadStart,
|
||||||
|
pointerEvents,
|
||||||
|
source,
|
||||||
|
testID
|
||||||
|
} = props;
|
||||||
|
|
||||||
const createTintColorSVG = (tintColor, id) =>
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
tintColor && id != null ? (
|
if (props.children) {
|
||||||
<svg style={{ position: 'absolute', height: 0, visibility: 'hidden', width: 0 }}>
|
throw new Error(
|
||||||
<defs>
|
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.'
|
||||||
<filter id={`tint-${id}`}>
|
);
|
||||||
<feFlood floodColor={`${tintColor}`} />
|
|
||||||
<feComposite in2="SourceAlpha" operator="atop" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
class Image extends React.Component<ImageProps, State> {
|
|
||||||
static displayName = 'Image';
|
|
||||||
|
|
||||||
static getSize(uri, success, failure) {
|
|
||||||
ImageLoader.getSize(uri, success, failure);
|
|
||||||
}
|
|
||||||
|
|
||||||
static prefetch(uri) {
|
|
||||||
return ImageLoader.prefetch(uri).then(() => {
|
|
||||||
// Add the uri to the cache so it can be immediately displayed when used
|
|
||||||
// but also immediately remove it to correctly reflect that it has no active references
|
|
||||||
ImageUriCache.add(uri);
|
|
||||||
ImageUriCache.remove(uri);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static queryCache(uris) {
|
|
||||||
const result = {};
|
|
||||||
uris.forEach(u => {
|
|
||||||
if (ImageUriCache.has(u)) {
|
|
||||||
result[u] = 'disk/memory';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Promise.resolve(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
_filterId = 0;
|
|
||||||
_imageRef = null;
|
|
||||||
_imageRequestId = null;
|
|
||||||
_imageState = null;
|
|
||||||
_isMounted = false;
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
// If an image has been loaded before, render it immediately
|
|
||||||
const uri = resolveAssetUri(props.source);
|
|
||||||
const shouldDisplaySource = ImageUriCache.has(uri);
|
|
||||||
this.state = { layout: {}, shouldDisplaySource };
|
|
||||||
this._imageState = getImageState(uri, shouldDisplaySource);
|
|
||||||
this._filterId = filterId;
|
|
||||||
filterId++;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._isMounted = true;
|
|
||||||
if (this._imageState === STATUS_PENDING) {
|
|
||||||
this._createImageLoader();
|
|
||||||
} else if (this._imageState === STATUS_LOADED) {
|
|
||||||
this._onLoad({ target: this._imageRef });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
const [state, updateState] = useState(() => {
|
||||||
const prevUri = resolveAssetUri(prevProps.source);
|
|
||||||
const uri = resolveAssetUri(this.props.source);
|
|
||||||
const hasDefaultSource = this.props.defaultSource != null;
|
|
||||||
if (prevUri !== uri) {
|
|
||||||
ImageUriCache.remove(prevUri);
|
|
||||||
const isPreviouslyLoaded = ImageUriCache.has(uri);
|
|
||||||
isPreviouslyLoaded && ImageUriCache.add(uri);
|
|
||||||
this._updateImageState(getImageState(uri, isPreviouslyLoaded), hasDefaultSource);
|
|
||||||
} else if (hasDefaultSource && prevProps.defaultSource !== this.props.defaultSource) {
|
|
||||||
this._updateImageState(this._imageState, hasDefaultSource);
|
|
||||||
}
|
|
||||||
if (this._imageState === STATUS_PENDING) {
|
|
||||||
this._createImageLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const uri = resolveAssetUri(this.props.source);
|
|
||||||
ImageUriCache.remove(uri);
|
|
||||||
this._destroyImageLoader();
|
|
||||||
this._isMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderImage(hasTextAncestor) {
|
|
||||||
const { shouldDisplaySource } = this.state;
|
|
||||||
const {
|
|
||||||
accessibilityLabel,
|
|
||||||
accessibilityRelationship,
|
|
||||||
accessibilityRole,
|
|
||||||
accessibilityState,
|
|
||||||
accessible,
|
|
||||||
blurRadius,
|
|
||||||
defaultSource,
|
|
||||||
draggable,
|
|
||||||
importantForAccessibility,
|
|
||||||
nativeID,
|
|
||||||
pointerEvents,
|
|
||||||
resizeMode,
|
|
||||||
source,
|
|
||||||
testID
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
if (this.props.children) {
|
|
||||||
throw new Error(
|
|
||||||
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedSource = shouldDisplaySource ? source : defaultSource;
|
|
||||||
const displayImageUri = resolveAssetUri(selectedSource);
|
|
||||||
const imageSizeStyle = resolveAssetDimensions(selectedSource);
|
|
||||||
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
|
|
||||||
const flatStyle = { ...StyleSheet.flatten(this.props.style) };
|
|
||||||
const finalResizeMode = resizeMode || flatStyle.resizeMode || 'cover';
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Accessibility image allows users to trigger the browser's image context menu
|
|
||||||
const hiddenImage = displayImageUri
|
|
||||||
? createElement('img', {
|
|
||||||
alt: accessibilityLabel || '',
|
|
||||||
classList: [classes.accessibilityImage],
|
|
||||||
draggable: draggable || false,
|
|
||||||
ref: this._setImageRef,
|
|
||||||
src: displayImageUri
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
accessibilityLabel={accessibilityLabel}
|
|
||||||
accessibilityRelationship={accessibilityRelationship}
|
|
||||||
accessibilityRole={accessibilityRole}
|
|
||||||
accessibilityState={accessibilityState}
|
|
||||||
accessible={accessible}
|
|
||||||
importantForAccessibility={importantForAccessibility}
|
|
||||||
nativeID={nativeID}
|
|
||||||
onLayout={this._createLayoutHandler(finalResizeMode)}
|
|
||||||
pointerEvents={pointerEvents}
|
|
||||||
style={[styles.root, hasTextAncestor && styles.inline, imageSizeStyle, flatStyle]}
|
|
||||||
testID={testID}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.image,
|
|
||||||
resizeModeStyles[finalResizeMode],
|
|
||||||
this._getBackgroundSize(finalResizeMode),
|
|
||||||
backgroundImage && { backgroundImage },
|
|
||||||
filters.length > 0 && { filter: filters.join(' ') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{hiddenImage}
|
|
||||||
{createTintColorSVG(tintColor, this._filterId)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<TextAncestorContext.Consumer>
|
|
||||||
{hasTextAncestor => this.renderImage(hasTextAncestor)}
|
|
||||||
</TextAncestorContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_createImageLoader() {
|
|
||||||
const { source } = this.props;
|
|
||||||
this._destroyImageLoader();
|
|
||||||
const uri = resolveAssetUri(source);
|
const uri = resolveAssetUri(source);
|
||||||
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
|
if (uri != null) {
|
||||||
this._onLoadStart();
|
const isLoaded = ImageLoader.has(uri);
|
||||||
}
|
if (isLoaded) {
|
||||||
|
return LOADED;
|
||||||
_destroyImageLoader() {
|
}
|
||||||
if (this._imageRequestId) {
|
|
||||||
ImageLoader.abort(this._imageRequestId);
|
|
||||||
this._imageRequestId = null;
|
|
||||||
}
|
}
|
||||||
}
|
return IDLE;
|
||||||
|
});
|
||||||
|
|
||||||
_createLayoutHandler = resizeMode => {
|
const [layout, updateLayout] = useState({});
|
||||||
const { onLayout } = this.props;
|
const hasTextAncestor = useContext(TextAncestorContext);
|
||||||
if (resizeMode === 'center' || resizeMode === 'repeat' || onLayout) {
|
const hiddenImageRef = useRef(null);
|
||||||
return e => {
|
const filterRef = useRef(_filterId++);
|
||||||
const { layout } = e.nativeEvent;
|
const requestRef = useRef(null);
|
||||||
onLayout && onLayout(e);
|
const shouldDisplaySource = state === LOADED || (state === LOADING && defaultSource == null);
|
||||||
this.setState(() => ({ layout }));
|
const [flatStyle, _resizeMode, filter, tintColor] = getFlatStyle(
|
||||||
};
|
props.style,
|
||||||
}
|
blurRadius,
|
||||||
};
|
filterRef.current
|
||||||
|
);
|
||||||
|
const resizeMode = props.resizeMode || _resizeMode || 'cover';
|
||||||
|
const selectedSource = shouldDisplaySource ? source : defaultSource;
|
||||||
|
const displayImageUri = resolveAssetUri(selectedSource);
|
||||||
|
const imageSizeStyle = resolveAssetDimensions(selectedSource);
|
||||||
|
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
|
||||||
|
const backgroundSize = getBackgroundSize();
|
||||||
|
|
||||||
_getBackgroundSize = resizeMode => {
|
// Accessibility image allows users to trigger the browser's image context menu
|
||||||
if (this._imageRef && (resizeMode === 'center' || resizeMode === 'repeat')) {
|
const hiddenImage = displayImageUri
|
||||||
const { naturalHeight, naturalWidth } = this._imageRef;
|
? createElement('img', {
|
||||||
const { height, width } = this.state.layout;
|
alt: accessibilityLabel || '',
|
||||||
|
classList: [classes.accessibilityImage],
|
||||||
|
draggable: draggable || false,
|
||||||
|
ref: hiddenImageRef,
|
||||||
|
src: displayImageUri
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function getBackgroundSize(): ?string {
|
||||||
|
if (hiddenImageRef.current != null && (resizeMode === 'center' || resizeMode === 'repeat')) {
|
||||||
|
const { naturalHeight, naturalWidth } = hiddenImageRef.current;
|
||||||
|
const { height, width } = layout;
|
||||||
if (naturalHeight && naturalWidth && height && width) {
|
if (naturalHeight && naturalWidth && height && width) {
|
||||||
const scaleFactor = Math.min(1, width / naturalWidth, height / naturalHeight);
|
const scaleFactor = Math.min(1, width / naturalWidth, height / naturalHeight);
|
||||||
const x = Math.ceil(scaleFactor * naturalWidth);
|
const x = Math.ceil(scaleFactor * naturalWidth);
|
||||||
const y = Math.ceil(scaleFactor * naturalHeight);
|
const y = Math.ceil(scaleFactor * naturalHeight);
|
||||||
return {
|
return `${x}px ${y}px`;
|
||||||
backgroundSize: `${x}px ${y}px`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
_onError = () => {
|
function handleLayout(e) {
|
||||||
const { onError, source } = this.props;
|
if (resizeMode === 'center' || resizeMode === 'repeat' || onLayout) {
|
||||||
this._updateImageState(STATUS_ERRORED);
|
const { layout } = e.nativeEvent;
|
||||||
if (onError) {
|
onLayout && onLayout(e);
|
||||||
onError({
|
updateLayout(layout);
|
||||||
nativeEvent: {
|
}
|
||||||
error: `Failed to load resource ${resolveAssetUri(source)} (404)`
|
}
|
||||||
|
|
||||||
|
// Image loading
|
||||||
|
useEffect(() => {
|
||||||
|
abortPendingRequest();
|
||||||
|
|
||||||
|
const uri = resolveAssetUri(source);
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
updateState(LOADING);
|
||||||
|
if (onLoadStart) {
|
||||||
|
onLoadStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRef.current = ImageLoader.load(
|
||||||
|
uri,
|
||||||
|
function load(e) {
|
||||||
|
updateState(LOADED);
|
||||||
|
if (onLoad) {
|
||||||
|
onLoad();
|
||||||
|
}
|
||||||
|
if (onLoadEnd) {
|
||||||
|
onLoadEnd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function error() {
|
||||||
|
updateState(ERRORED);
|
||||||
|
if (onError) {
|
||||||
|
onError({
|
||||||
|
nativeEvent: {
|
||||||
|
error: `Failed to load resource ${uri} (404)`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (onLoadEnd) {
|
||||||
|
onLoadEnd();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
this._onLoadEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
_onLoad = e => {
|
function abortPendingRequest() {
|
||||||
const { onLoad, source } = this.props;
|
if (requestRef.current != null) {
|
||||||
const event = { nativeEvent: e };
|
ImageLoader.abort(requestRef.current);
|
||||||
ImageUriCache.add(resolveAssetUri(source));
|
requestRef.current = null;
|
||||||
this._updateImageState(STATUS_LOADED);
|
|
||||||
if (onLoad) {
|
|
||||||
onLoad(event);
|
|
||||||
}
|
|
||||||
this._onLoadEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
_onLoadEnd() {
|
|
||||||
const { onLoadEnd } = this.props;
|
|
||||||
if (onLoadEnd) {
|
|
||||||
onLoadEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onLoadStart() {
|
|
||||||
const { defaultSource, onLoadStart } = this.props;
|
|
||||||
this._updateImageState(STATUS_LOADING, defaultSource != null);
|
|
||||||
if (onLoadStart) {
|
|
||||||
onLoadStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setImageRef = ref => {
|
|
||||||
this._imageRef = ref;
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateImageState(status: ?string, hasDefaultSource: ?boolean = false) {
|
|
||||||
this._imageState = status;
|
|
||||||
const shouldDisplaySource =
|
|
||||||
this._imageState === STATUS_LOADED ||
|
|
||||||
(this._imageState === STATUS_LOADING && !hasDefaultSource);
|
|
||||||
// only triggers a re-render when the image is loading and has no default image (to support PJPEG), loaded, or failed
|
|
||||||
if (shouldDisplaySource !== this.state.shouldDisplaySource) {
|
|
||||||
if (this._isMounted) {
|
|
||||||
this.setState(() => ({ shouldDisplaySource }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
return abortPendingRequest;
|
||||||
|
}, [source, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
accessibilityRelationship={accessibilityRelationship}
|
||||||
|
accessibilityRole={accessibilityRole}
|
||||||
|
accessibilityState={accessibilityState}
|
||||||
|
accessible={accessible}
|
||||||
|
importantForAccessibility={importantForAccessibility}
|
||||||
|
nativeID={nativeID}
|
||||||
|
onLayout={handleLayout}
|
||||||
|
pointerEvents={pointerEvents}
|
||||||
|
ref={ref}
|
||||||
|
style={[styles.root, hasTextAncestor && styles.inline, imageSizeStyle, flatStyle]}
|
||||||
|
testID={testID}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.image,
|
||||||
|
resizeModeStyles[resizeMode],
|
||||||
|
{ backgroundImage, filter },
|
||||||
|
backgroundSize != null && { backgroundSize }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{hiddenImage}
|
||||||
|
{createTintColorSVG(tintColor, filterRef.current)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Image.displayName = 'Image';
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
Image.getSize = function(uri, success, failure) {
|
||||||
|
ImageLoader.getSize(uri, success, failure);
|
||||||
|
};
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
Image.prefetch = function(uri) {
|
||||||
|
return ImageLoader.prefetch(uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
Image.queryCache = function(uris) {
|
||||||
|
return ImageLoader.queryCache(uris);
|
||||||
|
};
|
||||||
|
|
||||||
const classes = css.create({
|
const classes = css.create({
|
||||||
accessibilityImage: {
|
accessibilityImage: {
|
||||||
@@ -454,4 +389,4 @@ const resizeModeStyles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default applyNativeMethods(Image);
|
export default Image;
|
||||||
|
|||||||
+2
-2
@@ -33,14 +33,14 @@ export type ViewStyle = {
|
|||||||
backgroundBlendMode?: string,
|
backgroundBlendMode?: string,
|
||||||
backgroundClip?: string,
|
backgroundClip?: string,
|
||||||
backgroundColor?: ColorValue,
|
backgroundColor?: ColorValue,
|
||||||
backgroundImage?: string,
|
backgroundImage?: ?string,
|
||||||
backgroundOrigin?: 'border-box' | 'content-box' | 'padding-box',
|
backgroundOrigin?: 'border-box' | 'content-box' | 'padding-box',
|
||||||
backgroundPosition?: string,
|
backgroundPosition?: string,
|
||||||
backgroundRepeat?: string,
|
backgroundRepeat?: string,
|
||||||
backgroundSize?: string,
|
backgroundSize?: string,
|
||||||
boxShadow?: string,
|
boxShadow?: string,
|
||||||
clip?: string,
|
clip?: string,
|
||||||
filter?: string,
|
filter?: ?string,
|
||||||
opacity?: number,
|
opacity?: number,
|
||||||
outlineColor?: ColorValue,
|
outlineColor?: ColorValue,
|
||||||
outlineOffset?: string | number,
|
outlineOffset?: string | number,
|
||||||
|
|||||||
+93
-7
@@ -4,9 +4,71 @@
|
|||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @noflow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const dataUriPattern = /^data:/;
|
||||||
|
|
||||||
|
export class ImageUriCache {
|
||||||
|
static _maximumEntries: number = 256;
|
||||||
|
static _entries = {};
|
||||||
|
|
||||||
|
static has(uri: string) {
|
||||||
|
const entries = ImageUriCache._entries;
|
||||||
|
const isDataUri = dataUriPattern.test(uri);
|
||||||
|
return isDataUri || Boolean(entries[uri]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static add(uri: string) {
|
||||||
|
const entries = ImageUriCache._entries;
|
||||||
|
const lastUsedTimestamp = Date.now();
|
||||||
|
if (entries[uri]) {
|
||||||
|
entries[uri].lastUsedTimestamp = lastUsedTimestamp;
|
||||||
|
entries[uri].refCount += 1;
|
||||||
|
} else {
|
||||||
|
entries[uri] = {
|
||||||
|
lastUsedTimestamp,
|
||||||
|
refCount: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(uri: string) {
|
||||||
|
const entries = ImageUriCache._entries;
|
||||||
|
if (entries[uri]) {
|
||||||
|
entries[uri].refCount -= 1;
|
||||||
|
}
|
||||||
|
// Free up entries when the cache is "full"
|
||||||
|
ImageUriCache._cleanUpIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
static _cleanUpIfNeeded() {
|
||||||
|
const entries = ImageUriCache._entries;
|
||||||
|
const imageUris = Object.keys(entries);
|
||||||
|
|
||||||
|
if (imageUris.length + 1 > ImageUriCache._maximumEntries) {
|
||||||
|
let leastRecentlyUsedKey;
|
||||||
|
let leastRecentlyUsedEntry;
|
||||||
|
|
||||||
|
imageUris.forEach(uri => {
|
||||||
|
const entry = entries[uri];
|
||||||
|
if (
|
||||||
|
(!leastRecentlyUsedEntry ||
|
||||||
|
entry.lastUsedTimestamp < leastRecentlyUsedEntry.lastUsedTimestamp) &&
|
||||||
|
entry.refCount === 0
|
||||||
|
) {
|
||||||
|
leastRecentlyUsedKey = uri;
|
||||||
|
leastRecentlyUsedEntry = entry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (leastRecentlyUsedKey) {
|
||||||
|
delete entries[leastRecentlyUsedKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
const requests = {};
|
const requests = {};
|
||||||
|
|
||||||
@@ -14,11 +76,13 @@ const ImageLoader = {
|
|||||||
abort(requestId: number) {
|
abort(requestId: number) {
|
||||||
let image = requests[`${requestId}`];
|
let image = requests[`${requestId}`];
|
||||||
if (image) {
|
if (image) {
|
||||||
image.onerror = image.onload = image = null;
|
image.onerror = null;
|
||||||
|
image.onload = null;
|
||||||
|
image = null;
|
||||||
delete requests[`${requestId}`];
|
delete requests[`${requestId}`];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSize(uri, success, failure) {
|
getSize(uri: string, success: Function, failure: Function) {
|
||||||
let complete = false;
|
let complete = false;
|
||||||
const interval = setInterval(callback, 16);
|
const interval = setInterval(callback, 16);
|
||||||
const requestId = ImageLoader.load(uri, callback, errorCallback);
|
const requestId = ImageLoader.load(uri, callback, errorCallback);
|
||||||
@@ -46,13 +110,16 @@ const ImageLoader = {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load(uri, onLoad, onError): number {
|
has(uri: string) {
|
||||||
|
return ImageUriCache.has(uri);
|
||||||
|
},
|
||||||
|
load(uri: string, onLoad: Function, onError: Function): number {
|
||||||
id += 1;
|
id += 1;
|
||||||
const image = new window.Image();
|
const image = new window.Image();
|
||||||
image.onerror = onError;
|
image.onerror = onError;
|
||||||
image.onload = e => {
|
image.onload = e => {
|
||||||
// avoid blocking the main thread
|
// avoid blocking the main thread
|
||||||
const onDecode = () => onLoad(e);
|
const onDecode = () => onLoad();
|
||||||
if (typeof image.decode === 'function') {
|
if (typeof image.decode === 'function') {
|
||||||
// Safari currently throws exceptions when decoding svgs.
|
// Safari currently throws exceptions when decoding svgs.
|
||||||
// We want to catch that error and allow the load handler
|
// We want to catch that error and allow the load handler
|
||||||
@@ -66,10 +133,29 @@ const ImageLoader = {
|
|||||||
requests[`${id}`] = image;
|
requests[`${id}`] = image;
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
prefetch(uri): Promise {
|
prefetch(uri: string): Promise<*> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ImageLoader.load(uri, resolve, reject);
|
ImageLoader.load(
|
||||||
|
uri,
|
||||||
|
() => {
|
||||||
|
// Add the uri to the cache so it can be immediately displayed when used
|
||||||
|
// but also immediately remove it to correctly reflect that it has no active references
|
||||||
|
ImageUriCache.add(uri);
|
||||||
|
ImageUriCache.remove(uri);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
queryCache(uris: Array<string>): Object {
|
||||||
|
const result = {};
|
||||||
|
uris.forEach(u => {
|
||||||
|
if (ImageUriCache.has(u)) {
|
||||||
|
result[u] = 'disk/memory';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user