From 28027136dd1e378af6682e0471c1d87946285c02 Mon Sep 17 00:00:00 2001 From: Lee Zumstein Date: Wed, 3 Mar 2021 06:49:50 -0500 Subject: [PATCH] Android hardware artifacting fix (#38) * Add perspective to transform array for Android platform * Add renderToHardwareTextureAndroid prop to fix artifacting on some Android devices * Add missing semicolons * Only assign renderToHardwareTextureAndroid = true while animating * Apply suggestions from code review Co-authored-by: Vincent Catillon * Remove renderToHardwareTextureAndroid from top level Explosion prop * Remove state based renderToHardwareTextureAndroid enabled Co-authored-by: Vincent Catillon --- jest.config.js | 4 ++++ jestSetup.js | 27 ++++++++++++++++++++++++ src/__tests__/index.js | 42 ++++++++++++++++++++++++++++++++------ src/components/confetti.js | 8 ++++++-- src/index.js | 6 +++++- 5 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 jestSetup.js diff --git a/jest.config.js b/jest.config.js index 25bb3a3..96b66fe 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,10 @@ module.exports = { 'node_modules/(?!(.*-)?react(.*-)?(native)(-.*)?)', 'node_modules/core-js' ], + setupFiles: [ + '/node_modules/react-native/jest/setup.js', + '/jestSetup.js' + ], collectCoverage: true, coverageReporters: ['lcov', 'text', 'html'], collectCoverageFrom: [ diff --git a/jestSetup.js b/jestSetup.js new file mode 100644 index 0000000..1ca4465 --- /dev/null +++ b/jestSetup.js @@ -0,0 +1,27 @@ +// @flow + +jest.mock('react-native', () => { + const ReactNative = jest.requireActual('react-native'); + const { Platform } = ReactNative; + + jest.spyOn(Platform, 'select'); + const MockPlatform = { + ...Platform, + OS: 'ios', + }; + Platform.select.mockImplementation(specifics => { + const { OS } = MockPlatform + if (OS in specifics) { + return specifics[OS]; + } else if ('default' in specifics) { + return specifics.default; + } + return undefined; + }) + + return Object.setPrototypeOf({ + Platform: MockPlatform, + }, ReactNative); +}); + +jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); diff --git a/src/__tests__/index.js b/src/__tests__/index.js index 61f219a..e505e1c 100644 --- a/src/__tests__/index.js +++ b/src/__tests__/index.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; +import { Animated, Platform } from 'react-native'; import renderer from 'react-test-renderer'; import ConfettiCannon, {DEFAULT_EXPLOSION_SPEED, DEFAULT_FALL_SPEED} from '..'; @@ -8,6 +9,7 @@ import ConfettiCannon, {DEFAULT_EXPLOSION_SPEED, DEFAULT_FALL_SPEED} from '..'; describe('index', () => { beforeEach(() => { jest.useFakeTimers(); + Platform.OS = 'ios'; }); it('should trigger animations callbacks', () => { @@ -64,7 +66,7 @@ describe('index', () => { expect(handleAnimationEnd).toHaveBeenCalledTimes(1); }); - it('should not start is autoStart is disabled', () => { + it('should not start if autoStart is disabled', () => { const handleAnimationStart = jest.fn(); renderer.create( @@ -108,7 +110,7 @@ describe('index', () => { const handleAnimationResume = jest.fn(); const handleAnimationStop = jest.fn(); const handleAnimationEnd = jest.fn(); - const ref = jest.fn(); + const ref = jest.fn<[ConfettiCannon | null], void>(); renderer.create( { onAnimationResume={handleAnimationResume} onAnimationStop={handleAnimationStop} onAnimationEnd={handleAnimationEnd} - // $FlowFixMe this is a mock ref={ref} /> ); const [confettiCannon] = ref.mock.calls[0]; - confettiCannon.start(); + confettiCannon && confettiCannon.start(); expect(handleAnimationStart).toHaveBeenCalledTimes(1); expect(handleAnimationResume).toHaveBeenCalledTimes(0); expect(handleAnimationStop).toHaveBeenCalledTimes(0); expect(handleAnimationEnd).toHaveBeenCalledTimes(0); - confettiCannon.stop(); + confettiCannon && confettiCannon.stop(); expect(handleAnimationStart).toHaveBeenCalledTimes(1); expect(handleAnimationResume).toHaveBeenCalledTimes(0); expect(handleAnimationStop).toHaveBeenCalledTimes(1); expect(handleAnimationEnd).toHaveBeenCalledTimes(0); - confettiCannon.resume(); + confettiCannon && confettiCannon.resume(); expect(handleAnimationStart).toHaveBeenCalledTimes(1); expect(handleAnimationResume).toHaveBeenCalledTimes(1); @@ -230,4 +231,33 @@ describe('index', () => { expect(confettis1).toEqual(confettis2); }); + + it('should include the perspective transform on the Android platform', () => { + Platform.OS = 'android'; + + const origin = {x: -10, y: 0}; + const count = 1000; + + const component = renderer.create( + + ); + const confetti = component.root.find(el => el.props.testID === 'confetti-1'); + + expect(confetti.props.transform).toEqual(expect.arrayContaining([{ perspective: 100 }])); + }); + + it('should set "renderToHardwareTextureAndroid" prop to true for confetti animated view', () => { + const origin = {x: -10, y: 0}; + const count = 1000; + + const component = renderer.create( + + ); + + const confettiAnimatedView = component.root + .find(el => el.props.testID === 'confetti-1') + .findByType(Animated.View); + + expect(confettiAnimatedView.props.renderToHardwareTextureAndroid).toEqual(true); + }); }); diff --git a/src/components/confetti.js b/src/components/confetti.js index 7d9dc2b..2bb1dfe 100644 --- a/src/components/confetti.js +++ b/src/components/confetti.js @@ -10,7 +10,8 @@ type Interpolations = Array<{ translateY?: Animated.Interpolation, rotate?: Animated.Interpolation, rotateX?: Animated.Interpolation, - rotateY?: Animated.Interpolation + rotateY?: Animated.Interpolation, + perspective?: number }>; type Props = {| @@ -34,7 +35,10 @@ class Confetti extends React.PureComponent { const style = { width, height, backgroundColor: color, transform, opacity}; return ( - + ); diff --git a/src/index.js b/src/index.js index 5661618..7092e70 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import { Animated, Dimensions, Easing } from 'react-native'; +import { Animated, Dimensions, Easing, Platform } from 'react-native'; import type { CompositeAnimation } from 'react-native/Libraries/Animated/src/AnimatedImplementation'; import type { EndResult } from 'react-native/Libraries/Animated/src/animations/Animation'; @@ -216,6 +216,10 @@ class Explosion extends React.PureComponent { const containerTransform = [{translateX: left}, {translateY: top}]; const transform = [{rotateX}, {rotateY}, {rotate: rotateZ}, {translateX}]; + if (Platform.OS === 'android') { + transform.push({ perspective: 100 }); + } + return (