Skip to content

Commit

Permalink
feat: android transform origin (facebook#38558)
Browse files Browse the repository at this point in the history
Summary:
This PR adds transform-origin support for android to make it easier to review. facebook#37606 (review) by javache. I'll answer feedback from javache below.

## Changelog:
[Android] [ADDED] - Transform origin
<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: facebook#38558

Test Plan: Run iOS RNTester app in old architecture and test transform-origin example in `TransformExample.js`.

Differential Revision: D48528339

Pulled By: javache

fbshipit-source-id: 09e0c9ef569b7e9131da2f6efa9ba057aa98ff82
  • Loading branch information
intergalacticspacehighway authored and facebook-github-bot committed Sep 1, 2023
1 parent 8a84a2d commit 3cc1875
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
*/
transform: {process: processTransform},
transformOrigin: {process: processTransformOrigin},

/**
* 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 @@ -196,6 +196,7 @@ export interface TransformsStyle {
>[]
| string
| undefined;
transformOrigin?: Array<string | number> | string | undefined;
/**
* @deprecated Use matrix in transform prop instead.
*/
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 {

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,10 @@ 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.setTag(R.id.invalidate_transform, null);
view.removeOnLayoutChangeListener(this);
// setShadowColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.setOutlineAmbientShadowColor(Color.BLACK);
Expand Down Expand Up @@ -129,6 +133,35 @@ protected T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T vie
return view;
}

// Currently. onLayout listener is only attached when transform origin prop is being used.
@Override
public void onLayoutChange(
View v,
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)) {
ReadableArray transformOrigin = (ReadableArray) 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,10 +174,19 @@ public void setBackgroundColor(@NonNull T view, int backgroundColor) {
@Override
@ReactProp(name = ViewProps.TRANSFORM)
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
if (matrix == null) {
resetTransformProperty(view);
view.setTag(R.id.transform, matrix);
view.setTag(R.id.invalidate_transform, true);
}

@Override
@ReactProp(name = ViewProps.TRANSFORM_ORIGIN)
public void setTransformOrigin(@NonNull T view, @Nullable ReadableArray transformOrigin) {
view.setTag(R.id.transform_origin, transformOrigin);
view.setTag(R.id.invalidate_transform, true);
if (transformOrigin != null) {
view.addOnLayoutChangeListener(this);
} else {
setTransformProperty(view, matrix);
view.removeOnLayoutChangeListener(this);
}
}

Expand Down Expand Up @@ -439,9 +481,15 @@ 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, @Nullable ReadableArray 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 Expand Up @@ -526,6 +574,17 @@ private void updateViewAccessibility(@NonNull T view) {
protected void onAfterUpdateTransaction(@NonNull T view) {
super.onAfterUpdateTransaction(view);
updateViewAccessibility(view);
Boolean invalidateTransform = (Boolean) view.getTag(R.id.invalidate_transform);
if (invalidateTransform != null && invalidateTransform) {
ReadableArray transformOrigin = (ReadableArray) view.getTag(R.id.transform_origin);
ReadableArray transformMatrix = (ReadableArray) view.getTag(R.id.transform);
if (transformMatrix != null) {
setTransformProperty(view, transformMatrix, transformOrigin);
} else {
resetTransformProperty(view);
}
view.setTag(R.id.invalidate_transform, false);
}
}

@Override
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, (ReadableArray) 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 ReadableArray 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 @@ -46,65 +46,121 @@ private static double convertToRadians(ReadableMap transformMap, String key) {
}

public static void processTransform(ReadableArray transforms, double[] result) {
processTransform(transforms, result, 0, 0, null);
}

public static void processTransform(
ReadableArray transforms,
double[] result,
float viewWidth,
float viewHeight,
ReadableArray transformOrigin) {
double[] helperMatrix = sHelperMatrix.get();
MatrixMathHelper.resetIdentityMatrix(result);
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);
}
// If the transforms array is actually just the matrix itself,
// copy that directly. This is for Fabric LayoutAnimations support.
// All of the stuff this Java helper does is already done in C++ in Fabric, so we
// can just use that matrix directly.
if (transforms.size() == 16 && transforms.getType(0) == ReadableType.Number) {
MatrixMathHelper.resetIdentityMatrix(helperMatrix);
for (int i = 0; i < transforms.size(); i++) {
result[i] = transforms.getDouble(i);
helperMatrix[i] = transforms.getDouble(i);
}
return;
}

for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) {
ReadableMap transform = transforms.getMap(transformIdx);
String transformType = transform.keySetIterator().nextKey();
MatrixMathHelper.multiplyInto(result, result, helperMatrix);
} else {
for (int transformIdx = 0, size = transforms.size(); transformIdx < size; transformIdx++) {
ReadableMap transform = transforms.getMap(transformIdx);
String transformType = transform.keySetIterator().nextKey();

MatrixMathHelper.resetIdentityMatrix(helperMatrix);
if ("matrix".equals(transformType)) {
ReadableArray matrix = transform.getArray(transformType);
for (int i = 0; i < 16; i++) {
helperMatrix[i] = matrix.getDouble(i);
MatrixMathHelper.resetIdentityMatrix(helperMatrix);
if ("matrix".equals(transformType)) {
ReadableArray matrix = transform.getArray(transformType);
for (int i = 0; i < 16; i++) {
helperMatrix[i] = matrix.getDouble(i);
}
} else if ("perspective".equals(transformType)) {
MatrixMathHelper.applyPerspective(helperMatrix, transform.getDouble(transformType));
} else if ("rotateX".equals(transformType)) {
MatrixMathHelper.applyRotateX(helperMatrix, convertToRadians(transform, transformType));
} else if ("rotateY".equals(transformType)) {
MatrixMathHelper.applyRotateY(helperMatrix, convertToRadians(transform, transformType));
} else if ("rotate".equals(transformType) || "rotateZ".equals(transformType)) {
MatrixMathHelper.applyRotateZ(helperMatrix, convertToRadians(transform, transformType));
} else if ("scale".equals(transformType)) {
double scale = transform.getDouble(transformType);
MatrixMathHelper.applyScaleX(helperMatrix, scale);
MatrixMathHelper.applyScaleY(helperMatrix, scale);
} else if ("scaleX".equals(transformType)) {
MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType));
} else if ("scaleY".equals(transformType)) {
MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType));
} else if ("translate".equals(transformType)) {
ReadableArray value = transform.getArray(transformType);
double x = value.getDouble(0);
double y = value.getDouble(1);
double z = value.size() > 2 ? value.getDouble(2) : 0d;
MatrixMathHelper.applyTranslate3D(helperMatrix, x, y, z);
} else if ("translateX".equals(transformType)) {
MatrixMathHelper.applyTranslate2D(helperMatrix, transform.getDouble(transformType), 0d);
} else if ("translateY".equals(transformType)) {
MatrixMathHelper.applyTranslate2D(helperMatrix, 0d, transform.getDouble(transformType));
} else if ("skewX".equals(transformType)) {
MatrixMathHelper.applySkewX(helperMatrix, convertToRadians(transform, transformType));
} else if ("skewY".equals(transformType)) {
MatrixMathHelper.applySkewY(helperMatrix, convertToRadians(transform, transformType));
} else {
FLog.w(ReactConstants.TAG, "Unsupported transform type: " + transformType);
}
} else if ("perspective".equals(transformType)) {
MatrixMathHelper.applyPerspective(helperMatrix, transform.getDouble(transformType));
} else if ("rotateX".equals(transformType)) {
MatrixMathHelper.applyRotateX(helperMatrix, convertToRadians(transform, transformType));
} else if ("rotateY".equals(transformType)) {
MatrixMathHelper.applyRotateY(helperMatrix, convertToRadians(transform, transformType));
} else if ("rotate".equals(transformType) || "rotateZ".equals(transformType)) {
MatrixMathHelper.applyRotateZ(helperMatrix, convertToRadians(transform, transformType));
} else if ("scale".equals(transformType)) {
double scale = transform.getDouble(transformType);
MatrixMathHelper.applyScaleX(helperMatrix, scale);
MatrixMathHelper.applyScaleY(helperMatrix, scale);
} else if ("scaleX".equals(transformType)) {
MatrixMathHelper.applyScaleX(helperMatrix, transform.getDouble(transformType));
} else if ("scaleY".equals(transformType)) {
MatrixMathHelper.applyScaleY(helperMatrix, transform.getDouble(transformType));
} else if ("translate".equals(transformType)) {
ReadableArray value = transform.getArray(transformType);
double x = value.getDouble(0);
double y = value.getDouble(1);
double z = value.size() > 2 ? value.getDouble(2) : 0d;
MatrixMathHelper.applyTranslate3D(helperMatrix, x, y, z);
} else if ("translateX".equals(transformType)) {
MatrixMathHelper.applyTranslate2D(helperMatrix, transform.getDouble(transformType), 0d);
} else if ("translateY".equals(transformType)) {
MatrixMathHelper.applyTranslate2D(helperMatrix, 0d, transform.getDouble(transformType));
} else if ("skewX".equals(transformType)) {
MatrixMathHelper.applySkewX(helperMatrix, convertToRadians(transform, transformType));
} else if ("skewY".equals(transformType)) {
MatrixMathHelper.applySkewY(helperMatrix, convertToRadians(transform, transformType));
} else {
FLog.w(ReactConstants.TAG, "Unsupported transform type: " + transformType);

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);
}
}

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

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

for (int i = 0; i < transformOrigin.size() && i < 3; i++) {
switch (transformOrigin.getType(i)) {
case Number:
origin[i] = (float) transformOrigin.getDouble(i);
break;
case String:
{
String part = transformOrigin.getString(i);
if (part.endsWith("%")) {
float val = Float.parseFloat(part.substring(0, part.length() - 1));
origin[i] = (i == 0 ? viewWidth : viewHeight) * val / 100.0f;
}
break;
}
}
}

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,15 @@
<!-- 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"/>

<!-- tag is used to invalidate transform style in view manager -->
<item type="id" name="invalidate_transform"/>
</resources>

0 comments on commit 3cc1875

Please sign in to comment.