Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
* Transform
*/
transform: {process: processTransform},
transformOrigin: true,

/**
* View
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const validAttributesForNonEventProps = {
// @ReactProps from BaseViewManager
backgroundColor: {process: require('../StyleSheet/processColor').default},
transform: true,
transformOrigin: true,
opacity: true,
elevation: true,
shadowColor: {process: require('../StyleSheet/processColor').default},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
* provides support for base view properties such as backgroundColor, opacity, etc.
*/
public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode>
extends ViewManager<T, C> implements BaseViewManagerInterface<T> {
extends ViewManager<T, C> implements BaseViewManagerInterface<T>, View.OnLayoutChangeListener {
Copy link
Contributor Author

@intergalacticspacehighway intergalacticspacehighway Jul 23, 2023

Choose a reason for hiding this comment

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

layout on a view is updated here. We can use onSizeChanged callback from ReactViewGroup instead of a listener here but we'll have to move the transform logic there and might need refactor as MatrixHelper class is in uimanager package and has some fields private. Also do some type cast here to call ReactViewGroup's set transform.


private static final int PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX = 2;
private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5);
Expand Down Expand Up @@ -90,6 +90,9 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
view.setElevation(0);
view.setAnimationMatrix(null);

view.setTag(R.id.transform, null);
view.setTag(R.id.transform_origin, null);
view.removeOnLayoutChangeListener(this);
// setShadowColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.setOutlineAmbientShadowColor(Color.BLACK);
Expand Down Expand Up @@ -129,6 +132,33 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
return view;
}

@Override
public void onLayoutChange(View v,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It seems potentially error prone that this is only called when a transform is present. I.e. I could imagine someone adding code here, not realizing that it isn't always called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true, just didn't want to attach a listener if it was not needed. Should we write a comment maybe?

// Currently, the onLayout listener is only attached when transform-origin prop is being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also removed the listener when transform-origin is null.

int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
// Old width and height
int oldWidth = oldRight - oldLeft;
int oldHeight = oldBottom - oldTop;

// Current width and height
int currentWidth = right - left;
int currentHeight = bottom - top;

if ((currentHeight != oldHeight || currentWidth != oldWidth)) {
String transformOrigin = (String) v.getTag(R.id.transform_origin);
ReadableArray transformMatrix = (ReadableArray) v.getTag(R.id.transform);
if (transformMatrix != null && transformOrigin != null) {
setTransformProperty((T) v, transformMatrix, transformOrigin);
}
}
}

@Override
@ReactProp(
name = ViewProps.BACKGROUND_COLOR,
Expand All @@ -141,11 +171,24 @@ public void setBackgroundColor(@NonNull T view, int backgroundColor) {
@Override
@ReactProp(name = ViewProps.TRANSFORM)
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
view.setTag(R.id.transform, matrix);
if (matrix == null) {
resetTransformProperty(view);
} else {
setTransformProperty(view, matrix);
String transformOrigin = (String) view.getTag(R.id.transform_origin);
setTransformProperty(view, matrix, transformOrigin);
}
}

@Override
@ReactProp(name = ViewProps.TRANSFORM_ORIGIN)
public void setTransformOrigin(@NonNull T view, @Nullable String transformOrigin) {
view.setTag(R.id.transform_origin, transformOrigin);
ReadableArray transformMatrix = (ReadableArray) view.getTag(R.id.transform);
if (transformMatrix != null) {
setTransformProperty(view, transformMatrix, transformOrigin);
}
view.addOnLayoutChangeListener(this);
}

@Override
Expand Down Expand Up @@ -439,9 +482,10 @@ public void setAccessibilityLiveRegion(@NonNull T view, @Nullable String liveReg
}
}

