From 3f49e6763e66447f6ae17dc2f032e27330b7b74a Mon Sep 17 00:00:00 2001 From: Genki Kondo Date: Sat, 29 Jan 2022 13:50:34 -0800 Subject: [PATCH] Support color animation with native driver for Android Summary: Adds support for Animated.Color with native driver for Android. Reads the native config for the rbga channel AnimatedNodes, and on update(), converts the values into an integer (0xaarrggbb) Followup changes will include support for iOS and platform colors. Changelog: [Android][Added] - Support running animations with AnimatedColor with native driver Reviewed By: javache Differential Revision: D33833600 fbshipit-source-id: 2bf05c9715b603cf014ace09e9308b2bfd67f30a --- .../Animated/animations/TimingAnimation.js | 9 +-- Libraries/Animated/nodes/AnimatedColor.js | 21 +++++- .../java/com/facebook/react/animated/BUCK | 1 + .../react/animated/ColorAnimatedNode.java | 66 +++++++++++++++++++ .../animated/NativeAnimatedNodesManager.java | 2 + .../react/animated/PropsAnimatedNode.java | 2 + .../react/animated/StyleAnimatedNode.java | 2 + .../facebook/react/views/view/ColorUtil.java | 19 ++++++ .../test/java/com/facebook/react/views/BUCK | 7 +- .../react/views/view/ColorUtilTest.java | 9 +++ 10 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.java diff --git a/Libraries/Animated/animations/TimingAnimation.js b/Libraries/Animated/animations/TimingAnimation.js index 3ed0e0f1288..748bb47cc6f 100644 --- a/Libraries/Animated/animations/TimingAnimation.js +++ b/Libraries/Animated/animations/TimingAnimation.js @@ -19,6 +19,7 @@ const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {AnimationConfig, EndCallback} from './Animation'; +import type {RgbaValue} from '../nodes/AnimatedColor'; import AnimatedColor from '../nodes/AnimatedColor'; @@ -33,13 +34,7 @@ export type TimingAnimationConfig = { ... } | AnimatedValueXY - | { - r: number, - g: number, - b: number, - a: number, - ... - } + | RgbaValue | AnimatedColor | AnimatedInterpolation, easing?: (value: number) => number, diff --git a/Libraries/Animated/nodes/AnimatedColor.js b/Libraries/Animated/nodes/AnimatedColor.js index 1178a45b24c..4a5cdc5d748 100644 --- a/Libraries/Animated/nodes/AnimatedColor.js +++ b/Libraries/Animated/nodes/AnimatedColor.js @@ -15,11 +15,12 @@ import AnimatedWithChildren from './AnimatedWithChildren'; import normalizeColor from '../../StyleSheet/normalizeColor'; import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes'; +import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {NativeColorValue} from '../../StyleSheet/PlatformColorValueTypes'; type ColorListenerCallback = (value: string) => mixed; -type RgbaValue = { +export type RgbaValue = { +r: number, +g: number, +b: number, @@ -263,4 +264,22 @@ export default class AnimatedColor extends AnimatedWithChildren { this.a.__removeChild(this); super.__detach(); } + + __makeNative(platformConfig: ?PlatformConfig) { + this.r.__makeNative(platformConfig); + this.g.__makeNative(platformConfig); + this.b.__makeNative(platformConfig); + this.a.__makeNative(platformConfig); + super.__makeNative(platformConfig); + } + + __getNativeConfig(): {...} { + return { + type: 'color', + r: this.r.__getNativeTag(), + g: this.g.__getNativeTag(), + b: this.b.__getNativeTag(), + a: this.a.__getNativeTag(), + }; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK index 1f4e245b01f..2a61c4707f6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/BUCK @@ -28,6 +28,7 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/core:core"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/uimanager/annotations:annotations"), + react_native_target("java/com/facebook/react/views/view:view"), ], exported_deps = [react_native_root_target(":FBReactNativeSpec")], ) diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.java new file mode 100644 index 00000000000..0624a866ab9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.animated; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.views.view.ColorUtil; + +/** Animated node that represents a color. */ +/*package*/ class ColorAnimatedNode extends AnimatedNode { + + private final NativeAnimatedNodesManager mNativeAnimatedNodesManager; + private final int mRNodeId; + private final int mGNodeId; + private final int mBNodeId; + private final int mANodeId; + private int mColor; + + public ColorAnimatedNode( + ReadableMap config, NativeAnimatedNodesManager nativeAnimatedNodesManager) { + mNativeAnimatedNodesManager = nativeAnimatedNodesManager; + mRNodeId = config.getInt("r"); + mGNodeId = config.getInt("g"); + mBNodeId = config.getInt("b"); + mANodeId = config.getInt("a"); + + // TODO (T110930421): Support platform color + } + + public int getColor() { + return mColor; + } + + @Override + public void update() { + AnimatedNode rNode = mNativeAnimatedNodesManager.getNodeById(mRNodeId); + AnimatedNode gNode = mNativeAnimatedNodesManager.getNodeById(mGNodeId); + AnimatedNode bNode = mNativeAnimatedNodesManager.getNodeById(mBNodeId); + AnimatedNode aNode = mNativeAnimatedNodesManager.getNodeById(mANodeId); + + double r = ((ValueAnimatedNode) rNode).getValue(); + double g = ((ValueAnimatedNode) gNode).getValue(); + double b = ((ValueAnimatedNode) bNode).getValue(); + double a = ((ValueAnimatedNode) aNode).getValue(); + + mColor = ColorUtil.normalize(r, g, b, a); + } + + @Override + public String prettyPrint() { + return "ColorAnimatedNode[" + + mTag + + "]: r: " + + mRNodeId + + " g: " + + mGNodeId + + " b: " + + mBNodeId + + " a: " + + mANodeId; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java index 8321acffb82..10252abfa11 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedNodesManager.java @@ -129,6 +129,8 @@ import java.util.Queue; node = new StyleAnimatedNode(config, this); } else if ("value".equals(type)) { node = new ValueAnimatedNode(config); + } else if ("color".equals(type)) { + node = new ColorAnimatedNode(config, this); } else if ("props".equals(type)) { node = new PropsAnimatedNode(config, this); } else if ("interpolation".equals(type)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java index 9ee330d7dd0..759675e661a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.java @@ -105,6 +105,8 @@ import java.util.Map; } else { mPropMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue()); } + } else if (node instanceof ColorAnimatedNode) { + mPropMap.putInt(entry.getKey(), ((ColorAnimatedNode) node).getColor()); } else { throw new IllegalArgumentException( "Unsupported type of node used in property node " + node.getClass()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java index 65b65a10eda..68ef6a4186e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.java @@ -43,6 +43,8 @@ import java.util.Map; ((TransformAnimatedNode) node).collectViewUpdates(propsMap); } else if (node instanceof ValueAnimatedNode) { propsMap.putDouble(entry.getKey(), ((ValueAnimatedNode) node).getValue()); + } else if (node instanceof ColorAnimatedNode) { + propsMap.putInt(entry.getKey(), ((ColorAnimatedNode) node).getColor()); } else { throw new IllegalArgumentException( "Unsupported type of node used in property node " + node.getClass()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java index cc804ef703e..f1efecf3f0d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ColorUtil.java @@ -39,6 +39,7 @@ public class ColorUtil { /** * Gets the opacity from a color. Inspired by Android ColorDrawable. * + * @param color color to get opacity from * @return opacity expressed by one of PixelFormat constants */ public static int getOpacityFromColor(int color) { @@ -51,4 +52,22 @@ public class ColorUtil { return PixelFormat.TRANSLUCENT; } } + + /** + * Converts individual {r, g, b, a} channel values to a single integer representation of the color + * as 0xAARRGGBB. + * + * @param r red channel value, [0, 255] + * @param g green channel value, [0, 255] + * @param b blue channel value, [0, 255] + * @param a alpha channel value, [0, 1] + * @return integer representation of the color as 0xAARRGGBB + */ + public static int normalize(double r, double g, double b, double a) { + return (clamp255(a * 255) << 24) | (clamp255(r) << 16) | (clamp255(g) << 8) | clamp255(b); + } + + private static int clamp255(double value) { + return Math.max(0, Math.min(255, (int) Math.round(value))); + } } diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/BUCK b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK index 29dc6d734de..a8548c6f570 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK @@ -2,9 +2,12 @@ load("//tools/build_defs/oss:rn_defs.bzl", "YOGA_TARGET", "react_native_dep", "r rn_robolectric_test( name = "views", - # TODO Disabled temporarily until Yoga linking is fixed t14964130 + # TODO (T110934492): Disabled temporarily until tests are fixed # srcs = glob(['**/*.java']), - srcs = glob(["image/*.java"]), + srcs = glob([ + "image/*.java", + "view/*.java", + ]), contacts = ["oncall+fbandroid_sheriff@xmail.facebook.com"], deps = [ YOGA_TARGET, diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java index 5bb2bd71e05..a7ea56f0e2a 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java @@ -44,4 +44,13 @@ public class ColorUtilTest { assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFF123456)); assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFFFFFFFF)); } + + @Test + public void testNormalize() { + assertEquals(0x800B1621, ColorUtil.normalize(11, 22, 33, 0.5)); + assertEquals(0x00000000, ColorUtil.normalize(0, 0, 0, 0)); + assertEquals(0xFFFFFFFF, ColorUtil.normalize(255, 255, 255, 1)); + assertEquals(0xFF00FFFF, ColorUtil.normalize(-1, 256, 255, 1.1)); + assertEquals(0x000001FF, ColorUtil.normalize(0.4, 0.5, 255.4, -1)); + } }