mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
a15a46c78e
commit
91b3f5d48a
@@ -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 = [],
|
||||
)
|
||||
+348
@@ -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,
|
||||
],
|
||||
)
|
||||
+15
@@ -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(); });
|
||||
}
|
||||
+62
@@ -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
|
||||
+48
@@ -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));
|
||||
|
||||
|
||||
+588
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user