From ffa28481f350edef88b46d3ef2e8481498f12834 Mon Sep 17 00:00:00 2001 From: Morgan Hall Date: Sat, 16 Nov 2019 18:05:06 +1300 Subject: [PATCH 1/3] add setNativeProps to Group, Shape, Text - new `translatePropsToNativeProps` helper to return required nativeProps translated from props - change render method of ``, ``, `` to use the method above to translate props to nativeProps - implement `setNativeProps` in the above three components, using the same helper to translate changed and existing props into nativeProps. --- lib/Group.js | 32 +++++++++++----- lib/Shape.js | 54 ++++++++++++++++----------- lib/Text.js | 67 +++++++++++++++++---------------- lib/__tests__/helpers.test.js | 70 +++++++++++++++++++++++++++++++++++ lib/helpers.js | 56 ++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 61 deletions(-) diff --git a/lib/Group.js b/lib/Group.js index 24d32dc..df421b6 100644 --- a/lib/Group.js +++ b/lib/Group.js @@ -11,31 +11,45 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import invariant from 'invariant'; import {NativeGroup} from './nativeComponents'; -import {extractOpacity, extractTransform} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type {OpacityProps, TransformProps} from './types'; -type GroupProps = OpacityProps & +export type GroupProps = OpacityProps & TransformProps & { children: React.Node, }; +const nativePropList = ['opacity', 'transform']; + export default class Group extends React.Component { static contextTypes = { isInSurface: PropTypes.bool.isRequired, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: GroupProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { invariant( this.context.isInSurface, 'ART: must be a child of a ', ); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); - return ( - - {this.props.children} - - ); + return {this.props.children}; } } diff --git a/lib/Shape.js b/lib/Shape.js index fe2acea..7f51428 100644 --- a/lib/Shape.js +++ b/lib/Shape.js @@ -10,15 +10,7 @@ import * as React from 'react'; import {NativeShape} from './nativeComponents'; import Path from './ARTSerializablePath'; -import { - extractTransform, - extractOpacity, - childrenAsString, - extractColor, - extractStrokeJoin, - extractStrokeCap, - extractBrush, -} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type { TransformProps, OpacityProps, @@ -42,6 +34,18 @@ export type ShapeProps = TransformProps & height: number, }; +const nativePropList = [ + 'd', + 'fill', + 'opacity', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', +]; + export default class Shape extends React.Component { static defaultProps = { strokeWidth: 1, @@ -49,22 +53,30 @@ export default class Shape extends React.Component { height: 0, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: ShapeProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { - const props = this.props; - const path = props.d || childrenAsString(props.children); - const d = (path instanceof Path ? path : new Path(path)).toJSON(); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); return ( (this._rootComponent = component)} + {...nativeProps} /> ); } diff --git a/lib/Text.js b/lib/Text.js index 55ee318..312b88a 100644 --- a/lib/Text.js +++ b/lib/Text.js @@ -10,17 +10,7 @@ import * as React from 'react'; import Path from './ARTSerializablePath'; import {NativeText} from './nativeComponents'; -import { - extractBrush, - extractOpacity, - extractColor, - extractStrokeCap, - extractStrokeJoin, - extractTransform, - extractAlignment, - childrenAsString, - extractFontAndLines, -} from './helpers'; +import {translatePropsToNativeProps} from './helpers'; import type { TransformProps, OpacityProps, @@ -47,6 +37,20 @@ export type TextProps = TransformProps & path?: string | Path, }; +const nativePropList = [ + 'alignment', + 'frame', + 'fill', + 'opacity', + 'path', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', +]; + export default class Text extends React.Component { static defaultProps = { strokeWidth: 1, @@ -54,29 +58,30 @@ export default class Text extends React.Component { height: 0, }; + _rootComponent = null; + + setNativeProps(newProps: $Shape) { + if (this._rootComponent && newProps) { + // newProps will only include what is changed, but we need existing props for some translations: + const nativeProps = translatePropsToNativeProps( + ({ + ...this.props, + ...newProps, + }: TextProps), + nativePropList, + ); + // $FlowFixMe + this._rootComponent.setNativeProps(nativeProps); + } + } + render() { - const props = this.props; - const path = props.path; - const textPath = path - ? (path instanceof Path ? path : new Path(path)).toJSON() - : null; - const textFrame = extractFontAndLines( - props.font, - childrenAsString(props.children), - ); + const nativeProps = translatePropsToNativeProps(this.props, nativePropList); + return ( (this._rootComponent = component)} + {...nativeProps} /> ); } diff --git a/lib/__tests__/helpers.test.js b/lib/__tests__/helpers.test.js index f787a73..49e520e 100644 --- a/lib/__tests__/helpers.test.js +++ b/lib/__tests__/helpers.test.js @@ -6,6 +6,7 @@ import { extractStrokeJoin, extractStrokeCap, extractAlignment, + translatePropsToNativeProps, } from '../helpers'; describe('testing childrenAsString function', () => { @@ -78,3 +79,72 @@ describe('testing extractAlignment', () => { expect(extractAlignment()).toBe(LEFT); }); }); + +describe('testing translatePropsToNativeProps', () => { + it('returns correct data for each prop', () => { + // asserting the values in the returned object will fail if any of our helpers + // are changed, but this is required to ensure the correct arguments are passed to + // each of them at the right key: + const props = { + alignment: 'center', + d: 'M 10,30 A 20,20 z', + fill: '#D9003A', + font: 'trebuchet', + height: 100, + opacity: 0.5, + stroke: '#2DCD71', + strokeCap: 'butt', + strokeDash: 2, + strokeJoin: 'miter', + strokeWidth: 3, + scale: 1.1, + width: 200, + }; + const nativeProps1 = translatePropsToNativeProps(props, [ + 'alignment', + 'd', + 'fill', + 'frame', + 'opacity', + 'stroke', + 'strokeCap', + 'strokeDash', + 'strokeJoin', + 'strokeWidth', + 'transform', + ]); + expect(nativeProps1).toEqual({ + alignment: 2, + d: [0, 10, 30], + fill: [0, 0.8509803921568627, 0, 0.22745098039215686, 1], + frame: { + font: { + fontFamily: 'trebuchet', + fontSize: 12, + fontStyle: 'normal', + fontWeight: 'normal', + }, + lines: [''], + }, + opacity: 0.5, + stroke: '#2dcd71', + strokeCap: 0, + strokeDash: 2, + strokeJoin: 0, + strokeWidth: 3, + transform: [1.1, 0, 0, 1.1, 0, 0], + }); + + // test that we can select only a subset of nativeProps: + const nativeProps2 = translatePropsToNativeProps(props, [ + 'fill', + 'opacity', + 'stroke', + ]); + expect(nativeProps2).toEqual({ + fill: [0, 0.8509803921568627, 0, 0.22745098039215686, 1], + opacity: 0.5, + stroke: '#2dcd71', + }); + }); +}); diff --git a/lib/helpers.js b/lib/helpers.js index 136d377..b524be8 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -20,6 +20,10 @@ import type { StrokeJoin, TransformProps, } from './types'; +import Path from './ARTSerializablePath'; +import type {ShapeProps} from './Shape'; +import type {GroupProps} from './Group'; +import type {TextProps} from './Text'; export function childrenAsString(children?: string | Array) { if (!children) { @@ -311,3 +315,55 @@ export function insertDoubleColorStopsIntoArray( lastIndex = insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, false); insertOffsetsIntoArray(stops, targetArray, lastIndex, 0.5, true); } + +// a lookup of the methods we use to translate props to native props +export const propsToNativePropsTranslations = { + alignment: ({alignment}: {alignment: Alignment}) => + extractAlignment(alignment), + d: ({children, d}: {children?: string | Array, d: string | Path}) => { + const path = d || childrenAsString(children); + return (path instanceof Path ? path : new Path(path)).toJSON(); + }, + fill: ({ + fill, + height, + width, + }: { + fill?: Brush | string, + height: number, + width: number, + }) => extractBrush(fill, {height, width}), + frame: ({ + children, + font, + }: { + children?: string | Array, + font?: string | Font, + }) => extractFontAndLines(font, childrenAsString(children)), + opacity: extractOpacity, + stroke: ({stroke}: {stroke: ColorType}) => extractColor(stroke), + strokeCap: ({strokeCap}: {strokeCap: StrokeCap}) => + extractStrokeCap(strokeCap), + strokeDash: ({strokeDash}: {strokeDash: Array}) => strokeDash || null, + strokeJoin: ({strokeJoin}: {strokeJoin: StrokeJoin}) => + extractStrokeJoin(strokeJoin), + strokeWidth: ({strokeWidth}: {strokeWidth: number}) => strokeWidth, + transform: extractTransform, + path: ({path}: {path: string | Path}) => + path ? (path instanceof Path ? path : new Path(path)).toJSON() : null, +}; + +// take props, and an array of the required nativeProps, and return an object with +// values for those nativeProps +export function translatePropsToNativeProps( + props: GroupProps | ShapeProps | TextProps, + requiredProps: Array, +) { + return requiredProps.reduce( + (nativeProps, prop) => ({ + ...nativeProps, + [prop]: propsToNativePropsTranslations[prop](props), + }), + {}, + ); +} From c69c50196e967581b27ba70625159da0a1a78f06 Mon Sep 17 00:00:00 2001 From: Morgan Hall Date: Mon, 23 Dec 2019 11:48:25 +1300 Subject: [PATCH 2/3] Add an example of animating a Shape to the example App A simple 3-flashing circle animation to demonstrate animation. Note: this animation would still be possible without the addition of `setNativeProps`, however it would trigger a rerender of the Shape component for each step of the animation. --- example/App.js | 2 + example/components/AnimatedCircles.js | 75 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 example/components/AnimatedCircles.js diff --git a/example/App.js b/example/App.js index 38fb947..2107c21 100644 --- a/example/App.js +++ b/example/App.js @@ -4,6 +4,7 @@ import {StyleSheet, ScrollView, SafeAreaView} from 'react-native'; import Heart from './components/Heart'; import CustomShape from './components/CustomShape'; import CustomText from './components/CustomText'; +import AnimatedCircles from './components/AnimatedCircles'; export default function App() { return ( @@ -12,6 +13,7 @@ export default function App() { + ); diff --git a/example/components/AnimatedCircles.js b/example/components/AnimatedCircles.js new file mode 100644 index 0000000..31a77c0 --- /dev/null +++ b/example/components/AnimatedCircles.js @@ -0,0 +1,75 @@ +// @flow +import React from 'react'; +import {Animated, Easing, StyleSheet, Dimensions} from 'react-native'; +import {Surface, Shape, Group} from '@react-native-community/art'; + +/* +An example of Animated Shapes + +Animated uses 'setNativeProps' on the AnimatedShape, preventing rerendering +for each step in the animation +*/ + +const CIRCLE = 'M 25, 50 a 25,25 0 1,1 50,0 a 25,25 0 1,1 -50,0'; + +export default function AnimatedCircles() { + const surfaceWidth = Dimensions.get('window').width; + + const AnimatedShape = Animated.createAnimatedComponent(Shape); + + const [animatedOpacity] = React.useState(new Animated.Value(1)); + + const blink = React.useCallback( + toValue => + Animated.timing(animatedOpacity, { + duration: 900, + easing: Easing.linear, + toValue, + }).start(() => blink(toValue === 0 ? 1 : 0)), + [animatedOpacity], + ); + + React.useEffect(() => { + blink(0); + }, [blink]); + + return ( + + + + + + + + + + + + ); +} +const styles = StyleSheet.create({ + surface: { + backgroundColor: '#000', + }, +}); From 008526eaab781607994c577c61b76a9ff748c11c Mon Sep 17 00:00:00 2001 From: Morgan Hall Date: Mon, 23 Dec 2019 12:35:27 +1300 Subject: [PATCH 3/3] add new shadow props to helper --- lib/helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/helpers.js b/lib/helpers.js index cef08df..45a56c9 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -385,6 +385,7 @@ export const propsToNativePropsTranslations = { font?: string | Font, }) => extractFontAndLines(font, childrenAsString(children)), opacity: extractOpacity, + shadow: extractShadow, stroke: ({stroke}: {stroke: ColorType}) => extractColor(stroke), strokeCap: ({strokeCap}: {strokeCap: StrokeCap}) => extractStrokeCap(strokeCap),