Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android hardware artifacting fix #38

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ const MyComponent = () => (

## Props

| Name | Type | Description | Required | Default |
|------------------|------------------------|--------------------------------------------|----------|----------------|
| count | number | items count to display | required | |
| origin | {x: number, y: number} | animation position origin | required | |
| explosionSpeed | number | explosion duration (ms) from origin to top | | 350 |
| fallSpeed | number | fall duration (ms) from top to bottom | | 3000 |
| fadeOut | boolean | make the confettis disappear at the end | | false |
| colors | string[] | give your own colors to the confettis | | default colors |
| autoStart | boolean | auto start the animation | | true |
| autoStartDelay | number | delay to wait before triggering animation | | 0 |
| Name | Type | Description | Required | Default |
|--------------------------------|------------------------|--------------------------------------------|----------|----------------|
| count | number | items count to display | required | |
| origin | {x: number, y: number} | animation position origin | required | |
| explosionSpeed | number | explosion duration (ms) from origin to top | | 350 |
| fallSpeed | number | fall duration (ms) from top to bottom | | 3000 |
| fadeOut | boolean | make the confettis disappear at the end | | false |
| colors | string[] | give your own colors to the confettis | | default colors |
| autoStart | boolean | auto start the animation | | true |
| autoStartDelay | number | delay to wait before triggering animation | | 0 |
| renderToHardwareTextureAndroid | boolean | enable Android hardware texture rendering | | true |

## Events

Expand Down
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module.exports = {
'node_modules/(?!(.*-)?react(.*-)?(native)(-.*)?)',
'node_modules/core-js'
],
setupFiles: [
'<rootDir>/node_modules/react-native/jest/setup.js',
'<rootDir>/jestSetup.js'
],
collectCoverage: true,
coverageReporters: ['lcov', 'text', 'html'],
collectCoverageFrom: [
Expand Down
27 changes: 27 additions & 0 deletions jestSetup.js
Original file line number Diff line number Diff line change
@@ -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');
91 changes: 85 additions & 6 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// @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 '..';

describe('index', () => {
let ref: JestMockFn<[ConfettiCannon | null], void>

beforeEach(() => {
jest.useFakeTimers();
Platform.OS = 'ios';
ref = jest.fn()
});

it('should trigger animations callbacks', () => {
Expand Down Expand Up @@ -64,7 +69,7 @@ describe('index', () => {
expect(handleAnimationEnd).toHaveBeenCalledTimes(1);
});

it('should not start is autoStart is disabled', () => {
it('should not start if autoStart is disabled', () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const handleAnimationStart = jest.fn();

renderer.create(
Expand Down Expand Up @@ -108,7 +113,6 @@ describe('index', () => {
const handleAnimationResume = jest.fn();
const handleAnimationStop = jest.fn();
const handleAnimationEnd = jest.fn();
const ref = jest.fn();

renderer.create(
<ConfettiCannon
Expand All @@ -119,28 +123,27 @@ describe('index', () => {
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);
Expand Down Expand Up @@ -230,4 +233,80 @@ 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 = 1;

const component = renderer.create(
<ConfettiCannon count={count} origin={origin} />
);
const confetti = component.root.find(el => el.props.testID === 'confetti-1');

expect(confetti.props.transform).toEqual(expect.arrayContaining([{ perspective: 100 }]));
});

it('should default "renderToHardwareTextureAndroid" prop to true once animating', () => {
const origin = {x: -10, y: 0};
const count = 1;

const component = renderer.create(
<ConfettiCannon count={count} origin={origin} ref={ref} />
);
const confetti = component.root.find(el => el.props.testID === 'confetti-1');
const confettiAnimatedView = confetti.findByType(Animated.View);

const [confettiCannon] = ref.mock.calls[0];

confettiCannon && confettiCannon.start();

expect(confetti.props.renderToHardwareTextureAndroid).toEqual(true);
expect(confettiAnimatedView.props.renderToHardwareTextureAndroid).toEqual(true);
});

it('should set "renderToHardwareTextureAndroid" prop to false once done animating', () => {
const origin = {x: -10, y: 0};
const count = 1;

const component = renderer.create(
<ConfettiCannon count={count} origin={origin} ref={ref} />
);
const confetti = component.root.find(el => el.props.testID === 'confetti-1');
const confettiAnimatedView = confetti.findByType(Animated.View);

const [confettiCannon] = ref.mock.calls[0];

confettiCannon && confettiCannon.start();

expect(confetti.props.renderToHardwareTextureAndroid).toEqual(true);
expect(confettiAnimatedView.props.renderToHardwareTextureAndroid).toEqual(true);

jest.advanceTimersByTime(DEFAULT_EXPLOSION_SPEED + DEFAULT_FALL_SPEED);

expect(confetti.props.renderToHardwareTextureAndroid).toEqual(false);
expect(confettiAnimatedView.props.renderToHardwareTextureAndroid).toEqual(false);
});

it('should accept "renderToHardwareTextureAndroid = false" prop', () => {
const origin = {x: -10, y: 0};
const count = 1;

const component = renderer.create(
<ConfettiCannon
count={count}
origin={origin}
renderToHardwareTextureAndroid={false}
ref={ref} />
);
const confetti = component.root.find(el => el.props.testID === 'confetti-1');
const confettiAnimatedView = confetti.findByType(Animated.View);

const [confettiCannon] = ref.mock.calls[0];

confettiCannon && confettiCannon.start();

expect(confetti.props.renderToHardwareTextureAndroid).toEqual(false);
expect(confettiAnimatedView.props.renderToHardwareTextureAndroid).toEqual(false);
});
});
11 changes: 8 additions & 3 deletions src/components/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ type Interpolations = Array<{
translateY?: Animated.Interpolation,
rotate?: Animated.Interpolation,
rotateX?: Animated.Interpolation,
rotateY?: Animated.Interpolation
rotateY?: Animated.Interpolation,
perspective?: number
}>;

type Props = {|
containerTransform: Interpolations,
transform: Interpolations,
color: string,
opacity: Animated.Interpolation,
renderToHardwareTextureAndroid?: boolean,
testID?: string
|};

Expand All @@ -28,13 +30,16 @@ class Confetti extends React.PureComponent<Props> {
isRounded: boolean = Math.round(randomValue(0, 1)) === 1;

render() {
const { containerTransform, transform, opacity, color } = this.props;
const { containerTransform, transform, opacity, color, renderToHardwareTextureAndroid } = this.props;
const { width, height, isRounded } = this;
const containerStyle = { transform: containerTransform };
const style = { width, height, backgroundColor: color, transform, opacity};

return (
<Animated.View style={[styles.confetti, containerStyle]}>
<Animated.View
pointerEvents="none"
renderToHardwareTextureAndroid={renderToHardwareTextureAndroid}
style={[styles.confetti, containerStyle]}>
<Animated.View style={[isRounded && styles.rounded, style]} />
</Animated.View>
);
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ExplosionProps {
fadeOut?: boolean;
autoStart?: boolean;
autoStartDelay?: number;
renderToHardwareTextureAndroid?: boolean;
VincentCATILLON marked this conversation as resolved.
Show resolved Hide resolved
onAnimationStart?: () => void;
onAnimationResume?: () => void;
onAnimationStop?: () => void;
Expand Down
27 changes: 19 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -20,6 +20,7 @@ type Props = {|
fadeOut?: boolean,
autoStart?: boolean,
autoStartDelay?: number,
renderToHardwareTextureAndroid?: boolean,
VincentCATILLON marked this conversation as resolved.
Show resolved Hide resolved
onAnimationStart?: () => void,
onAnimationResume?: () => void,
onAnimationStop?: () => void,
Expand All @@ -40,7 +41,8 @@ type Item = {|
|};

type State = {|
items: Array<Item>
items: Array<Item>,
isAnimating: boolean
|};

export const TOP_MIN = 0.7;
Expand All @@ -63,7 +65,8 @@ export const DEFAULT_FALL_SPEED = 3000;
class Explosion extends React.PureComponent<Props, State> {
props: Props;
state: State = {
items: []
items: [],
isAnimating: false
};
start: () => void;
resume: () => void;
Expand Down Expand Up @@ -158,12 +161,16 @@ class Explosion extends React.PureComponent<Props, State> {
]);

onAnimationStart && onAnimationStart();

this.setState({ isAnimating: true });
}

this.sequence && this.sequence.start(({finished}: EndResult) => {
if (finished) {
onAnimationEnd && onAnimationEnd();
}
if (!finished) return;

this.setState({ isAnimating: false });

onAnimationEnd && onAnimationEnd();
});
};

Expand All @@ -178,8 +185,8 @@ class Explosion extends React.PureComponent<Props, State> {
};

render() {
const { origin, fadeOut } = this.props;
const { items } = this.state;
const { origin, fadeOut, renderToHardwareTextureAndroid = true } = this.props;
const { items, isAnimating } = this.state;
const { height, width } = Dimensions.get('window');

return (
Expand Down Expand Up @@ -215,13 +222,17 @@ class Explosion extends React.PureComponent<Props, State> {
});
const containerTransform = [{translateX: left}, {translateY: top}];
const transform = [{rotateX}, {rotateY}, {rotate: rotateZ}, {translateX}];
if (Platform.OS === 'android') {
leezumstein marked this conversation as resolved.
Show resolved Hide resolved
transform.push({ perspective: 100 });
VincentCATILLON marked this conversation as resolved.
Show resolved Hide resolved
}

return (
<Confetti
color={item.color}
containerTransform={containerTransform}
transform={transform}
opacity={opacity}
renderToHardwareTextureAndroid={renderToHardwareTextureAndroid && isAnimating}
VincentCATILLON marked this conversation as resolved.
Show resolved Hide resolved
key={index}
testID={`confetti-${index + 1}`}
/>
Expand Down