Skip to content

Commit

Permalink
Shared style (#1470)
Browse files Browse the repository at this point in the history
## Description
I added the possibility to use one instance of `useAnimationStyle()` for many components. `AnimationStyle` is connected with view by ViewDescriptor object. I changed this to a set of `ViewDescriptors` and now `AnimationStyle` can be shared between many components. 

Fixes #1263 

## Example of code
<details>
<summary>code</summary>

```Js
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
} from 'react-native-reanimated';
import { Button, ScrollView } from 'react-native';
import React from 'react';

export default function SharedAnimatedStyleUpdateExample(props) {
  const randomWidth = useSharedValue(10);

  const config = {
    duration: 500,
    easing: Easing.bezier(0.5, 0.01, 0, 1),
  };

  const style = useAnimatedStyle(() => {
    return {
      width: withTiming(randomWidth.value, config),
    };
  });

  const renderList = () => {
    const items = [];
    for (let i = 0; i < 100; i++) {
      items.push(
        <Animated.View
          key={i}
          style={[
            { width: 100, height: 5, backgroundColor: 'black', margin: 1 },
            style,
          ]}
        />
      );
    }
    return items;
  };

  return (
    <ScrollView
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Button
        title="toggle"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
      {renderList()}
    </ScrollView>
  );
}
```

</details>

<details>
<summary>code2</summary>

```Js
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
} from 'react-native-reanimated';
import { View, Button } from 'react-native';
import React, { useState } from 'react';

export default function AnimatedStyleUpdateExample(props) {
  const randomWidth = useSharedValue(10);

  const [counter, setCounter] = useState(0);
  const [counter2, setCounter2] = useState(0);
  const [itemList, setItemList] = useState([]);
  const [toggleState, setToggleState] = useState(false);

  const config = {
    duration: 500,
    easing: Easing.bezier(0.5, 0.01, 0, 1),
  };

  const style = useAnimatedStyle(() => {
    return {
      width: withTiming(randomWidth.value, config),
    };
  });

  const staticObject = <Animated.View
    style={[
      { width: 100, height: 8, backgroundColor: 'black', margin: 1 },
      style,
    ]}
  />

  const renderItems = () => {
    let output = []
    for(let i = 0; i < counter; i++) {
      output.push(
        <Animated.View
        key={i + 'a'}
          style={[
            { width: 100, height: 8, backgroundColor: 'blue', margin: 1 },
            style,
          ]}
        />
      )
    }
    return output
  }

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Button
        title="animate"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
      <Button
        title="increment counter"
        onPress={() => {
          setCounter(counter + 1)
        }}
      />
      <Button
        title="add item to static lists"
        onPress={() => {
          setCounter2(counter2 + 1)
          setItemList([...itemList, <Animated.View
            key={counter2 + 'b'}
            style={[
              { width: 100, height: 8, backgroundColor: 'green', margin: 1 },
              style,
            ]}
          />])
        }}
      />
      <Button
        title="toggle state"
        onPress={() => {
          setToggleState(!toggleState)
        }}
      />
      <Animated.View
        style={[
          { width: 100, height: 8, backgroundColor: 'orange', margin: 1 },
          style,
        ]}
      />
      {toggleState && <Animated.View
        style={[
          { width: 100, height: 8, backgroundColor: 'black', margin: 1 },
          style,
        ]}
      />}
      {toggleState && staticObject}
      {renderItems()}
      {itemList}
    </View>
  );
}

```

</details>

## Change
before changes:
![1](https://user-images.githubusercontent.com/36106620/102333064-3100b280-3f8d-11eb-9ad8-c61facd2d1af.gif)

final result:
![3](https://user-images.githubusercontent.com/36106620/102333162-5392cb80-3f8d-11eb-9d71-e2b800385098.gif)
  • Loading branch information
piaskowyk committed Jul 19, 2021
1 parent f9e5db9 commit 0c2f66f
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 112 deletions.
16 changes: 6 additions & 10 deletions Common/cpp/NativeModules/NativeReanimatedModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ jsi::Value NativeReanimatedModule::startMapper(
const jsi::Value &inputs,
const jsi::Value &outputs,
const jsi::Value &updater,
const jsi::Value &tag,
const jsi::Value &name
)
const jsi::Value &viewDescriptors)
{
static unsigned long MAPPER_ID = 1;

Expand All @@ -136,8 +134,7 @@ jsi::Value NativeReanimatedModule::startMapper(
optimalizationLvl = optimalization.asNumber();
}
auto updaterSV = ShareableValue::adapt(rt, updater, this);
const int tagInt = tag.asNumber();
const std::string nameStr = name.asString(rt).utf8(rt);
auto viewDescriptorsSV = ShareableValue::adapt(rt, viewDescriptors, this);

scheduler->scheduleOnUI([=] {
auto mapperFunction = mapperShareable->getValue(*runtime).asObject(*runtime).asFunction(*runtime);
Expand All @@ -148,12 +145,11 @@ jsi::Value NativeReanimatedModule::startMapper(
newMapperId,
mapperFunctionPointer,
inputMutables,
outputMutables,
updaterSV,
tagInt,
nameStr,
optimalizationLvl
outputMutables
);
if(optimalizationLvl > 0) {
mapperPointer->enableFastMode(optimalizationLvl, updaterSV, viewDescriptorsSV);
}
mapperRegistry->startMapper(mapperPointer);
maybeRequestRender();
});
Expand Down
5 changes: 2 additions & 3 deletions Common/cpp/NativeModules/NativeReanimatedModuleSpec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ static jsi::Value __hostFunction_NativeReanimatedModuleSpec_startMapper(
std::move(args[1]),
std::move(args[2]),
std::move(args[3]),
std::move(args[4]),
std::move(args[5]));
std::move(args[4]));
}

static jsi::Value __hostFunction_NativeReanimatedModuleSpec_stopMapper(
Expand Down Expand Up @@ -110,7 +109,7 @@ NativeReanimatedModuleSpec::NativeReanimatedModuleSpec(std::shared_ptr<CallInvok


methodMap_["startMapper"] = MethodMetadata{
6, __hostFunction_NativeReanimatedModuleSpec_startMapper};
5, __hostFunction_NativeReanimatedModuleSpec_startMapper};
methodMap_["stopMapper"] = MethodMetadata{
1, __hostFunction_NativeReanimatedModuleSpec_stopMapper};

Expand Down
49 changes: 33 additions & 16 deletions Common/cpp/Tools/Mapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,13 @@ Mapper::Mapper(NativeReanimatedModule *module,
unsigned long id,
std::shared_ptr<jsi::Function> mapper,
std::vector<std::shared_ptr<MutableValue>> inputs,
std::vector<std::shared_ptr<MutableValue>> outputs,
std::shared_ptr<ShareableValue> updater,
const int viewTag,
const std::string& viewName,
const int optimalizationLvl):
std::vector<std::shared_ptr<MutableValue>> outputs):
id(id),
module(module),
mapper(mapper),
inputs(inputs),
outputs(outputs),
viewTag(viewTag),
optimalizationLvl(optimalizationLvl)
outputs(outputs)
{
jsi::Runtime* rt = module->runtime.get();
this->viewName = jsi::Value(*rt, jsi::String::createFromUtf8(*rt, viewName));
updateProps = &module->updaterFunction;
userUpdater = std::make_shared<jsi::Function>(updater->getValue(*rt).asObject(*rt).asFunction(*rt));


auto markDirty = [this, module]() {
this->dirty = true;
module->maybeRequestRender();
Expand All @@ -39,11 +27,40 @@ optimalizationLvl(optimalizationLvl)
void Mapper::execute(jsi::Runtime &rt) {
dirty = false;
if(optimalizationLvl == 0) {
mapper->callWithThis(rt, *mapper);// call styleUpdater
mapper->callWithThis(rt, *mapper); // call styleUpdater
}
else {
(*updateProps)(rt, viewTag, viewName, userUpdater->call(rt).asObject(rt));
for(auto& viewDescriptor : viewDescriptors) {
(*updateProps)(rt, viewDescriptor.tag, viewDescriptor.name, userUpdater->call(rt).asObject(rt));
}
}
}

void Mapper::enableFastMode(
const int optimalizationLvl,
const std::shared_ptr<ShareableValue>& updater,
const std::shared_ptr<ShareableValue>& jsViewDescriptors
) {
if(optimalizationLvl == 0) {
return;
}
this->optimalizationLvl = optimalizationLvl;
updateProps = &module->updaterFunction;
jsi::Runtime* rt = module->runtime.get();
userUpdater = std::make_shared<jsi::Function>(updater->getValue(*rt).asObject(*rt).asFunction(*rt));
auto jsViewDescriptorArray = jsViewDescriptors->getValue(*rt)
.getObject(*rt)
.getProperty(*rt, "value")
.asObject(*rt)
.getArray(*rt);
for(int i = 0; i < jsViewDescriptorArray.length(*rt); ++i) {
auto jsViewDescriptor = jsViewDescriptorArray.getValueAtIndex(*rt, i).getObject(*rt);
viewDescriptors.push_back(ViewDescriptor {
(int)jsViewDescriptor.getProperty(*rt, "tag").asNumber(),
jsViewDescriptor.getProperty(*rt, "name"),
});
}

}

Mapper::~Mapper() {
Expand Down
3 changes: 1 addition & 2 deletions Common/cpp/headers/NativeModules/NativeReanimatedModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec, public Runtime
const jsi::Value &inputs,
const jsi::Value &outputs,
const jsi::Value &updater,
const jsi::Value &tag,
const jsi::Value &name) override;
const jsi::Value &viewDescriptors) override;
void stopMapper(jsi::Runtime &rt, const jsi::Value &mapperId) override;

jsi::Value registerEventHandler(jsi::Runtime &rt, const jsi::Value &eventHash, const jsi::Value &worklet) override;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ class JSI_EXPORT NativeReanimatedModuleSpec : public TurboModule {
const jsi::Value &inputs,
const jsi::Value &outputs,
const jsi::Value &updater,
const jsi::Value &tag,
const jsi::Value &name) = 0;
const jsi::Value &viewDescriptors) = 0;
virtual void stopMapper(jsi::Runtime &rt, const jsi::Value &mapperId) = 0;

// events
Expand Down
19 changes: 12 additions & 7 deletions Common/cpp/headers/Tools/Mapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace reanimated {

class MapperRegistry;

struct ViewDescriptor {
int tag;
jsi::Value name;
};

class Mapper : public std::enable_shared_from_this<Mapper> {
friend MapperRegistry;
private:
Expand All @@ -22,21 +27,21 @@ class Mapper : public std::enable_shared_from_this<Mapper> {
bool dirty = true;
std::shared_ptr<jsi::Function> userUpdater;
UpdaterFunction* updateProps;
jsi::Value viewName;
int viewTag;
std::vector<ViewDescriptor> viewDescriptors;
int optimalizationLvl = 0;

public:
Mapper(NativeReanimatedModule *module,
unsigned long id,
std::shared_ptr<jsi::Function> mapper,
std::vector<std::shared_ptr<MutableValue>> inputs,
std::vector<std::shared_ptr<MutableValue>> outputs,
std::shared_ptr<ShareableValue> updater,
const int viewTag,
const std::string& viewName,
const int optimalizationLvl);
std::vector<std::shared_ptr<MutableValue>> outputs);
void execute(jsi::Runtime &rt);
void enableFastMode(
const int optimalizationLvl,
const std::shared_ptr<ShareableValue>& updater,
const std::shared_ptr<ShareableValue>&jsViewDescriptors
);
virtual ~Mapper();
};

Expand Down
2 changes: 1 addition & 1 deletion Example/src/AnimatedStyleUpdateExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ function AnimatedStyleUpdateExample(): React.ReactElement {
);
}

export default AnimatedStyleUpdateExample;
export default AnimatedStyleUpdateExample;
21 changes: 12 additions & 9 deletions plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const blacklistedFunctions = new Set([
'apply',
'call',
'__callAsync',
'includes',
]);

const possibleOptFunction = new Set(['interpolate']);
Expand Down Expand Up @@ -275,7 +276,7 @@ function buildWorkletString(t, fun, closureVariables, name) {
return generate(workletFunction, { compact: true }).code;
}

function processWorkletFunction(t, fun, fileName, options = {}) {
function processWorkletFunction(t, fun, fileName) {
if (!t.isFunctionParent(fun)) {
return;
}
Expand All @@ -284,6 +285,7 @@ function processWorkletFunction(t, fun, fileName, options = {}) {
const closure = new Map();
const outputs = new Set();
const closureGenerator = new ClosureGenerator();
const options = {};

// We use copy because some of the plugins don't update bindings and
// some even break them
Expand All @@ -302,7 +304,13 @@ function processWorkletFunction(t, fun, fileName, options = {}) {
babelrc: false,
configFile: false,
});

if (
fun.parent &&
fun.parent.callee &&
fun.parent.callee.name === 'useAnimatedStyle'
) {
options.optFlags = isPossibleOptimization(transformed.ast);
}
traverse(transformed.ast, {
ReferencedIdentifier(path) {
const name = path.node.name;
Expand Down Expand Up @@ -498,12 +506,7 @@ function processIfWorkletNode(t, fun, fileName) {
directive.value.value === 'worklet'
)
) {
const flags = isPossibleOptimization(fun);
if (flags) {
processWorkletFunction(t, fun, fileName, { optFlags: flags });
} else {
processWorkletFunction(t, fun, fileName);
}
processWorkletFunction(t, fun, fileName);
}
}
},
Expand Down Expand Up @@ -547,7 +550,7 @@ const STATEMENTLESS_FLAG = 0b00000010;
function isPossibleOptimization(fun) {
let isFunctionCall = false;
let isSteatements = false;
fun.scope.path.traverse({
traverse(fun, {
CallExpression(path) {
if (!possibleOptFunction.has(path.node.callee.name)) {
isFunctionCall = true;
Expand Down
57 changes: 39 additions & 18 deletions src/createAnimatedComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export default function createAnimatedComponent(Component, options = {}) {

class AnimatedComponent extends React.Component {
_invokeAnimatedPropsCallbackOnMount = false;
_styles = null;
_viewTag = -1;

constructor(props) {
super(props);
Expand All @@ -87,6 +89,7 @@ export default function createAnimatedComponent(Component, options = {}) {
this._detachPropUpdater();
this._propsAnimated && this._propsAnimated.__detach();
this._detachNativeEvents();
this._detachStyles();
this.sv = null;
}

Expand Down Expand Up @@ -143,6 +146,25 @@ export default function createAnimatedComponent(Component, options = {}) {
}
}

_detachStyles() {
if (Platform.OS === 'web') {
for (const style of this._styles) {
if (style.viewsRef) {
style.viewsRef.remove(this);
}
}
} else if (this._viewTag !== -1) {
for (const style of this._styles) {
if (style?.viewDescriptors) {
style.viewDescriptors.remove(this._viewTag);
}
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
}
}

_reattachNativeEvents(prevProps) {
const node = this._getEventViewRef();
const attached = new Set();
Expand Down Expand Up @@ -261,6 +283,7 @@ export default function createAnimatedComponent(Component, options = {}) {
? this.props.style
: [this.props.style];
styles = flattenArray(styles);
this._styles = styles;
let viewTag, viewName;
if (Platform.OS === 'web') {
viewTag = findNodeHandle(this);
Expand Down Expand Up @@ -289,10 +312,11 @@ export default function createAnimatedComponent(Component, options = {}) {
adaptViewConfig(hostInstance.viewConfig);
}
}
this._viewTag = viewTag;

styles.forEach((style) => {
if (style?.viewDescriptor) {
style.viewDescriptor.value = { tag: viewTag, name: viewName };
if (style?.viewDescriptors) {
style.viewDescriptors.add({ tag: viewTag, name: viewName });
if (process.env.JEST_WORKER_ID) {
/**
* We need to connect Jest's TestObject instance whose contains just props object
Expand All @@ -302,29 +326,29 @@ export default function createAnimatedComponent(Component, options = {}) {
*/
this.animatedStyle.value = {
...this.animatedStyle.value,
...style.initial,
...style.initial.value,
};
style.animatedStyle.current = this.animatedStyle;
}
}
});
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptor) {
this.props.animatedProps.viewDescriptor.value = {
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
tag: viewTag,
name: viewName,
};
});
}
}

_hasReanimated2Props(flattenStyles) {
if (this.props.animatedProps?.viewDescriptor) {
if (this.props.animatedProps?.viewDescriptors) {
return true;
}
if (this.props.style) {
for (const style of flattenStyles) {
// eslint-disable-next-line no-prototype-builtins
if (style?.hasOwnProperty('viewDescriptor')) {
if (style?.hasOwnProperty('viewDescriptors')) {
return true;
}
}
Expand All @@ -345,6 +369,7 @@ export default function createAnimatedComponent(Component, options = {}) {
this._reattachNativeEvents(prevProps);

this._propsAnimated && this._propsAnimated.setNativeView(this._component);
this._attachAnimatedStyles();
}

_setComponentRef = setAndForwardRef({
Expand Down Expand Up @@ -430,12 +455,10 @@ export default function createAnimatedComponent(Component, options = {}) {
if (key === 'style') {
const styles = Array.isArray(value) ? value : [value];
const processedStyle = styles.map((style) => {
if (style && style.viewDescriptor) {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
if (style.viewRef.current === null) {
style.viewRef.current = this;
}
return style.initial;
style.viewsRef.add(this);
return style.initial.value;
} else {
return style;
}
Expand All @@ -444,11 +467,9 @@ export default function createAnimatedComponent(Component, options = {}) {
StyleSheet.flatten(processedStyle)
);
} else if (key === 'animatedProps') {
Object.keys(value.initial).forEach((key) => {
props[key] = value.initial[key];
if (value.viewRef.current === null) {
value.viewRef.current = this;
}
Object.keys(value.initial.value).forEach((key) => {
props[key] = value.initial.value[key];
value.viewsRef.add(this);
});
} else if (value instanceof AnimatedEvent) {
// we cannot filter out event listeners completely as some components
Expand Down

0 comments on commit 0c2f66f

Please sign in to comment.