com.facebook.react.uimanager.BaseViewManager.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.react.uimanager.BaseViewManager.java

Source

/*
 * 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.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.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;
        }
        view.setTag(R.id.accessibility_state, accessibilityState);
        view.setSelected(false);
        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);
    }
}