private static void setTransformProperty(@NonNull View view, ReadableArray transforms) {
private static void setTransformProperty(@NonNull View view, ReadableArray transforms, String transformOrigin) {
sMatrixDecompositionContext.reset();
TransformHelper.processTransform(transforms, sTransformDecompositionArray);
TransformHelper.processTransform(transforms, sTransformDecompositionArray,
PixelUtil.toDIPFromPixel(view.getWidth()), PixelUtil.toDIPFromPixel(view.getHeight()), transformOrigin);
MatrixMathHelper.decomposeMatrix(sTransformDecompositionArray, sMatrixDecompositionContext);
view.setTranslationX(
PixelUtil.toPixelFromDIP(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case ViewProps.TRANSFORM:
mViewManager.setTransform(view, (ReadableArray) value);
break;
case ViewProps.TRANSFORM_ORIGIN:
mViewManager.setTransformOrigin(view, (String) value);
break;
case ViewProps.TRANSLATE_X:
mViewManager.setTranslateX(view, value == null ? 0.0f : ((Double) value).floatValue());
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public interface BaseViewManagerInterface<T extends View> {

void setTransform(T view, @Nullable ReadableArray matrix);

void setTransformOrigin(T view, @Nullable String transformOrigin);

void setTranslateX(T view, float translateX);

void setTranslateY(T view, float translateY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
public class TransformHelper {

private static ThreadLocal<double[]> sHelperMatrix =
new ThreadLocal<double[]>() {
@Override
protected double[] initialValue() {
return new double[16];
}
};
new ThreadLocal<double[]>() {
@Override
protected double[] initialValue() {
return new double[16];
}
};

private static double convertToRadians(ReadableMap transformMap, String key) {
double value;
Expand All @@ -45,7 +45,7 @@ private static double convertToRadians(ReadableMap transformMap, String key) {
return inRadians ? value : MatrixMathHelper.degreesToRadians(value);
}

public static void processTransform(ReadableArray transforms, double[] result) {
public static void processTransform(ReadableArray transforms, double[] result, float viewWidth, float viewHeight, String transformOrigin) {
double[] helperMatrix = sHelperMatrix.get();
MatrixMathHelper.resetIdentityMatrix(result);

Expand All @@ -60,6 +60,13 @@ public static void processTransform(ReadableArray transforms, double[] result) {
return;
}

float[] offsets = getTranslateForTransformOrigin(viewWidth, viewHeight, transformOrigin);
if (offsets != null) {
MatrixMathHelper.resetIdentityMatrix(helperMatrix);
MatrixMathHelper.applyTranslate3D(helperMatrix, offsets[0], offsets[1], offsets[2]);
MatrixMathHelper.multiplyInto(result, result, helperMatrix);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

answering #37606 (comment)
Multiply is required because the operation is order dependent. i.e. we need to multiply the translate operations or else the view is not translated to the transform-origin before applying other transforms 😅

We also reset the helperMatrix which behaves weird when there are multiple view transform animations running. I think it's reusing the instance between different animations.

}

for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) {
ReadableMap transform = transforms.getMap(transformIdx);
String transformType = transform.keySetIterator().nextKey();
Expand Down Expand Up @@ -106,5 +113,47 @@ public static void processTransform(ReadableArray transforms, double[] result) {

MatrixMathHelper.multiplyInto(result, result, helperMatrix);
}
if (offsets != null) {
MatrixMathHelper.resetIdentityMatrix(helperMatrix);
MatrixMathHelper.applyTranslate3D(helperMatrix, -offsets[0], -offsets[1], -offsets[2]);
MatrixMathHelper.multiplyInto(result, result, helperMatrix);
}
}

public static float[] getTranslateForTransformOrigin(float viewWidth, float viewHeight, String transformOrigin) {
if (transformOrigin == null || (viewHeight == 0 && viewWidth == 0)) {
return null;
}
float viewCenterX = viewWidth / 2;
float viewCenterY = viewHeight / 2;

float[] origin = {viewCenterX, viewCenterY, 0.0f};

String[] parts = transformOrigin.split(" ");
for (int i = 0; i < parts.length && i < 3; i++) {
String part = parts[i];
if (part.endsWith("%")) {
float val = Float.parseFloat(part.substring(0, part.length() - 1));
origin[i] = (i == 0 ? viewWidth : viewHeight) * val / 100.0f;
} else if (part.equals("top")) {
origin[1] = 0.0f;
} else if (part.equals("bottom")) {
origin[1] = viewHeight;
} else if (part.equals("left")) {
origin[0] = 0.0f;
} else if (part.equals("right")) {
origin[0] = viewWidth;
} else if (part.equals("center")) {
continue;
} else {
origin[i] = Float.parseFloat(part);
}
}

float newTranslateX = -viewCenterX + origin[0];
float newTranslateY = -viewCenterY + origin[1];
float newTranslateZ = origin[2];

return new float[]{newTranslateX, newTranslateY, newTranslateZ};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ public class ViewProps {
public static final String ON_LAYOUT = "onLayout";

public static final String TRANSFORM = "transform";

public static final String TRANSFORM_ORIGIN = "transformOrigin";
public static final String ELEVATION = "elevation";
public static final String SHADOW_COLOR = "shadowColor";
public static final String Z_INDEX = "zIndex";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
<!-- tag is used store bitset of pointer events observed -->
<item type="id" name="pointer_events"/>

<!-- tag is used store raw transform style on the view -->
<item type="id" name="transform"/>

<!-- tag is used store raw transform origin style on the view -->
<item type="id" name="transform_origin"/>

<!-- tag is used to store role tag-->
<item type="id" name="role"/>
</resources>
51 changes: 50 additions & 1 deletion packages/rn-tester/js/examples/Transform/TransformExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import React, {useEffect, useState} from 'react';
import {Animated, StyleSheet, Text, View} from 'react-native';
import {Animated, StyleSheet, Text, View, Easing} from 'react-native';

import type {Node, Element} from 'react';

Expand Down Expand Up @@ -50,6 +50,39 @@ function AnimateTransformSingleProp() {
);
}

function TransformOriginExample() {
const rotateAnim = React.useRef(new Animated.Value(0)).current;

useEffect(() => {
Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: 5000,
easing: Easing.linear,
useNativeDriver: true,
}),
).start();
}, [rotateAnim]);

const spin = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});

return (
<View style={styles.transformOriginWrapper}>
<Animated.View
style={[
styles.transformOriginView,
{
transform: [{rotate: spin}],
},
]}
/>
</View>
);
}

function Flip() {
const [theta] = useState(new Animated.Value(45));
const animate = () => {
Expand Down Expand Up @@ -234,6 +267,15 @@ const styles = StyleSheet.create({
color: 'white',
fontWeight: 'bold',
},
transformOriginWrapper: {
alignItems: 'center',
},
transformOriginView: {
backgroundColor: 'pink',
width: 100,
height: 100,
transformOrigin: 'top left',
},
});

exports.title = 'Transforms';
Expand Down Expand Up @@ -346,4 +388,11 @@ exports.examples = [
);
},
},
{
title: 'Transform origin',
description: "transformOrigin: 'top'",
render(): Node {
return <TransformOriginExample />;
},
},
];