Implement and integrate Mapbuffer

Summary:
This diff contains the code from the 35 diff stack  - D27210587

This diff implement and integrates Mapbuffer into Fabric text measure system

changelog: [internal] internal

Reviewed By: JoshuaGross

Differential Revision: D27241836

fbshipit-source-id: f40a780df0723f27da440f709a8676cfcca63953
This commit is contained in:
David Vacca
2021-03-24 03:52:31 -07:00
committed by Facebook GitHub Bot
parent a15a46c78e
commit 91b3f5d48a
71 changed files with 2617 additions and 203 deletions
@@ -2,6 +2,7 @@ load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "rn_android_build
SUB_PROJECTS = [
"network/**/*",
"mapbuffer/**/*",
]
rn_android_library(
@@ -0,0 +1,26 @@
load("//tools/build_defs/oss:rn_defs.bzl", "FBJNI_TARGET", "react_native_dep", "react_native_target", "rn_android_library")
rn_android_library(
name = "mapbuffer",
srcs = glob([
"*.java",
]),
autoglob = False,
is_androidx = True,
labels = ["supermodule:xplat/default/public.react_native.infra"],
provided_deps = [],
required_for_source_only_abi = True,
visibility = [
"PUBLIC",
],
deps = [
FBJNI_TARGET,
react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"),
react_native_target("java/com/facebook/react/common/mapbuffer/jni:jni"),
react_native_dep("libraries/fbjni:java"),
react_native_dep("third-party/android/androidx:annotation"),
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
react_native_target("java/com/facebook/react/common:common"),
],
exported_deps = [],
)
@@ -0,0 +1,348 @@
/*
* 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.common.mapbuffer;
import androidx.annotation.Nullable;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.soloader.SoLoader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Iterator;
/**
* TODO T83483191: add documentation.
*
* <p>NOTE: {@link ReadableMapBuffer} is NOT thread safe.
*/
public class ReadableMapBuffer implements Iterable<ReadableMapBuffer.MapBufferEntry> {
static {
SoLoader.loadLibrary("mapbufferjni");
}
// Value used to verify if the data is serialized with LittleEndian order.
private static final int ALIGNMENT = 0xFE;
// 6 bytes = 2 (alignment) + 2 (count) + 2 (size)
private static final int HEADER_SIZE = 6;
// key size = 2 bytes
private static final int KEY_SIZE = 2;
// 10 bytes = 2 bytes key + 8 bytes value
private static final int BUCKET_SIZE = 10;
private static final int INT_SIZE = 4;
// TODO T83483191: consider moving short to INTs, we are doing extra cast operations just because
// of short java operates with int
private static final int SHORT_SIZE = 2;
private static final short SHORT_ONE = (short) 1;
@Nullable ByteBuffer mBuffer = null;
// Size of the Serialized Data
@SuppressWarnings("unused")
private short mSizeOfData = 0;
// Amount of items serialized on the ByteBuffer
@SuppressWarnings("unused")
private short mCount = 0;
private ReadableMapBuffer(HybridData hybridData) {
mHybridData = hybridData;
}
private ReadableMapBuffer(ByteBuffer buffer) {
mBuffer = buffer;
readHeader();
}
private native ByteBuffer importByteBufferAllocateDirect();
private native ByteBuffer importByteBuffer();
@SuppressWarnings("unused")
@DoNotStrip
@Nullable
private HybridData mHybridData;
@Override
protected void finalize() throws Throwable {
super.finalize();
if (mHybridData != null) {
mHybridData.resetNative();
}
}
private int getKeyOffsetForBucketIndex(int bucketIndex) {
return HEADER_SIZE + BUCKET_SIZE * bucketIndex;
}
private int getValueOffsetForKey(short key) {
importByteBufferAndReadHeader();
int bucketIndex = getBucketIndexForKey(key);
if (bucketIndex == -1) {
// TODO T83483191: Add tests
throw new IllegalArgumentException("Unable to find key: " + key);
}
assertKeyExists(key, bucketIndex);
return getKeyOffsetForBucketIndex(bucketIndex) + KEY_SIZE;
}
// returns the relative offset of the first byte of dynamic data
private int getOffsetForDynamicData() {
// TODO T83483191: check if there's dynamic data?
return getKeyOffsetForBucketIndex(mCount);
}
/**
* @param key Key to search for
* @return the "bucket index" for a key or -1 if not found. It uses a binary search algorithm
* (log(n))
*/
private int getBucketIndexForKey(short key) {
short lo = 0;
short hi = (short) (getCount() - SHORT_ONE);
while (lo <= hi) {
final short mid = (short) ((lo + hi) >>> SHORT_ONE);
final short midVal = readKey(getKeyOffsetForBucketIndex(mid));
if (midVal < key) {
lo = (short) (mid + SHORT_ONE);
} else if (midVal > key) {
hi = (short) (mid - SHORT_ONE);
} else {
return mid;
}
}
return -1;
}
private short readKey(int position) {
return mBuffer.getShort(position);
}
private double readDoubleValue(int bufferPosition) {
return mBuffer.getDouble(bufferPosition);
}
private int readIntValue(int bufferPosition) {
return mBuffer.getInt(bufferPosition);
}
private boolean readBooleanValue(int bufferPosition) {
return readIntValue(bufferPosition) == 1;
}
private String readStringValue(int bufferPosition) {
int offset = getOffsetForDynamicData() + mBuffer.getInt(bufferPosition);
int sizeOfString = mBuffer.getInt(offset);
byte[] result = new byte[sizeOfString];
int stringOffset = offset + INT_SIZE;
mBuffer.position(stringOffset);
mBuffer.get(result, 0, sizeOfString);
return new String(result);
}
private ReadableMapBuffer readMapBufferValue(int position) {
int offset = getOffsetForDynamicData() + mBuffer.getInt(position);
int sizeMapBuffer = mBuffer.getShort(offset);
byte[] buffer = new byte[sizeMapBuffer];
int bufferOffset = offset + SHORT_SIZE;
mBuffer.position(bufferOffset);
mBuffer.get(buffer, 0, sizeMapBuffer);
return new ReadableMapBuffer(ByteBuffer.wrap(buffer));
}
private void readHeader() {
// byte order
short storedAlignment = mBuffer.getShort();
if (storedAlignment != ALIGNMENT) {
mBuffer.order(ByteOrder.LITTLE_ENDIAN);
}
// count
mCount = mBuffer.getShort();
// size
mSizeOfData = mBuffer.getShort();
}
/**
* Binary search of the key inside the mapBuffer (log(n)).
*
* @param key Key to search for
* @return true if and only if the Key received as a parameter is stored in the MapBuffer.
*/
public boolean hasKey(short key) {
// TODO T83483191: Add tests
return getBucketIndexForKey(key) != -1;
}
/** @return amount of elements stored into the MapBuffer */
public short getCount() {
importByteBufferAndReadHeader();
return mCount;
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public int getInt(short key) {
// TODO T83483191: extract common code of "get methods"
return readIntValue(getValueOffsetForKey(key));
}
/**
* @param key {@link int} representing the key
* @return return the double associated to the Key received as a parameter.
*/
public double getDouble(short key) {
return readDoubleValue(getValueOffsetForKey(key));
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public String getString(short key) {
return readStringValue(getValueOffsetForKey(key));
}
public boolean getBoolean(short key) {
return readBooleanValue(getValueOffsetForKey(key));
}
/**
* @param key {@link int} representing the key
* @return return the int associated to the Key received as a parameter.
*/
public ReadableMapBuffer getMapBuffer(short key) {
return readMapBufferValue(getValueOffsetForKey(key));
}
/**
* Import ByteBuffer from C++, read the header and move the current cursor at the start of the
* payload.
*/
private ByteBuffer importByteBufferAndReadHeader() {
if (mBuffer != null) {
return mBuffer;
}
// mBuffer = importByteBufferAllocateDirect();
mBuffer = importByteBuffer();
readHeader();
return mBuffer;
}
private void assertKeyExists(short key, int bucketIndex) {
short storedKey = mBuffer.getShort(getKeyOffsetForBucketIndex(bucketIndex));
if (storedKey != key) {
throw new IllegalStateException(
"Stored key doesn't match parameter - expected: " + key + " - found: " + storedKey);
}
}
@Override
public int hashCode() {
ByteBuffer byteBuffer = importByteBufferAndReadHeader();
byteBuffer.rewind();
return byteBuffer.hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof ReadableMapBuffer)) {
return false;
}
ReadableMapBuffer other = (ReadableMapBuffer) obj;
ByteBuffer thisByteBuffer = importByteBufferAndReadHeader();
ByteBuffer otherByteBuffer = other.importByteBufferAndReadHeader();
if (thisByteBuffer == otherByteBuffer) {
return true;
}
thisByteBuffer.rewind();
otherByteBuffer.rewind();
return thisByteBuffer.equals(otherByteBuffer);
}
/** @return an {@link Iterator<MapBufferEntry>} for the entries of this MapBuffer. */
@Override
public Iterator<MapBufferEntry> iterator() {
return new Iterator<MapBufferEntry>() {
short current = 0;
short last = (short) (getCount() - SHORT_ONE);
@Override
public boolean hasNext() {
return current <= last;
}
@Override
public MapBufferEntry next() {
return new MapBufferEntry(getKeyOffsetForBucketIndex(current++));
}
};
}
/** This class represents an Entry of the {@link ReadableMapBuffer} class. */
public class MapBufferEntry {
private final int mBucketOffset;
private MapBufferEntry(int position) {
mBucketOffset = position;
}
/** @return a {@link short} that represents the key of this {@link MapBufferEntry}. */
public short getKey() {
return readKey(mBucketOffset);
}
/** @return the double value that is stored in this {@link MapBufferEntry}. */
public double getDouble(double defaultValue) {
// TODO T83483191 Extend serialization of MapBuffer to add type checking
// TODO T83483191 Extend serialization of MapBuffer to return null if there's no value
// stored in this MapBufferEntry.
return readDoubleValue(mBucketOffset + KEY_SIZE);
}
/** @return the int value that is stored in this {@link MapBufferEntry}. */
public int getInt(int defaultValue) {
return readIntValue(mBucketOffset + KEY_SIZE);
}
/** @return the boolean value that is stored in this {@link MapBufferEntry}. */
public boolean getBoolean(boolean defaultValue) {
return readBooleanValue(mBucketOffset + KEY_SIZE);
}
/** @return the String value that is stored in this {@link MapBufferEntry}. */
public @Nullable String getString() {
return readStringValue(mBucketOffset + KEY_SIZE);
}
/**
* @return the {@link ReadableMapBuffer} value that is stored in this {@link MapBufferEntry}.
*/
public @Nullable ReadableMapBuffer getReadableMapBuffer() {
return readMapBufferValue(mBucketOffset + KEY_SIZE);
}
}
}
@@ -0,0 +1,39 @@
# 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.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := mapbufferjni
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/react/common/mapbuffer/*.cpp)
LOCAL_SHARED_LIBRARIES := libreactconfig libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_utils libreact_render_mapbuffer libreact_debug
LOCAL_STATIC_LIBRARIES :=
LOCAL_C_INCLUDES := $(LOCAL_PATH)/
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/
LOCAL_CFLAGS := \
-DLOG_TAG=\"Fabric\"
LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall
include $(BUILD_SHARED_LIBRARY)
$(call import-module,fbgloginit)
$(call import-module,folly)
$(call import-module,fb)
$(call import-module,fbjni)
$(call import-module,yogajni)
$(call import-module,glog)
$(call import-module,react/utils)
$(call import-module,react/debug)
$(call import-module,react/config)
$(call import-module,react/renderer/mapbuffer)
@@ -0,0 +1,33 @@
load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "FBJNI_TARGET", "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob")
rn_xplat_cxx_library(
name = "jni",
srcs = glob(["**/*.cpp"]),
headers = glob(["**/*.h"]),
header_namespace = "",
exported_headers = subdir_glob(
[
("react/common/mapbuffer", "*.h"),
],
prefix = "react/common/mapbuffer",
),
compiler_flags = [
"-fexceptions",
"-frtti",
"-std=c++14",
"-Wall",
],
fbandroid_allow_jni_merging = True,
labels = ["supermodule:xplat/default/public.react_native.infra"],
platforms = (ANDROID),
preprocessor_flags = [
"-DLOG_TAG=\"ReactNative\"",
"-DWITH_FBSYSTRACE=1",
],
soname = "libmapbufferjni.$(ext)",
visibility = ["PUBLIC"],
deps = [
react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"),
FBJNI_TARGET,
],
)
@@ -0,0 +1,15 @@
/*
* 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.
*/
#include <fbjni/fbjni.h>
#include "ReadableMapBuffer.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(
vm, [] { facebook::react::ReadableMapBuffer::registerNatives(); });
}
@@ -0,0 +1,62 @@
/*
* 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.
*/
#include "ReadableMapBuffer.h"
namespace facebook {
namespace react {
void ReadableMapBuffer::registerNatives() {
registerHybrid({
makeNativeMethod("importByteBuffer", ReadableMapBuffer::importByteBuffer),
makeNativeMethod(
"importByteBufferAllocateDirect",
ReadableMapBuffer::importByteBufferAllocateDirect),
});
}
jni::local_ref<jni::JByteBuffer>
ReadableMapBuffer::importByteBufferAllocateDirect() {
// TODO: Using this method is safer than "importByteBuffer" because ByteBuffer
// memory will be deallocated once the "Java ByteBuffer" is deallocated. Next
// steps:
// - Validate perf of this method vs importByteBuffer
// - Validate that there's no leaking of memory
return jni::JByteBuffer::allocateDirect(_serializedDataSize);
}
jni::JByteBuffer::javaobject ReadableMapBuffer::importByteBuffer() {
// TODO: Reevaluate what's the best approach here (allocateDirect vs
// DirectByteBuffer).
//
// On this method we should:
// - Review deallocation of serializedData (we are probably leaking
// _serializedData now).
// - Consider using allocate() or allocateDirect() methods from java instead
// of newDirectByteBuffer (to simplify de/allocation) :
// https://www.internalfb.com/intern/diffusion/FBS/browsefile/master/fbandroid/libraries/fbjni/cxx/fbjni/ByteBuffer.cpp
// - Add flags to describe if the data was already 'imported'
// - Long-term: Consider creating a big ByteBuffer that can be re-used to
// transfer data of multitple Maps
return static_cast<jni::JByteBuffer::javaobject>(
jni::Environment::current()->NewDirectByteBuffer(
(void *)_serializedData, _serializedDataSize));
}
jni::local_ref<ReadableMapBuffer::jhybridobject>
ReadableMapBuffer::createWithContents(MapBuffer &&map) {
return newObjectCxxArgs(std::move(map));
}
ReadableMapBuffer::~ReadableMapBuffer() {
delete[] _serializedData;
_serializedData = nullptr;
_serializedDataSize = 0;
}
} // namespace react
} // namespace facebook
@@ -0,0 +1,48 @@
/*
* 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.
*/
#pragma once
#include <fbjni/fbjni.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <fbjni/ByteBuffer.h>
namespace facebook {
namespace react {
class ReadableMapBuffer : public jni::HybridClass<ReadableMapBuffer> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/facebook/react/common/mapbuffer/ReadableMapBuffer;";
static void registerNatives();
static jni::local_ref<jhybridobject> createWithContents(MapBuffer &&map);
jni::local_ref<jni::JByteBuffer> importByteBufferAllocateDirect();
jni::JByteBuffer::javaobject importByteBuffer();
~ReadableMapBuffer();
private:
uint8_t *_serializedData = nullptr;
int _serializedDataSize = 0;
friend HybridBase;
explicit ReadableMapBuffer(MapBuffer &&map) {
_serializedDataSize = map.getBufferSize();
_serializedData = new Byte[_serializedDataSize];
map.copy(_serializedData);
}
};
} // namespace react
} // namespace facebook
@@ -65,4 +65,7 @@ public class ReactFeatureFlags {
/** Enables JS Responder in Fabric */
public static boolean enableJSResponder = false;
/** Enables MapBuffer Serialization */
public static boolean mapBufferSerializationEnabled = false;
}
@@ -37,6 +37,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/modules/core:core"),
react_native_target("java/com/facebook/react/modules/i18nmanager:i18nmanager"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"),
react_native_target("java/com/facebook/react/uimanager:uimanager"),
react_native_target("java/com/facebook/react/views/view:view"),
react_native_target("java/com/facebook/react/views/text:text"),
@@ -53,6 +53,7 @@ import com.facebook.react.bridge.UIManagerListener;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.fabric.events.EventBeatManager;
import com.facebook.react.fabric.events.EventEmitterWrapper;
@@ -81,6 +82,7 @@ import com.facebook.react.uimanager.ViewManagerRegistry;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.EventDispatcherImpl;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
import com.facebook.systrace.Systrace;
import java.util.ArrayList;
import java.util.Collection;
@@ -418,6 +420,21 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
PixelUtil.toPixelFromDIP(width));
}
@DoNotStrip
@SuppressWarnings("unused")
private NativeArray measureLinesMapBuffer(
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float width,
float height) {
return (NativeArray)
TextLayoutManagerMapBuffer.measureLines(
mReactApplicationContext,
attributedString,
paragraphAttributes,
PixelUtil.toPixelFromDIP(width));
}
@DoNotStrip
@SuppressWarnings("unused")
private long measure(
@@ -482,6 +499,44 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
attachmentsPositions);
}
@DoNotStrip
@SuppressWarnings("unused")
private long measureMapBuffer(
int surfaceId,
String componentName,
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float minWidth,
float maxWidth,
float minHeight,
float maxHeight,
@Nullable float[] attachmentsPositions) {
ReactContext context;
if (surfaceId > 0) {
SurfaceMountingManager surfaceMountingManager =
mMountingManager.getSurfaceManagerEnforced(surfaceId, "measure");
if (surfaceMountingManager.isStopped()) {
return 0;
}
context = surfaceMountingManager.getContext();
} else {
context = mReactApplicationContext;
}
// TODO: replace ReadableNativeMap -> ReadableMapBuffer
return mMountingManager.measureTextMapBuffer(
context,
componentName,
attributedString,
paragraphAttributes,
getYogaSize(minWidth, maxWidth),
getYogaMeasureMode(minWidth, maxWidth),
getYogaSize(minHeight, maxHeight),
getYogaMeasureMode(minHeight, maxHeight),
attachmentsPositions);
}
/**
* @param surfaceId {@link int} surface ID
* @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme
@@ -16,6 +16,7 @@ import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.NativeMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.uimanager.StateWrapper;
/**
@@ -41,6 +42,18 @@ public class StateWrapperImpl implements StateWrapper {
private native ReadableNativeMap getStateDataImpl();
private native ReadableMapBuffer getStateMapBufferDataImpl();
@Override
@Nullable
public ReadableMapBuffer getStatDataMapBuffer() {
if (mDestroyed) {
FLog.e(TAG, "Race between StateWrapperImpl destruction and getState");
return null;
}
return getStateMapBufferDataImpl();
}
@Override
@Nullable
public ReadableNativeMap getStateData() {
@@ -11,7 +11,7 @@ LOCAL_MODULE := fabricjni
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
LOCAL_SHARED_LIBRARIES := libreactconfig librrc_slider librrc_progressbar librrc_switch librrc_modal libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_render_mounting libreactnativeutilsjni libreact_utils libreact_render_debug libreact_render_graphics libreact_render_core libreact_render_mapbuffer react_render_componentregistry librrc_view librrc_unimplementedview librrc_root librrc_scrollview libbetter libreact_render_attributedstring libreact_render_uimanager libreact_render_templateprocessor libreact_render_scheduler libreact_render_animations libreact_render_imagemanager libreact_render_textlayoutmanager libreact_codegen_rncore rrc_text librrc_image librrc_textinput librrc_picker libreact_debug
LOCAL_SHARED_LIBRARIES := libreactconfig librrc_slider librrc_progressbar librrc_switch librrc_modal libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_render_mounting libreactnativeutilsjni libreact_utils libreact_render_debug libreact_render_graphics libreact_render_core react_render_componentregistry librrc_view librrc_unimplementedview librrc_root librrc_scrollview libbetter libreact_render_attributedstring libreact_render_uimanager libreact_render_templateprocessor libreact_render_scheduler libreact_render_animations libreact_render_imagemanager libreact_render_textlayoutmanager libreact_codegen_rncore rrc_text librrc_image librrc_textinput librrc_picker libreact_debug libreact_render_mapbuffer libmapbufferjni
LOCAL_STATIC_LIBRARIES :=
@@ -27,6 +27,7 @@ rn_xplat_cxx_library(
soname = "libfabricjni.$(ext)",
visibility = ["PUBLIC"],
deps = [
react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"),
react_native_xplat_target("react/config:config"),
react_native_xplat_target("react/renderer/animations:animations"),
react_native_xplat_target("react/renderer/uimanager:uimanager"),
@@ -508,6 +508,11 @@ void Binding::installFabricUIManager(
// Keep reference to config object and cache some feature flags here
reactNativeConfig_ = config;
contextContainer->insert(
"MapBufferSerializationEnabled",
reactNativeConfig_->getBool(
"react_fabric:enable_mapbuffer_serialization_android"));
disablePreallocateViews_ = reactNativeConfig_->getBool(
"react_fabric:disabled_view_preallocation_android");
@@ -8,6 +8,8 @@
#include "StateWrapperImpl.h"
#include <fbjni/fbjni.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
using namespace facebook::jni;
@@ -30,6 +32,14 @@ StateWrapperImpl::getStateDataImpl() {
return readableNativeMap;
}
jni::local_ref<ReadableMapBuffer::jhybridobject>
StateWrapperImpl::getStateMapBufferDataImpl() {
MapBuffer map = state_->getMapBuffer();
auto ReadableMapBuffer =
ReadableMapBuffer::createWithContents(std::move(map));
return ReadableMapBuffer;
}
void StateWrapperImpl::updateStateImpl(NativeMap *map) {
// Get folly::dynamic from map
auto dynamicMap = map->consume();
@@ -42,6 +52,9 @@ void StateWrapperImpl::registerNatives() {
makeNativeMethod("initHybrid", StateWrapperImpl::initHybrid),
makeNativeMethod("getStateDataImpl", StateWrapperImpl::getStateDataImpl),
makeNativeMethod("updateStateImpl", StateWrapperImpl::updateStateImpl),
makeNativeMethod(
"getStateMapBufferDataImpl",
StateWrapperImpl::getStateMapBufferDataImpl),
});
}
@@ -8,6 +8,7 @@
#pragma once
#include <fbjni/fbjni.h>
#include <react/common/mapbuffer/ReadableMapBuffer.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/renderer/core/State.h>
@@ -25,6 +26,7 @@ class StateWrapperImpl : public jni::HybridClass<StateWrapperImpl> {
static void registerNatives();
jni::local_ref<ReadableMapBuffer::jhybridobject> getStateMapBufferDataImpl();
jni::local_ref<ReadableNativeMap::jhybridobject> getStateDataImpl();
void updateStateImpl(NativeMap *map);
void updateStateWithFailureCallbackImpl(
@@ -9,6 +9,7 @@ package com.facebook.react.fabric.mounting;
import static com.facebook.infer.annotation.ThreadConfined.ANY;
import android.text.Spannable;
import android.view.View;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
@@ -22,6 +23,7 @@ import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.RetryableMountingLayerException;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.fabric.events.EventEmitterWrapper;
import com.facebook.react.fabric.mounting.mountitems.MountItem;
@@ -30,6 +32,8 @@ import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.RootViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerRegistry;
import com.facebook.react.views.text.ReactTextViewManagerCallback;
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
import com.facebook.yoga.YogaMeasureMode;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -337,6 +341,49 @@ public class MountingManager {
attachmentsPositions);
}
/**
* Measure a component, given localData, props, state, and measurement information. This needs to
* remain here for now - and not in SurfaceMountingManager - because sometimes measures are made
* outside of the context of a Surface; especially from C++ before StartSurface is called.
*
* @param context
* @param componentName
* @param attributedString
* @param paragraphAttributes
* @param width
* @param widthMode
* @param height
* @param heightMode
* @param attachmentsPositions
* @return
*/
@AnyThread
public long measureTextMapBuffer(
@NonNull ReactContext context,
@NonNull String componentName,
@NonNull ReadableMapBuffer attributedString,
@NonNull ReadableMapBuffer paragraphAttributes,
float width,
@NonNull YogaMeasureMode widthMode,
float height,
@NonNull YogaMeasureMode heightMode,
@Nullable float[] attachmentsPositions) {
return TextLayoutManagerMapBuffer.measureText(
context,
attributedString,
paragraphAttributes,
width,
widthMode,
height,
heightMode,
new ReactTextViewManagerCallback() {
@Override
public void onPostProcessSpannable(Spannable text) {}
},
attachmentsPositions);
}
public void initializeViewManager(String componentName) {
mViewManagerRegistry.get(componentName);
}
@@ -39,6 +39,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/uimanager/jni:jni"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"),
react_native_target("java/com/facebook/react/config:config"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/modules/core:core"),
@@ -9,6 +9,7 @@ package com.facebook.react.uimanager;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import javax.annotation.Nullable;
/**
@@ -17,6 +18,15 @@ import javax.annotation.Nullable;
* by calling updateState, which communicates state back to the C++ layer.
*/
public interface StateWrapper {
/**
* Get a ReadableMapBuffer object from the C++ layer, which is a K/V map of short keys to values.
*
* <p>Unstable API - DO NOT USE.
*/
@Nullable
ReadableMapBuffer getStatDataMapBuffer();
/**
* Get a ReadableNativeMap object from the C++ layer, which is a K/V map of string keys to values.
*/
@@ -23,6 +23,7 @@ rn_android_library(
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"),
react_native_target("java/com/facebook/react/config:config"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/uimanager:uimanager"),
@@ -14,6 +14,8 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.IViewManagerWithChildren;
import com.facebook.react.uimanager.ReactStylesDiffMap;
@@ -31,6 +33,12 @@ public class ReactTextViewManager
extends ReactTextAnchorViewManager<ReactTextView, ReactTextShadowNode>
implements IViewManagerWithChildren {
private static final short TX_STATE_KEY_ATTRIBUTED_STRING = 0;
private static final short TX_STATE_KEY_PARAGRAPH_ATTRIBUTES = 1;
// used for text input
private static final short TX_STATE_KEY_HASH = 2;
private static final short TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3;
@VisibleForTesting public static final String REACT_CLASS = "RCTText";
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
@@ -87,6 +95,13 @@ public class ReactTextViewManager
return null;
}
if (ReactFeatureFlags.mapBufferSerializationEnabled) {
ReadableMapBuffer stateMapBuffer = stateWrapper.getStatDataMapBuffer();
if (stateMapBuffer != null) {
return getReactTextUpdate(view, props, stateMapBuffer);
}
}
ReadableNativeMap state = stateWrapper.getStateData();
if (state == null) {
return null;
@@ -111,6 +126,30 @@ public class ReactTextViewManager
TextAttributeProps.getJustificationMode(props));
}
private Object getReactTextUpdate(
ReactTextView view, ReactStylesDiffMap props, ReadableMapBuffer state) {
ReadableMapBuffer attributedString = state.getMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING);
ReadableMapBuffer paragraphAttributes = state.getMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES);
Spannable spanned =
TextLayoutManagerMapBuffer.getOrCreateSpannableForText(
view.getContext(), attributedString, mReactTextViewManagerCallback);
view.setSpanned(spanned);
int textBreakStrategy =
TextAttributeProps.getTextBreakStrategy(
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));
return new ReactTextUpdate(
spanned,
-1, // UNUSED FOR TEXT
false, // TODO add this into local Data
TextAttributeProps.getTextAlignment(
props, TextLayoutManagerMapBuffer.isRTL(attributedString)),
textBreakStrategy,
TextAttributeProps.getJustificationMode(props));
}
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
@@ -10,23 +10,52 @@ package com.facebook.react.views.text;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Layout;
import android.text.TextUtils;
import android.util.LayoutDirection;
import android.view.Gravity;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
// TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents
// TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to
// TextAttributeProps (e.g. TextAlign)
public class TextAttributeProps {
private static final String INLINE_IMAGE_PLACEHOLDER = "I";
// constants for Text Attributes serialization
public static final short TA_KEY_FOREGROUND_COLOR = 0;
public static final short TA_KEY_BACKGROUND_COLOR = 1;
public static final short TA_KEY_OPACITY = 2;
public static final short TA_KEY_FONT_FAMILY = 3;
public static final short TA_KEY_FONT_SIZE = 4;
public static final short TA_KEY_FONT_SIZE_MULTIPLIER = 5;
public static final short TA_KEY_FONT_WEIGHT = 6;
public static final short TA_KEY_FONT_STYLE = 7;
public static final short TA_KEY_FONT_VARIANT = 8;
public static final short TA_KEY_ALLOW_FONT_SCALING = 9;
public static final short TA_KEY_LETTER_SPACING = 10;
public static final short TA_KEY_LINE_HEIGHT = 11;
public static final short TA_KEY_ALIGNMENT = 12;
public static final short TA_KEY_BEST_WRITING_DIRECTION = 13;
public static final short TA_KEY_TEXT_DECORATION_COLOR = 14;
public static final short TA_KEY_TEXT_DECORATION_LINE = 15;
public static final short TA_KEY_TEXT_DECORATION_LINE_STYLE = 16;
public static final short TA_KEY_TEXT_DECORATION_LINE_PATTERN = 17;
public static final short TA_KEY_TEXT_SHADOW_RAIDUS = 18;
public static final short TA_KEY_TEXT_SHADOW_COLOR = 19;
public static final short TA_KEY_IS_HIGHLIGHTED = 20;
public static final short TA_KEY_LAYOUT_DIRECTION = 21;
public static final short TA_KEY_ACCESSIBILITY_ROLE = 22;
public static final int UNSET = -1;
private static final String PROP_SHADOW_OFFSET = "textShadowOffset";
@@ -61,7 +90,7 @@ public class TextAttributeProps {
// `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available.
protected int mLayoutDirection = UNSET;
protected TextTransform mTextTransform = TextTransform.UNSET;
protected TextTransform mTextTransform = TextTransform.NONE;
protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
@@ -111,36 +140,123 @@ public class TextAttributeProps {
protected boolean mContainsImages = false;
protected float mHeightOfTallestInlineImage = Float.NaN;
private final ReactStylesDiffMap mProps;
private TextAttributeProps() {}
public TextAttributeProps(ReactStylesDiffMap props) {
mProps = props;
setNumberOfLines(getIntProp(ViewProps.NUMBER_OF_LINES, UNSET));
setLineHeight(getFloatProp(ViewProps.LINE_HEIGHT, UNSET));
setLetterSpacing(getFloatProp(ViewProps.LETTER_SPACING, Float.NaN));
setAllowFontScaling(getBooleanProp(ViewProps.ALLOW_FONT_SCALING, true));
setFontSize(getFloatProp(ViewProps.FONT_SIZE, UNSET));
setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
setColor(
/**
* Build a TextAttributeProps using data from the {@link ReadableMapBuffer} received as a
* parameter.
*/
public static TextAttributeProps fromReadableMapBuffer(ReadableMapBuffer props) {
TextAttributeProps result = new TextAttributeProps();
// TODO T83483191: Review constants that are not being set!
Iterator<ReadableMapBuffer.MapBufferEntry> iterator = props.iterator();
while (iterator.hasNext()) {
ReadableMapBuffer.MapBufferEntry entry = iterator.next();
switch (entry.getKey()) {
case TA_KEY_FOREGROUND_COLOR:
result.setColor(entry.getInt(0));
break;
case TA_KEY_BACKGROUND_COLOR:
result.setBackgroundColor(entry.getInt(0));
break;
case TA_KEY_OPACITY:
break;
case TA_KEY_FONT_FAMILY:
result.setFontFamily(entry.getString());
break;
case TA_KEY_FONT_SIZE:
result.setFontSize((float) entry.getDouble(UNSET));
break;
case TA_KEY_FONT_SIZE_MULTIPLIER:
break;
case TA_KEY_FONT_WEIGHT:
result.setFontWeight(entry.getString());
break;
case TA_KEY_FONT_STYLE:
result.setFontStyle(entry.getString());
break;
case TA_KEY_FONT_VARIANT:
result.setFontVariant(entry.getReadableMapBuffer());
break;
case TA_KEY_ALLOW_FONT_SCALING:
result.setAllowFontScaling(entry.getBoolean(true));
break;
case TA_KEY_LETTER_SPACING:
result.setLetterSpacing((float) entry.getDouble(Float.NaN));
break;
case TA_KEY_LINE_HEIGHT:
result.setLineHeight((float) entry.getDouble(UNSET));
break;
case TA_KEY_ALIGNMENT:
break;
case TA_KEY_BEST_WRITING_DIRECTION:
break;
case TA_KEY_TEXT_DECORATION_COLOR:
break;
case TA_KEY_TEXT_DECORATION_LINE:
result.setTextDecorationLine(entry.getString());
break;
case TA_KEY_TEXT_DECORATION_LINE_STYLE:
break;
case TA_KEY_TEXT_DECORATION_LINE_PATTERN:
break;
case TA_KEY_TEXT_SHADOW_RAIDUS:
result.setTextShadowRadius(entry.getInt(1));
break;
case TA_KEY_TEXT_SHADOW_COLOR:
result.setTextShadowColor(entry.getInt(DEFAULT_TEXT_SHADOW_COLOR));
break;
case TA_KEY_IS_HIGHLIGHTED:
break;
case TA_KEY_LAYOUT_DIRECTION:
result.setLayoutDirection(entry.getString());
break;
case TA_KEY_ACCESSIBILITY_ROLE:
result.setAccessibilityRole(entry.getString());
break;
}
}
// TODO T83483191: Review why the following props are not serialized:
// setNumberOfLines
// setColor
// setIncludeFontPadding
// setTextShadowOffset
// setTextTransform
return result;
}
public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) {
TextAttributeProps result = new TextAttributeProps();
result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, UNSET));
result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, UNSET));
result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN));
result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true));
result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, UNSET));
result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
result.setColor(
props.hasKey(ViewProps.FOREGROUND_COLOR)
? props.getInt(ViewProps.FOREGROUND_COLOR, 0)
: null);
setBackgroundColor(
result.setBackgroundColor(
props.hasKey(ViewProps.BACKGROUND_COLOR)
? props.getInt(ViewProps.BACKGROUND_COLOR, 0)
: null);
setFontFamily(getStringProp(ViewProps.FONT_FAMILY));
setFontWeight(getStringProp(ViewProps.FONT_WEIGHT));
setFontStyle(getStringProp(ViewProps.FONT_STYLE));
setFontVariant(getArrayProp(ViewProps.FONT_VARIANT));
setIncludeFontPadding(getBooleanProp(ViewProps.INCLUDE_FONT_PADDING, true));
setTextDecorationLine(getStringProp(ViewProps.TEXT_DECORATION_LINE));
setTextShadowOffset(props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null);
setTextShadowRadius(getIntProp(PROP_SHADOW_RADIUS, 1));
setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR));
setTextTransform(getStringProp(PROP_TEXT_TRANSFORM));
setLayoutDirection(getStringProp(ViewProps.LAYOUT_DIRECTION));
setAccessibilityRole(getStringProp(ViewProps.ACCESSIBILITY_ROLE));
result.setFontFamily(getStringProp(props, ViewProps.FONT_FAMILY));
result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT));
result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE));
result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT));
result.setIncludeFontPadding(getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true));
result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE));
result.setTextShadowOffset(
props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null);
result.setTextShadowRadius(getIntProp(props, PROP_SHADOW_RADIUS, 1));
result.setTextShadowColor(getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR));
result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM));
result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION));
result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE));
return result;
}
public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL) {
@@ -178,7 +294,8 @@ public class TextAttributeProps {
return DEFAULT_JUSTIFICATION_MODE;
}
private boolean getBooleanProp(String name, boolean defaultValue) {
private static boolean getBooleanProp(
ReactStylesDiffMap mProps, String name, boolean defaultValue) {
if (mProps.hasKey(name)) {
return mProps.getBoolean(name, defaultValue);
} else {
@@ -186,7 +303,7 @@ public class TextAttributeProps {
}
}
private String getStringProp(String name) {
private static String getStringProp(ReactStylesDiffMap mProps, String name) {
if (mProps.hasKey(name)) {
return mProps.getString(name);
} else {
@@ -194,7 +311,7 @@ public class TextAttributeProps {
}
}
private int getIntProp(String name, int defaultvalue) {
private static int getIntProp(ReactStylesDiffMap mProps, String name, int defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getInt(name, defaultvalue);
} else {
@@ -202,7 +319,7 @@ public class TextAttributeProps {
}
}
private float getFloatProp(String name, float defaultvalue) {
private static float getFloatProp(ReactStylesDiffMap mProps, String name, float defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getFloat(name, defaultvalue);
} else {
@@ -210,7 +327,7 @@ public class TextAttributeProps {
}
}
private @Nullable ReadableArray getArrayProp(String name) {
private static @Nullable ReadableArray getArrayProp(ReactStylesDiffMap mProps, String name) {
if (mProps.hasKey(name)) {
return mProps.getArray(name);
} else {
@@ -309,6 +426,40 @@ public class TextAttributeProps {
mFontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariant);
}
private void setFontVariant(@Nullable ReadableMapBuffer fontVariant) {
if (fontVariant == null || fontVariant.getCount() == 0) {
mFontFeatureSettings = null;
return;
}
List<String> features = new ArrayList<>();
Iterator<ReadableMapBuffer.MapBufferEntry> iterator = fontVariant.iterator();
while (iterator.hasNext()) {
ReadableMapBuffer.MapBufferEntry entry = iterator.next();
String value = entry.getString();
if (value != null) {
switch (value) {
case "small-caps":
features.add("'smcp'");
break;
case "oldstyle-nums":
features.add("'onum'");
break;
case "lining-nums":
features.add("'lnum'");
break;
case "tabular-nums":
features.add("'tnum'");
break;
case "proportional-nums":
features.add("'pnum'");
break;
}
}
}
mFontFeatureSettings = TextUtils.join(", ", features);
}
/**
* /* This code is duplicated in ReactTextInputManager /* TODO: Factor into a common place they
* can both use
@@ -380,17 +531,23 @@ public class TextAttributeProps {
}
}
private void setLayoutDirection(@Nullable String layoutDirection) {
public static int getLayoutDirection(@Nullable String layoutDirection) {
int androidLayoutDirection;
if (layoutDirection == null || "undefined".equals(layoutDirection)) {
mLayoutDirection = UNSET;
androidLayoutDirection = UNSET;
} else if ("rtl".equals(layoutDirection)) {
mLayoutDirection = LayoutDirection.RTL;
androidLayoutDirection = LayoutDirection.RTL;
} else if ("ltr".equals(layoutDirection)) {
mLayoutDirection = LayoutDirection.LTR;
androidLayoutDirection = LayoutDirection.LTR;
} else {
throw new JSApplicationIllegalArgumentException(
"Invalid layoutDirection: " + layoutDirection);
}
return androidLayoutDirection;
}
private void setLayoutDirection(@Nullable String layoutDirection) {
mLayoutDirection = getLayoutDirection(layoutDirection);
}
private void setTextShadowRadius(float textShadowRadius) {
@@ -70,11 +70,11 @@ public class TextLayoutManager {
public static boolean isRTL(ReadableMap attributedString) {
ReadableArray fragments = attributedString.getArray("fragments");
for (int i = 0, length = fragments.size(); i < length; i++) {
for (int i = 0; i < fragments.size(); i++) {
ReadableMap fragment = fragments.getMap(i);
ReactStylesDiffMap map = new ReactStylesDiffMap(fragment.getMap("textAttributes"));
TextAttributeProps textAttributes = new TextAttributeProps(map);
return textAttributes.mLayoutDirection == LayoutDirection.RTL;
ReadableMap map = fragment.getMap("textAttributes");
return TextAttributeProps.getLayoutDirection(map.getString(ViewProps.LAYOUT_DIRECTION))
== LayoutDirection.RTL;
}
return false;
}
@@ -105,7 +105,8 @@ public class TextLayoutManager {
// ReactRawText
TextAttributeProps textAttributes =
new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes")));
TextAttributeProps.fromReadableMap(
new ReactStylesDiffMap(fragment.getMap("textAttributes")));
sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform));
@@ -0,0 +1,588 @@
/*
* 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 static com.facebook.react.views.text.TextAttributeProps.UNSET;
import android.content.Context;
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.LayoutDirection;
import android.util.LruCache;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.common.mapbuffer.ReadableMapBuffer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/** Class responsible of creating {@link Spanned} object for the JS representation of Text */
public class TextLayoutManagerMapBuffer {
// constants for AttributedString serialization
public static final short AS_KEY_HASH = 0;
public static final short AS_KEY_STRING = 1;
public static final short AS_KEY_FRAGMENTS = 2;
public static final short AS_KEY_CACHE_ID = 3;
// constants for Fragment serialization
public static final short FR_KEY_STRING = 0;
public static final short FR_KEY_REACT_TAG = 1;
public static final short FR_KEY_IS_ATTACHMENT = 2;
public static final short FR_KEY_WIDTH = 3;
public static final short FR_KEY_HEIGHT = 4;
public static final short FR_KEY_TEXT_ATTRIBUTES = 5;
// constants for ParagraphAttributes serialization
public static final short PA_KEY_MAX_NUMBER_OF_LINES = 0;
public static final short PA_KEY_ELLIPSIZE_MODE = 1;
public static final short PA_KEY_TEXT_BREAK_STRATEGY = 2;
public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3;
public static final short PA_KEY_INCLUDE_FONT_PADDING = 4;
private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false;
private static final String TAG = TextLayoutManagerMapBuffer.class.getSimpleName();
// 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);
// Specifies the amount of spannable that are stored into the {@link sSpannableCache}.
private static final short spannableCacheSize = 100;
private static final String INLINE_VIEW_PLACEHOLDER = "0";
private static final Object sSpannableCacheLock = new Object();
private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
private static final LruCache<ReadableMapBuffer, Spannable> sSpannableCache =
new LruCache<>(spannableCacheSize);
private static final ConcurrentHashMap<Integer, Spannable> sTagToSpannableCache =
new ConcurrentHashMap<>();
public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) {
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString());
}
sTagToSpannableCache.put(reactTag, sp);
}
public static void deleteCachedSpannableForTag(int reactTag) {
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Delete cached spannable for tag[" + reactTag + "]");
}
sTagToSpannableCache.remove(reactTag);
}
public static boolean isRTL(ReadableMapBuffer attributedString) {
ReadableMapBuffer fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS);
if (fragments.getCount() == 0) {
return false;
}
ReadableMapBuffer fragment = fragments.getMapBuffer((short) 0);
ReadableMapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES);
return TextAttributeProps.getLayoutDirection(
textAttributes.getString(TextAttributeProps.TA_KEY_LAYOUT_DIRECTION))
== LayoutDirection.RTL;
}
private static void buildSpannableFromFragment(
Context context,
ReadableMapBuffer fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
for (short i = 0, length = fragments.getCount(); i < length; i++) {
ReadableMapBuffer fragment = fragments.getMapBuffer(i);
int start = sb.length();
TextAttributeProps textAttributes =
TextAttributeProps.fromReadableMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES));
sb.append(
TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform));
int end = sb.length();
int reactTag =
fragment.hasKey(FR_KEY_REACT_TAG) ? fragment.getInt(FR_KEY_REACT_TAG) : View.NO_ID;
if (fragment.hasKey(FR_KEY_IS_ATTACHMENT) && fragment.getBoolean(FR_KEY_IS_ATTACHMENT)) {
float width = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_WIDTH));
float height = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_HEIGHT));
ops.add(
new SetSpanOperation(
sb.length() - INLINE_VIEW_PLACEHOLDER.length(),
sb.length(),
new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height)));
} else if (end >= start) {
if (ReactAccessibilityDelegate.AccessibilityRole.LINK.equals(
textAttributes.mAccessibilityRole)) {
ops.add(
new SetSpanOperation(
start, end, new ReactClickableSpan(reactTag, textAttributes.mColor)));
} else if (textAttributes.mIsColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactForegroundColorSpan(textAttributes.mColor)));
}
if (textAttributes.mIsBackgroundColorSet) {
ops.add(
new SetSpanOperation(
start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor)));
}
if (!Float.isNaN(textAttributes.getLetterSpacing())) {
ops.add(
new SetSpanOperation(
start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing())));
}
ops.add(
new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize)));
if (textAttributes.mFontStyle != UNSET
|| textAttributes.mFontWeight != UNSET
|| textAttributes.mFontFamily != null) {
ops.add(
new SetSpanOperation(
start,
end,
new CustomStyleSpan(
textAttributes.mFontStyle,
textAttributes.mFontWeight,
textAttributes.mFontFeatureSettings,
textAttributes.mFontFamily,
context.getAssets())));
}
if (textAttributes.mIsUnderlineTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan()));
}
if (textAttributes.mIsLineThroughTextDecorationSet) {
ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan()));
}
if (textAttributes.mTextShadowOffsetDx != 0 || textAttributes.mTextShadowOffsetDy != 0) {
ops.add(
new SetSpanOperation(
start,
end,
new ShadowStyleSpan(
textAttributes.mTextShadowOffsetDx,
textAttributes.mTextShadowOffsetDy,
textAttributes.mTextShadowRadius,
textAttributes.mTextShadowColor)));
}
if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) {
ops.add(
new SetSpanOperation(
start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight())));
}
ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag)));
}
}
}
// public because both ReactTextViewManager and ReactTextInputManager need to use this
public static Spannable getOrCreateSpannableForText(
Context context,
ReadableMapBuffer attributedString,
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
Spannable preparedSpannableText;
synchronized (sSpannableCacheLock) {
preparedSpannableText = sSpannableCache.get(attributedString);
if (preparedSpannableText != null) {
return preparedSpannableText;
}
}
preparedSpannableText =
createSpannableFromAttributedString(
context, attributedString, reactTextViewManagerCallback);
synchronized (sSpannableCacheLock) {
sSpannableCache.put(attributedString, preparedSpannableText);
}
return preparedSpannableText;
}
private static Spannable createSpannableFromAttributedString(
Context context,
ReadableMapBuffer attributedString,
@Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) {
SpannableStringBuilder sb = new SpannableStringBuilder();
// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
// up-to-bottom, otherwise all the spannables that are within the region for which one may set
// a new spannable will be wiped out
List<SetSpanOperation> ops = new ArrayList<>();
buildSpannableFromFragment(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops);
// TODO T31905686: add support for inline Images
// While setting the Spans on the final text, we also check whether any of them are images.
int priority = 0;
for (SetSpanOperation op : ops) {
// Actual order of calling {@code execute} does NOT matter,
// but the {@code priority} DOES matter.
op.execute(sb, priority);
priority++;
}
if (reactTextViewManagerCallback != null) {
reactTextViewManagerCallback.onPostProcessSpannable(sb);
}
return sb;
}
private static Layout createLayout(
Spannable text,
BoringLayout.Metrics boring,
float width,
YogaMeasureMode widthYogaMeasureMode,
boolean includeFontPadding,
int textBreakStrategy) {
Layout layout;
int spanLength = text.length();
boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0;
TextPaint textPaint = sTextPaintInstance;
float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN;
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,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
includeFontPadding);
} else {
layout =
StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.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,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
boring,
includeFontPadding);
} 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,
Layout.Alignment.ALIGN_NORMAL,
1.f,
0.f,
includeFontPadding);
} else {
StaticLayout.Builder builder =
StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(0.f, 1.f)
.setIncludePad(includeFontPadding)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(true);
}
layout = builder.build();
}
}
return layout;
}
public static long measureText(
Context context,
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float width,
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode,
ReactTextViewManagerCallback reactTextViewManagerCallback,
@Nullable float[] attachmentsPositions) {
// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
TextPaint textPaint = sTextPaintInstance;
Spannable text;
if (attributedString.hasKey(AS_KEY_CACHE_ID)) {
int cacheId = attributedString.getInt(AS_KEY_CACHE_ID);
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Get cached spannable for cacheId[" + cacheId + "]");
}
if (sTagToSpannableCache.containsKey(cacheId)) {
text = sTagToSpannableCache.get(cacheId);
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "Text for spannable found for cacheId[" + cacheId + "]: " + text.toString());
}
} else {
if (ENABLE_MEASURE_LOGGING) {
FLog.e(TAG, "No cached spannable found for cacheId[" + cacheId + "]");
}
return 0;
}
} else {
text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback);
}
int textBreakStrategy =
TextAttributeProps.getTextBreakStrategy(
paragraphAttributes.getString(PA_KEY_TEXT_BREAK_STRATEGY));
boolean includeFontPadding =
paragraphAttributes.hasKey(PA_KEY_INCLUDE_FONT_PADDING)
? paragraphAttributes.getBoolean(PA_KEY_INCLUDE_FONT_PADDING)
: DEFAULT_INCLUDE_FONT_PADDING;
if (text == null) {
throw new IllegalStateException("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 = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0;
Layout layout =
createLayout(
text, boring, width, widthYogaMeasureMode, includeFontPadding, textBreakStrategy);
int maximumNumberOfLines =
paragraphAttributes.hasKey(PA_KEY_MAX_NUMBER_OF_LINES)
? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
: UNSET;
int calculatedLineCount =
maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
? layout.getLineCount()
: Math.min(maximumNumberOfLines, layout.getLineCount());
// Instead of using `layout.getWidth()` (which may yield a significantly larger width for
// text that is wrapping), compute width using the longest line.
float calculatedWidth = 0;
if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) {
calculatedWidth = width;
} else {
for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) {
float lineWidth = layout.getLineWidth(lineIndex);
if (lineWidth > calculatedWidth) {
calculatedWidth = lineWidth;
}
}
if (widthYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedWidth > width) {
calculatedWidth = width;
}
}
float calculatedHeight = height;
if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) {
calculatedHeight = layout.getLineBottom(calculatedLineCount - 1);
if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) {
calculatedHeight = height;
}
}
// Calculate the positions of the attachments (views) that will be rendered inside the
// Spanned Text. The following logic is only executed when a text contains views inside.
// This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method).
int attachmentIndex = 0;
int lastAttachmentFoundInSpan;
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
lastAttachmentFoundInSpan =
text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class);
TextInlineViewPlaceholderSpan[] placeholders =
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
int start = text.getSpanStart(placeholder);
int line = layout.getLineForOffset(start);
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
// This truncation check works well on recent versions of Android (tested on 5.1.1 and
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the
// first thing to be truncated.
if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line))
|| start >= layout.getLineEnd(line)) {
float placeholderWidth = placeholder.getWidth();
float placeholderHeight = placeholder.getHeight();
// Calculate if the direction of the placeholder character is Right-To-Left.
boolean isRtlChar = layout.isRtlCharAt(start);
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
float placeholderLeftPosition;
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
// the last offset in the layout will result in an endless loop. Work around
// this bug by avoiding getPrimaryHorizontal in that case.
if (start == text.length() - 1) {
placeholderLeftPosition =
isRtlParagraph
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns
// incorrect
// values when the paragraph is RTL and `setSingleLine(true)`.
? calculatedWidth - layout.getLineWidth(line)
: layout.getLineRight(line) - placeholderWidth;
} else {
// The direction of the paragraph may not be exactly the direction the string is
// heading
// in at the
// position of the placeholder. So, if the direction of the character is the same
// as the
// paragraph
// use primary, secondary otherwise.
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
placeholderLeftPosition =
characterAndParagraphDirectionMatch
? layout.getPrimaryHorizontal(start)
: layout.getSecondaryHorizontal(start);
if (isRtlParagraph) {
// Adjust `placeholderLeftPosition` to work around an Android bug.
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
// `getLineRight` return incorrect values. Their return values seem to be off
// by the same number of pixels so subtracting these values cancels out the
// error.
//
// The result is equivalent to bugless versions of
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
placeholderLeftPosition =
calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition);
}
if (isRtlChar) {
placeholderLeftPosition -= placeholderWidth;
}
}
// Vertically align the inline view to the baseline of the line of text.
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
int attachmentPosition = attachmentIndex * 2;
// The attachment array returns the positions of each of the attachments as
attachmentsPositions[attachmentPosition] =
PixelUtil.toSPFromPixel(placeholderTopPosition);
attachmentsPositions[attachmentPosition + 1] =
PixelUtil.toSPFromPixel(placeholderLeftPosition);
attachmentIndex++;
}
}
}
float widthInSP = PixelUtil.toSPFromPixel(calculatedWidth);
float heightInSP = PixelUtil.toSPFromPixel(calculatedHeight);
if (ENABLE_MEASURE_LOGGING) {
FLog.e(
TAG,
"TextMeasure call ('"
+ text
+ "'): w: "
+ calculatedWidth
+ " px - h: "
+ calculatedHeight
+ " px - w : "
+ widthInSP
+ " sp - h: "
+ heightInSP
+ " sp");
}
return YogaMeasureOutput.make(widthInSP, heightInSP);
}
public static WritableArray measureLines(
@NonNull Context context,
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float width) {
TextPaint textPaint = sTextPaintInstance;
Spannable text = getOrCreateSpannableForText(context, attributedString, null);
BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
int textBreakStrategy =
TextAttributeProps.getTextBreakStrategy(
paragraphAttributes.getString(PA_KEY_TEXT_BREAK_STRATEGY));
boolean includeFontPadding =
paragraphAttributes.hasKey(PA_KEY_INCLUDE_FONT_PADDING)
? paragraphAttributes.getBoolean(PA_KEY_INCLUDE_FONT_PADDING)
: DEFAULT_INCLUDE_FONT_PADDING;
Layout layout =
createLayout(
text, boring, width, YogaMeasureMode.EXACTLY, includeFontPadding, textBreakStrategy);
return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context);
}
// TODO T31905686: This class should be private
public static class SetSpanOperation {
protected int start, end;
protected ReactSpan what;
public SetSpanOperation(int start, int end, ReactSpan what) {
this.start = start;
this.end = end;
this.what = what;
}
public void execute(Spannable sb, int priority) {
// All spans will automatically extend to the right of the text, but not the left - except
// for spans that start at the beginning of the text.
int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
if (start == 0) {
spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
}
spanFlags &= ~Spannable.SPAN_PRIORITY;
spanFlags |= (priority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY;
sb.setSpan(what, start, end, spanFlags);
}
}
}