Files
react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
T
David Vacca a9ccdcace5 Store metadata to determine if a view is listening for a JS event
Summary:
This diff updates the BaseViewManager in order to store metadata in views that are handling JS events.

This information will be used later in the stack to optimize dispatching of hover events and fix viewFlattening bugs

changelog: [internal] internal

Reviewed By: philIip

Differential Revision: D32253127

fbshipit-source-id: b6b74f0b1a5b8cc652b3ac3fff42165ee4ce85e1
2021-11-19 15:48:14 -08:00

481 lines
18 KiB
Java

/*
* Copyright (c) Facebook, Inc. and its 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.uimanager;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import com.facebook.common.logging.FLog;
import com.facebook.react.R;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.util.ReactFindViewUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Base class that should be suitable for the majority of subclasses of {@link ViewManager}. It
* 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> {
private static final int PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX = 2;
private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5);
private static MatrixMathHelper.MatrixDecompositionContext sMatrixDecompositionContext =
new MatrixMathHelper.MatrixDecompositionContext();
private static double[] sTransformDecompositionArray = new double[16];
public static final Map<String, Integer> sStateDescription = new HashMap<>();
static {
sStateDescription.put("busy", R.string.state_busy_description);
sStateDescription.put("expanded", R.string.state_expanded_description);
sStateDescription.put("collapsed", R.string.state_collapsed_description);
}
// State definition constants -- must match the definition in
// ViewAccessibility.js. These only include states for which there
// is no native support in android.
private static final String STATE_CHECKED = "checked"; // Special case for mixed state checkboxes
private static final String STATE_BUSY = "busy";
private static final String STATE_EXPANDED = "expanded";
private static final String STATE_MIXED = "mixed";
@Override
@ReactProp(
name = ViewProps.BACKGROUND_COLOR,
defaultInt = Color.TRANSPARENT,
customType = "Color")
public void setBackgroundColor(@NonNull T view, int backgroundColor) {
view.setBackgroundColor(backgroundColor);
}
@Override
@ReactProp(name = ViewProps.TRANSFORM)
public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) {
if (matrix == null) {
resetTransformProperty(view);
} else {
setTransformProperty(view, matrix);
}
}
@Override
@ReactProp(name = ViewProps.OPACITY, defaultFloat = 1.f)
public void setOpacity(@NonNull T view, float opacity) {
view.setAlpha(opacity);
}
@Override
@ReactProp(name = ViewProps.ELEVATION)
public void setElevation(@NonNull T view, float elevation) {
ViewCompat.setElevation(view, PixelUtil.toPixelFromDIP(elevation));
}
@Override
@ReactProp(name = ViewProps.SHADOW_COLOR, defaultInt = Color.BLACK, customType = "Color")
public void setShadowColor(@NonNull T view, int shadowColor) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.setOutlineAmbientShadowColor(shadowColor);
view.setOutlineSpotShadowColor(shadowColor);
}
}
@Override
@ReactProp(name = ViewProps.Z_INDEX)
public void setZIndex(@NonNull T view, float zIndex) {
int integerZIndex = Math.round(zIndex);
ViewGroupManager.setViewZIndex(view, integerZIndex);
ViewParent parent = view.getParent();
if (parent instanceof ReactZIndexedViewGroup) {
((ReactZIndexedViewGroup) parent).updateDrawingOrder();
}
}
@Override
@ReactProp(name = ViewProps.RENDER_TO_HARDWARE_TEXTURE)
public void setRenderToHardwareTexture(@NonNull T view, boolean useHWTexture) {
view.setLayerType(useHWTexture ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
}
@Override
@ReactProp(name = ViewProps.TEST_ID)
public void setTestId(@NonNull T view, @Nullable String testId) {
view.setTag(R.id.react_test_id, testId);
// temporarily set the tag and keyed tags to avoid end to end test regressions
view.setTag(testId);
}
@Override
@ReactProp(name = ViewProps.NATIVE_ID)
public void setNativeId(@NonNull T view, @Nullable String nativeId) {
view.setTag(R.id.view_tag_native_id, nativeId);
ReactFindViewUtil.notifyViewRendered(view);
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_LABEL)
public void setAccessibilityLabel(@NonNull T view, @Nullable String accessibilityLabel) {
view.setTag(R.id.accessibility_label, accessibilityLabel);
updateViewContentDescription(view);
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_HINT)
public void setAccessibilityHint(@NonNull T view, @Nullable String accessibilityHint) {
view.setTag(R.id.accessibility_hint, accessibilityHint);
updateViewContentDescription(view);
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_ROLE)
public void setAccessibilityRole(@NonNull T view, @Nullable String accessibilityRole) {
if (accessibilityRole == null) {
return;
}
view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole));
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_STATE)
public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilityState) {
if (accessibilityState == null) {
return;
}
if (accessibilityState.hasKey("selected")) {
boolean prevSelected = view.isSelected();
boolean nextSelected = accessibilityState.getBoolean("selected");
view.setSelected(nextSelected);
// For some reason, Android does not announce "unselected" when state changes.
// This is inconsistent with other platforms, but also with the "checked" state.
// So manually announce this.
if (view.isAccessibilityFocused() && prevSelected && !nextSelected) {
view.announceForAccessibility(
view.getContext().getString(R.string.state_unselected_description));
}
} else {
view.setSelected(false);
}
view.setTag(R.id.accessibility_state, accessibilityState);
view.setEnabled(true);
// For states which don't have corresponding methods in
// AccessibilityNodeInfo, update the view's content description
// here
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
while (i.hasNextKey()) {
final String state = i.nextKey();
if (state.equals(STATE_BUSY)
|| state.equals(STATE_EXPANDED)
|| (state.equals(STATE_CHECKED)
&& accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) {
updateViewContentDescription(view);
break;
} else if (view.isAccessibilityFocused()) {
// Internally Talkback ONLY uses TYPE_VIEW_CLICKED for "checked" and
// "selected" announcements. Send a click event to make sure Talkback
// get notified for the state changes that don't happen upon users' click.
// For the state changes that happens immediately, Talkback will skip
// the duplicated click event.
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
}
private void updateViewContentDescription(@NonNull T view) {
final String accessibilityLabel = (String) view.getTag(R.id.accessibility_label);
final ReadableMap accessibilityState = (ReadableMap) view.getTag(R.id.accessibility_state);
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
final List<String> contentDescription = new ArrayList<>();
final ReadableMap accessibilityValue = (ReadableMap) view.getTag(R.id.accessibility_value);
if (accessibilityLabel != null) {
contentDescription.add(accessibilityLabel);
}
if (accessibilityState != null) {
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
while (i.hasNextKey()) {
final String state = i.nextKey();
final Dynamic value = accessibilityState.getDynamic(state);
if (state.equals(STATE_CHECKED)
&& value.getType() == ReadableType.String
&& value.asString().equals(STATE_MIXED)) {
contentDescription.add(view.getContext().getString(R.string.state_mixed_description));
} else if (state.equals(STATE_BUSY)
&& value.getType() == ReadableType.Boolean
&& value.asBoolean()) {
contentDescription.add(view.getContext().getString(R.string.state_busy_description));
} else if (state.equals(STATE_EXPANDED) && value.getType() == ReadableType.Boolean) {
contentDescription.add(
view.getContext()
.getString(
value.asBoolean()
? R.string.state_expanded_description
: R.string.state_collapsed_description));
}
}
}
if (accessibilityValue != null && accessibilityValue.hasKey("text")) {
final Dynamic text = accessibilityValue.getDynamic("text");
if (text != null && text.getType() == ReadableType.String) {
contentDescription.add(text.asString());
}
}
if (accessibilityHint != null) {
contentDescription.add(accessibilityHint);
}
if (contentDescription.size() > 0) {
view.setContentDescription(TextUtils.join(", ", contentDescription));
}
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_ACTIONS)
public void setAccessibilityActions(T view, ReadableArray accessibilityActions) {
if (accessibilityActions == null) {
return;
}
view.setTag(R.id.accessibility_actions, accessibilityActions);
}
@ReactProp(name = ViewProps.ACCESSIBILITY_VALUE)
public void setAccessibilityValue(T view, ReadableMap accessibilityValue) {
if (accessibilityValue == null) {
return;
}
view.setTag(R.id.accessibility_value, accessibilityValue);
if (accessibilityValue.hasKey("text")) {
updateViewContentDescription(view);
}
}
@Override
@ReactProp(name = ViewProps.IMPORTANT_FOR_ACCESSIBILITY)
public void setImportantForAccessibility(
@NonNull T view, @Nullable String importantForAccessibility) {
if (importantForAccessibility == null || importantForAccessibility.equals("auto")) {
ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
} else if (importantForAccessibility.equals("yes")) {
ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
} else if (importantForAccessibility.equals("no")) {
ViewCompat.setImportantForAccessibility(view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
} else if (importantForAccessibility.equals("no-hide-descendants")) {
ViewCompat.setImportantForAccessibility(
view, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
}
@Override
@Deprecated
@ReactProp(name = ViewProps.ROTATION)
public void setRotation(@NonNull T view, float rotation) {
view.setRotation(rotation);
}
@Override
@Deprecated
@ReactProp(name = ViewProps.SCALE_X, defaultFloat = 1f)
public void setScaleX(@NonNull T view, float scaleX) {
view.setScaleX(scaleX);
}
@Override
@Deprecated
@ReactProp(name = ViewProps.SCALE_Y, defaultFloat = 1f)
public void setScaleY(@NonNull T view, float scaleY) {
view.setScaleY(scaleY);
}
@Override
@Deprecated
@ReactProp(name = ViewProps.TRANSLATE_X, defaultFloat = 0f)
public void setTranslateX(@NonNull T view, float translateX) {
view.setTranslationX(PixelUtil.toPixelFromDIP(translateX));
}
@Override
@Deprecated
@ReactProp(name = ViewProps.TRANSLATE_Y, defaultFloat = 0f)
public void setTranslateY(@NonNull T view, float translateY) {
view.setTranslationY(PixelUtil.toPixelFromDIP(translateY));
}
@Override
@ReactProp(name = ViewProps.ACCESSIBILITY_LIVE_REGION)
public void setAccessibilityLiveRegion(@NonNull T view, @Nullable String liveRegion) {
if (liveRegion == null || liveRegion.equals("none")) {
ViewCompat.setAccessibilityLiveRegion(view, ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE);
} else if (liveRegion.equals("polite")) {
ViewCompat.setAccessibilityLiveRegion(view, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
} else if (liveRegion.equals("assertive")) {
ViewCompat.setAccessibilityLiveRegion(view, ViewCompat.ACCESSIBILITY_LIVE_REGION_ASSERTIVE);
}
}
private static void setTransformProperty(@NonNull View view, ReadableArray transforms) {
sMatrixDecompositionContext.reset();
TransformHelper.processTransform(transforms, sTransformDecompositionArray);
MatrixMathHelper.decomposeMatrix(sTransformDecompositionArray, sMatrixDecompositionContext);
view.setTranslationX(
PixelUtil.toPixelFromDIP(
sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.translation[0])));
view.setTranslationY(
PixelUtil.toPixelFromDIP(
sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.translation[1])));
view.setRotation(
sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.rotationDegrees[2]));
view.setRotationX(
sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.rotationDegrees[0]));
view.setRotationY(
sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.rotationDegrees[1]));
view.setScaleX(sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.scale[0]));
view.setScaleY(sanitizeFloatPropertyValue((float) sMatrixDecompositionContext.scale[1]));
double[] perspectiveArray = sMatrixDecompositionContext.perspective;
if (perspectiveArray.length > PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX) {
float invertedCameraDistance =
(float) perspectiveArray[PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX];
if (invertedCameraDistance == 0) {
// Default camera distance, before scale multiplier (1280)
invertedCameraDistance = 0.00078125f;
}
float cameraDistance = -1 / invertedCameraDistance;
float scale = DisplayMetricsHolder.getScreenDisplayMetrics().density;
// The following converts the matrix's perspective to a camera distance
// such that the camera perspective looks the same on Android and iOS.
// The native Android implementation removed the screen density from the
// calculation, so squaring and a normalization value of
// sqrt(5) produces an exact replica with iOS.
// For more information, see https://github.com/facebook/react-native/pull/18302
float normalizedCameraDistance =
sanitizeFloatPropertyValue(
scale * scale * cameraDistance * CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER);
view.setCameraDistance(normalizedCameraDistance);
}
}
/**
* Prior to Android P things like setScaleX() allowed passing float values that were bogus such as
* Float.NaN. If the app is targeting Android P or later then passing these values will result in
* an exception being thrown. Since JS might still send Float.NaN, we want to keep the code
* backward compatible and continue using the fallback value if an invalid float is passed.
*/
private static float sanitizeFloatPropertyValue(float value) {
if (value >= -Float.MAX_VALUE && value <= Float.MAX_VALUE) {
return value;
}
if (value < -Float.MAX_VALUE || value == Float.NEGATIVE_INFINITY) {
return -Float.MAX_VALUE;
}
if (value > Float.MAX_VALUE || value == Float.POSITIVE_INFINITY) {
return Float.MAX_VALUE;
}
if (Float.isNaN(value)) {
return 0;
}
// Shouldn't be possible to reach this point.
throw new IllegalStateException("Invalid float property value: " + value);
}
private static void resetTransformProperty(@NonNull View view) {
view.setTranslationX(PixelUtil.toPixelFromDIP(0));
view.setTranslationY(PixelUtil.toPixelFromDIP(0));
view.setRotation(0);
view.setRotationX(0);
view.setRotationY(0);
view.setScaleX(1);
view.setScaleY(1);
view.setCameraDistance(0);
}
private void updateViewAccessibility(@NonNull T view) {
ReactAccessibilityDelegate.setDelegate(view);
}
@Override
protected void onAfterUpdateTransaction(@NonNull T view) {
super.onAfterUpdateTransaction(view);
updateViewAccessibility(view);
}
@Override
public @Nullable Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put("topAccessibilityAction", MapBuilder.of("registrationName", "onAccessibilityAction"))
.build();
}
@Override
public void setBorderRadius(T view, float borderRadius) {
logUnsupportedPropertyWarning(ViewProps.BORDER_RADIUS);
}
@Override
public void setBorderBottomLeftRadius(T view, float borderRadius) {
logUnsupportedPropertyWarning(ViewProps.BORDER_BOTTOM_LEFT_RADIUS);
}
@Override
public void setBorderBottomRightRadius(T view, float borderRadius) {
logUnsupportedPropertyWarning(ViewProps.BORDER_BOTTOM_RIGHT_RADIUS);
}
@Override
public void setBorderTopLeftRadius(T view, float borderRadius) {
logUnsupportedPropertyWarning(ViewProps.BORDER_TOP_LEFT_RADIUS);
}
@Override
public void setBorderTopRightRadius(T view, float borderRadius) {
logUnsupportedPropertyWarning(ViewProps.BORDER_TOP_RIGHT_RADIUS);
}
private void logUnsupportedPropertyWarning(String propName) {
FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName);
}
@ReactProp(name = "pointerenter")
public void setPointerEnter(@NonNull T view, @Nullable boolean value) {
view.setTag(R.id.pointer_enter, value);
}
@ReactProp(name = "pointerleave")
public void setPointerLeave(@NonNull T view, @Nullable boolean value) {
view.setTag(R.id.pointer_leave, value);
}
@ReactProp(name = "pointermove")
public void setPointerMove(@NonNull T view, @Nullable boolean value) {
view.setTag(R.id.pointer_move, value);
}
}