com.facebook.react.views.text.ReactTextShadowNode.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.react.views.text.ReactTextShadowNode.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.views.text;

import android.annotation.TargetApi;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.view.Gravity;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactNoCrashSoftException;
import com.facebook.react.bridge.ReactSoftException;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.NativeViewHierarchyOptimizer;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import com.facebook.yoga.YogaMeasureFunction;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import com.facebook.yoga.YogaNode;
import java.util.ArrayList;

/**
 * {@link ReactBaseTextShadowNode} concrete class for anchor {@code Text} node.
 *
 * <p>The class measures text in {@code <Text>} view and feeds native {@link TextView} using {@code
 * Spannable} object constructed in superclass.
 */
@TargetApi(Build.VERSION_CODES.M)
public class ReactTextShadowNode extends ReactBaseTextShadowNode {

    // It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it
    // later by calling setFlags. This is because the latter approach triggers a bug on Android 4.4.2.
    // The bug is that unicode emoticons aren't measured properly which causes text to be clipped.
    private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);

    private @Nullable Spannable mPreparedSpannableText;

    private boolean mShouldNotifyOnTextLayout;

    private final YogaMeasureFunction mTextMeasureFunction = new YogaMeasureFunction() {
        @Override
        public long measure(YogaNode node, float width, YogaMeasureMode widthMode, float height,
                YogaMeasureMode heightMode) {

            // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
            TextPaint textPaint = sTextPaintInstance;
            textPaint.setTextSize(mTextAttributes.getEffectiveFontSize());
            Layout layout;
            Spanned text = Assertions.assertNotNull(mPreparedSpannableText,
                    "Spannable element has not been prepared in onBeforeLayout");
            BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
            float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN;

            // technically, width should never be negative, but there is currently a bug in
            boolean unconstrainedWidth = widthMode == YogaMeasureMode.UNDEFINED || width < 0;

            Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
            switch (getTextAlign()) {
            case Gravity.LEFT:
                alignment = Layout.Alignment.ALIGN_NORMAL;
                break;
            case Gravity.RIGHT:
                alignment = Layout.Alignment.ALIGN_OPPOSITE;
                break;
            case Gravity.CENTER_HORIZONTAL:
                alignment = Layout.Alignment.ALIGN_CENTER;
                break;
            }

            if (boring == null && (unconstrainedWidth
                    || (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
                // Is used when the width is not known and the text is not boring, ie. if it contains
                // unicode characters.

                int hintWidth = (int) Math.ceil(desiredWidth);
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                    layout = new StaticLayout(text, textPaint, hintWidth, alignment, 1.f, 0.f, mIncludeFontPadding);
                } else {
                    StaticLayout.Builder builder = StaticLayout.Builder
                            .obtain(text, 0, text.length(), textPaint, hintWidth).setAlignment(alignment)
                            .setLineSpacing(0.f, 1.f).setIncludePad(mIncludeFontPadding)
                            .setBreakStrategy(mTextBreakStrategy).setHyphenationFrequency(mHyphenationFrequency);

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        builder.setJustificationMode(mJustificationMode);
                    }
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        builder.setUseLineSpacingFromFallbacks(true);
                    }
                    layout = builder.build();
                }

            } else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
                // Is used for single-line, boring text when the width is either unknown or bigger
                // than the width of the text.
                layout = BoringLayout.make(text, textPaint, boring.width, alignment, 1.f, 0.f, boring,
                        mIncludeFontPadding);
            } else {
                // Is used for multiline, boring text and the width is known.

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                    layout = new StaticLayout(text, textPaint, (int) width, alignment, 1.f, 0.f,
                            mIncludeFontPadding);
                } else {
                    StaticLayout.Builder builder = StaticLayout.Builder
                            .obtain(text, 0, text.length(), textPaint, (int) width).setAlignment(alignment)
                            .setLineSpacing(0.f, 1.f).setIncludePad(mIncludeFontPadding)
                            .setBreakStrategy(mTextBreakStrategy).setHyphenationFrequency(mHyphenationFrequency);

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        builder.setUseLineSpacingFromFallbacks(true);
                    }
                    layout = builder.build();
                }
            }

            if (mShouldNotifyOnTextLayout) {
                ThemedReactContext themedReactContext = getThemedContext();
                WritableArray lines = FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance,
                        themedReactContext);
                WritableMap event = Arguments.createMap();
                event.putArray("lines", lines);
                if (themedReactContext.hasActiveCatalystInstance()) {
                    themedReactContext.getJSModule(RCTEventEmitter.class).receiveEvent(getReactTag(),
                            "topTextLayout", event);
                } else {
                    ReactSoftException.logSoftException("ReactTextShadowNode",
                            new ReactNoCrashSoftException("Cannot get RCTEventEmitter, no CatalystInstance"));
                }
            }

            if (mNumberOfLines != UNSET && mNumberOfLines < layout.getLineCount()) {
                return YogaMeasureOutput.make(layout.getWidth(), layout.getLineBottom(mNumberOfLines - 1));
            } else {
                return YogaMeasureOutput.make(layout.getWidth(), layout.getHeight());
            }
        }
    };

    public ReactTextShadowNode() {
        initMeasureFunction();
    }

    private void initMeasureFunction() {
        if (!isVirtual()) {
            setMeasureFunction(mTextMeasureFunction);
        }
    }

    // Return text alignment according to LTR or RTL style
    private int getTextAlign() {
        int textAlign = mTextAlign;
        if (getLayoutDirection() == YogaDirection.RTL) {
            if (textAlign == Gravity.RIGHT) {
                textAlign = Gravity.LEFT;
            } else if (textAlign == Gravity.LEFT) {
                textAlign = Gravity.RIGHT;
            }
        }
        return textAlign;
    }

    @Override
    public void onBeforeLayout(NativeViewHierarchyOptimizer nativeViewHierarchyOptimizer) {
        mPreparedSpannableText = spannedFromShadowNode(this, /* text (e.g. from `value` prop): */ null,
                /* supportsInlineViews: */ true, nativeViewHierarchyOptimizer);
        markUpdated();
    }

    @Override
    public boolean isVirtualAnchor() {
        // Text's descendants aren't necessarily all virtual nodes. Text can contain a combination of
        // virtual and non-virtual (e.g. inline views) nodes. Therefore it's not a virtual anchor
        // by the doc comment on {@link ReactShadowNode#isVirtualAnchor}.
        return false;
    }

    @Override
    public boolean hoistNativeChildren() {
        return true;
    }

    @Override
    public void markUpdated() {
        super.markUpdated();
        // Telling to Yoga that the node should be remeasured on next layout pass.
        super.dirty();
    }

    @Override
    public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
        super.onCollectExtraUpdates(uiViewOperationQueue);

        if (mPreparedSpannableText != null) {
            ReactTextUpdate reactTextUpdate = new ReactTextUpdate(mPreparedSpannableText, UNSET, mContainsImages,
                    getPadding(Spacing.START), getPadding(Spacing.TOP), getPadding(Spacing.END),
                    getPadding(Spacing.BOTTOM), getTextAlign(), mTextBreakStrategy, mJustificationMode);
            uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);
        }
    }

    @ReactProp(name = "onTextLayout")
    public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) {
        mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout;
    }

    @Override
    public Iterable<? extends ReactShadowNode> calculateLayoutOnChildren() {
        // Run flexbox on and return the descendants which are inline views.

        if (mInlineViews == null || mInlineViews.isEmpty()) {
            return null;
        }

        Spanned text = Assertions.assertNotNull(this.mPreparedSpannableText,
                "Spannable element has not been prepared in onBeforeLayout");
        TextInlineViewPlaceholderSpan[] placeholders = text.getSpans(0, text.length(),
                TextInlineViewPlaceholderSpan.class);
        ArrayList<ReactShadowNode> shadowNodes = new ArrayList<ReactShadowNode>(placeholders.length);

        for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
            ReactShadowNode child = mInlineViews.get(placeholder.getReactTag());
            child.calculateLayout();
            shadowNodes.add(child);
        }

        return shadowNodes;
    }
}