Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/api/api-quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ accessibilityCategory="button"
rotation={0.5} // radians
translationX={10}
translationY={10}
transformOrigin="top left"
>
{/* children */}
</view>
Expand Down
13 changes: 10 additions & 3 deletions docs/api/api-reference-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,19 @@ All properties from [Layout](#layout), plus:
**`rotation`**: `number`
- Specifies the rotation component in angle radians of the affine transformation to be applied to the view.

**`translationX`**: `number`
**`translationX`**: `number | string`
- Specifies the horizontal translation component of the affine transformation to be applied to the view.
- Note: When the device is in RTL mode, the applied translationX value will be flipped.
- Numeric values are points. Percent strings such as `"50%"` resolve against the view's own calculated width.
- Note: When the device is in RTL mode, the resolved translationX value will be flipped.

**`translationY`**: `number`
**`translationY`**: `number | string`
- Specifies the vertical translation component of the affine transformation to be applied to the view.
- Numeric values are points. Percent strings such as `"-50%"` resolve against the view's own calculated height.

**`transformOrigin`**: `string`
- Specifies the origin used for scale and rotation transforms.
- Supports keywords such as `"center"` and `"top left"`, point pairs such as `"50px 70px"`, and percent pairs such as `"25% 75%"`.
- 3D transform origins are not supported.

#### Mask Properties

Expand Down
7 changes: 5 additions & 2 deletions docs/api/api-style-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,11 @@ new Style<View>({
rotation: 0, // number (radians)

// Translation
translationX: 0, // number (points, flipped in RTL)
translationY: 0, // number (points)
translationX: 0, // number (points) or percent string of own width, flipped in RTL
translationY: 0, // number (points) or percent string of own height

// Transform origin
transformOrigin: 'center', // keywords, "50px 70px", or "25% 75%" (2D only)
})
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -977,19 +977,38 @@ interface ViewAttributes {

/**
* Specifies the horizontal translation component of the affine transformation to be applied to the view.
* Numeric values are points. Percent strings (for example, "50%") resolve against the view's own calculated width.
*
* NOTE: When the device is in RTL mode, the applied translationX value will be flipped
* NOTE: When the device is in RTL mode, the resolved translationX value will be flipped.
*
* @see transform for the order in which the transformations are applied
*/
translationX?: number;
translationX?: number | string;

/**
* Specifies the vertical translation component of the affine transformation to be applied to the view.
* Numeric values are points. Percent strings (for example, "-50%") resolve against the view's own calculated height.
*
* @see transform for the order in which the transformations are applied
*/
translationY?: number;
translationY?: number | string;

/**
* Specifies the origin used for scale and rotation transforms.
* Supports CSS-like keywords (for example, "center" or "top left"), point values (for example, "50px 70px"),
* and percent values (for example, "25% 75%"). 3D transform origins are not supported.
*/
transformOrigin?: string;

/**
* Specifies a CSS-like transform string.
* Supports translate(), translateX(), translateY(), scale(), scaleX(), scaleY(), rotate(), and rotateZ().
*
* When set, this value overrides the individual transform attributes (translationX, translationY, scaleX,
* scaleY, and rotation). It does not compose with those values. transformOrigin still applies to the resolved
* transform.
*/
transform?: string;

/**
* Sets the view's accessibility identifier.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,23 @@ class AttributesBindingContext<T : View>(val native: AttributesBindingContextNat
native.bindCompositeAttribute(attribute, parts, delegate)
}

inline fun bindTransformAttributes(
crossinline apply: (view: T, value: Any?, animator: ValdiAnimator?) -> Unit,
crossinline reset: (view: T, animator: ValdiAnimator?) -> Unit
) {
val delegate = object: ObjectAttributeHandlerDelegate() {
override fun onApply(view: View, value: Any?, animator: ValdiAnimator?) {
apply(view as T, value, animator)
}

override fun onReset(view: View, animator: ValdiAnimator?) {
reset(view as T, animator)
}
}

native.bindTransformAttributes(delegate)
}

inline fun <AttributeType>bindDeserializableAttribute(
attribute: String,
invalidateLayoutOnChange: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class AttributesBindingContextNative(viewClass: Class<*>,
doBindAttribute(ATTRIBUTE_TYPE_COMPOSITE, name, false, delegate, parts.toTypedArray())
}

fun bindTransformAttributes(delegate: ObjectAttributeHandlerDelegate) {
NativeBridge.bindTransformAttributes(nativeHandle, delegate)
}

fun registerPreprocessor(attributeName: String, enableCache: Boolean, preprocessor: AttributePreprocessor) {
NativeBridge.registerAttributePreprocessor(nativeHandle, attributeName, enableCache, preprocessor)
}
Expand Down
123 changes: 37 additions & 86 deletions valdi/src/java/com/snap/valdi/attributes/impl/ViewAttributesBinder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -286,101 +286,56 @@ class ViewAttributesBinder(private val context: Context,
}
}

private fun reapplyTranslationXIfNeeded(view: View, value: Float) {
val translationX = ViewUtils.resolveDeltaX(view, value)
val valueAnimator = ViewUtils.getTransitionInfo(view)?.getValueAnimator(TRANSLATION_X_KEY)

if (valueAnimator == null) {
view.translationX = translationX
} else if (valueAnimator.valueAnimation.additionalData != translationX) {
ViewUtils.cancelAnimation(view, TRANSLATION_X_KEY)
view.translationX = translationX
fun applyTransform(view: View, value: Any?, animator: ValdiAnimator?) {
if (value !is Array<*> || value.size != 5) {
throw AttributeError("transform components should have 5 entries")
}
}

fun applyTranslationX(view: View, value: Float, animator: ValdiAnimator?) {
val resolvedValue = coordinateResolver.toPixelF(value)
val resolvedTranslationX = ViewUtils.resolveDeltaX(view, resolvedValue)

if (value != 0.0f) {
ViewUtils.setDidFinishLayoutForKey(view, "translationX") {
reapplyTranslationXIfNeeded(it, resolvedValue)
}
} else {
ViewUtils.removeDidFinishLayoutForKey(view, "translationX")
}
val translationX = coordinateResolver.toPixelF((value[0] as? Number)?.toDouble() ?: 0.0)
val translationY = coordinateResolver.toPixelF((value[1] as? Number)?.toDouble() ?: 0.0)
val scaleX = (value[2] as? Number)?.toFloat() ?: 1.0f
val scaleY = (value[3] as? Number)?.toFloat() ?: 1.0f
val rotation = Math.toDegrees(((value[4] as? Number)?.toDouble() ?: 0.0)).toFloat()

setTransformElement(view,
resolvedTranslationX,
translationX,
animator,
TRANSLATION_X_KEY,
{ translationX },
Comment on lines 300 to 304

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

setTransformElement declares getValue: View.() -> Float. Kotlin resolves an unqualified identifier in the lambda body to the outer local val translationX (target value), not this.translationX on the View receiver. The animator's "current value" always equals the target, so the delta is zero and animation is skipped.

Same shadowing bug repeats for translationY, scaleX, scaleY, rotation.

iOS uses a different binder path (UntypedAttributeHandlerDelegate in SCValdiAttributesBinder.mm) that doesn't have this shadow, so it animates fine.

setTransformElement(view,
        translationX,
        animator,
        TRANSLATION_X_KEY,
        { translationX },              // <-- reads the LOCAL val, not view.translationX
        { view.translationX = it },

Apply to all five setTransformElement call sites in applyTransform.

{ translationX = it },
{ view.translationX = it },
ValdiValueAnimation.MinimumVisibleChange.PIXEL)
}

fun resetTranslationX(view: View, animator: ValdiAnimator?) {
applyTranslationX(view, 0.0f, animator)
}

fun applyTranslationY(view: View, value: Float, animator: ValdiAnimator?) {
val resolvedValue = coordinateResolver.toPixelF(value)
setTransformElement(view,
resolvedValue,
animator,
TRANSLATION_Y_KEY,
{ translationY },
{ translationY = it },
ValdiValueAnimation.MinimumVisibleChange.PIXEL)
}

fun resetTranslationY(view: View, animator: ValdiAnimator?) {
applyTranslationY(view, 0.0f, animator)
}

fun applyScaleX(view: View, value: Float, animator: ValdiAnimator?) {
translationY,
animator,
TRANSLATION_Y_KEY,
{ translationY },
{ view.translationY = it },
ValdiValueAnimation.MinimumVisibleChange.PIXEL)
setTransformElement(view,
value,
animator,
SCALE_X_KEY,
{ scaleX },
{ scaleX = it },
ValdiValueAnimation.MinimumVisibleChange.SCALE_RATIO)
}

fun resetScaleX(view: View, animator: ValdiAnimator?) {
applyScaleX(view, 1.0f, animator)
}

fun applyScaleY(view: View, value: Float, animator: ValdiAnimator?) {
scaleX,
animator,
SCALE_X_KEY,
{ scaleX },
{ view.scaleX = it },
ValdiValueAnimation.MinimumVisibleChange.SCALE_RATIO)
setTransformElement(view,
value,
animator,
SCALE_Y_KEY,
{ scaleY },
{ scaleY = it },
ValdiValueAnimation.MinimumVisibleChange.SCALE_RATIO)
}

fun resetScaleY(view: View, animator: ValdiAnimator?) {
applyScaleY(view, 1.0f, animator)
}

fun applyRotation(view: View, value: Float, animator: ValdiAnimator?) {
val radianValue = ViewUtils.resolveDeltaX(view, value)
val resolvedValue = Math.toDegrees(radianValue.toDouble()).toFloat()

scaleY,
animator,
SCALE_Y_KEY,
{ scaleY },
{ view.scaleY = it },
ValdiValueAnimation.MinimumVisibleChange.SCALE_RATIO)
setTransformElement(view,
resolvedValue,
animator,
ROTATION_KEY,
{ rotation },
{ rotation = it },
ValdiValueAnimation.MinimumVisibleChange.ROTATION_DEGREES_ANGLE)
rotation,
animator,
ROTATION_KEY,
{ rotation },
{ view.rotation = it },
ValdiValueAnimation.MinimumVisibleChange.ROTATION_DEGREES_ANGLE)
}

fun resetRotation(view: View, animator: ValdiAnimator?) {
applyRotation(view, 0.0f, animator)
fun resetTransform(view: View, animator: ValdiAnimator?) {
applyTransform(view, arrayOf(0.0, 0.0, 1.0, 1.0, 0.0), animator)
}

private fun getResourceIdForGeneratedValdiId(value: String): Int {
Expand Down Expand Up @@ -610,11 +565,7 @@ class ViewAttributesBinder(private val context: Context,
CompositeAttributePart("borderColor", AttributeType.COLOR, true, false)
), this::applyBorderComposite, this::resetBorder)

attributesBindingContext.bindFloatAttribute("translationX", false, this::applyTranslationX, this::resetTranslationX)
attributesBindingContext.bindFloatAttribute("translationY", false, this::applyTranslationY, this::resetTranslationY)
attributesBindingContext.bindFloatAttribute("scaleX", false, this::applyScaleX, this::resetScaleX)
attributesBindingContext.bindFloatAttribute("scaleY", false, this::applyScaleY, this::resetScaleY)
attributesBindingContext.bindFloatAttribute("rotation", false, this::applyRotation, this::resetRotation)
attributesBindingContext.bindTransformAttributes(this::applyTransform, this::resetTransform)

attributesBindingContext.bindUntypedAttribute("maskPath", false, this::applyMaskPath, this::resetMaskPath)
attributesBindingContext.bindFloatAttribute("maskOpacity", false, this::applyMaskOpacity, this::resetMaskOpacity)
Expand Down
2 changes: 1 addition & 1 deletion valdi/src/java/com/snapchat/client/valdi/NativeBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ public static native boolean notifyAssetLoaderCompleted(long callbackHandle,
public static native void registerModuleFactoriesProvider(long runtimeManagerHandle, Object moduleFactoriesProvider);
public static native long getViewNodePoint(long runtimeHandle, long viewNodeHandle, int x, int y, int mode, boolean fromBoundsOrigin);
public static native long getViewNodeSize(long runtimeHandle, long viewNodeHandle, int mode);

public static native boolean canViewNodeScroll(long runtimeHandle, long viewNodeHandler, int x, int y, int direction);

public static native boolean isViewNodeScrollingOrAnimating(long viewNodeHandle);
Expand Down Expand Up @@ -199,6 +198,7 @@ public static native int bindAttribute(long bindingContextHandle,
boolean invalidateLayoutOnChange,
Object delegate,
Object compositeParts);
public static native void bindTransformAttributes(long bindingContextHandle, Object delegate);
public static native void bindScrollAttributes(long bindingContextHandle);
public static native void bindAssetAttributes(long bindingContextHandle, int outputType);
public static native void setMeasureDelegate(long bindingContextHandle, Object measureDelegate);
Expand Down
4 changes: 4 additions & 0 deletions valdi/src/valdi/android/AttributesBindingContextWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ jint AttributesBindingContextWrapper::bindAttributes(
return static_cast<jint>(attributeId);
}

void AttributesBindingContextWrapper::bindTransformAttributes(jobject delegate) {
_bindingContext.bindTransformAttributes(Valdi::makeShared<ObjectAttributeHandlerDelegate>(delegate));
}

void AttributesBindingContextWrapper::bindScrollAttributes() {
_bindingContext.bindScrollAttributes();
}
Expand Down
2 changes: 2 additions & 0 deletions valdi/src/valdi/android/AttributesBindingContextWrapper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class AttributesBindingContextWrapper : public Valdi::SimpleRefCountable {

jint bindAttributes(jint type, jstring name, jboolean invalidateLayoutOnChange, jobject delegate, jobject parts);

void bindTransformAttributes(jobject delegate);

void bindScrollAttributes();

void bindAssetAttributes(snap::valdi_core::AssetOutputType assetOutputType);
Expand Down
8 changes: 8 additions & 0 deletions valdi/src/valdi/android/NativeBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,13 @@ jint ValdiAndroid::NativeBridge::bindAttribute(fbjni::alias_ref<fbjni::JClass> c
return wrapper->bindAttributes(type, name, invalidateLayoutOnChange, delegate, compositeParts);
}

void ValdiAndroid::NativeBridge::bindTransformAttributes(fbjni::alias_ref<fbjni::JClass> clazz, // NOLINT
jlong bindingContextHandle,
jobject delegate) {
auto wrapper = getBindingContextWrapper(bindingContextHandle);
wrapper->bindTransformAttributes(delegate);
}

void ValdiAndroid::NativeBridge::bindScrollAttributes(fbjni::alias_ref<fbjni::JClass> clazz, // NOLINT
jlong bindingContextHandle) {
auto wrapper = getBindingContextWrapper(bindingContextHandle);
Expand Down Expand Up @@ -2397,6 +2404,7 @@ void ValdiAndroid::NativeBridge::registerNatives() {
makeNativeMethod("destroyContext", ValdiAndroid::NativeBridge::destroyContext),
makeNativeMethod("forceBindAttributes", ValdiAndroid::NativeBridge::forceBindAttributes),
makeNativeMethod("bindAttribute", ValdiAndroid::NativeBridge::bindAttribute),
makeNativeMethod("bindTransformAttributes", ValdiAndroid::NativeBridge::bindTransformAttributes),
makeNativeMethod("bindScrollAttributes", ValdiAndroid::NativeBridge::bindScrollAttributes),
makeNativeMethod("bindAssetAttributes", ValdiAndroid::NativeBridge::bindAssetAttributes),
makeNativeMethod("setPlaceholderViewMeasureDelegate",
Expand Down
3 changes: 3 additions & 0 deletions valdi/src/valdi/android/NativeBridge.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ class NativeBridge : public fbjni::JavaClass<NativeBridge> {
jboolean invalidateLayoutOnChange,
jobject delegate,
jobject compositeParts);
static void bindTransformAttributes(fbjni::alias_ref<fbjni::JClass> clazz,
jlong bindingContextHandle,
jobject delegate);
static void bindScrollAttributes(fbjni::alias_ref<fbjni::JClass> clazz, jlong bindingContextHandle);
static void bindAssetAttributes(fbjni::alias_ref<fbjni::JClass> clazz, jlong bindingContextHandle, jint outputType);
static void setMeasureDelegate(fbjni::alias_ref<fbjni::JClass> clazz,
Expand Down
Loading
Loading