From 216a30adc7e2fb3e98daa9e9eec3591a2df649d7 Mon Sep 17 00:00:00 2001 From: fabriziobertoglio1987 Date: Sat, 29 Oct 2022 13:31:55 +0200 Subject: [PATCH 01/79] draft solution partially spelling correctly --- .../uimanager/ReactAccessibilityDelegate.java | 68 ++ .../views/text/ReactBaseTextShadowNode.java | 4 + .../views/text/ReactTextViewManager.java | 9 + .../react/views/text/ReactTtsSpan.java | 38 + .../text/TextLayoutManagerMapBuffer.java | 13 +- .../main/res/views/uimanager/values/ids.xml | 2 + build.gradle.kts | 2 +- .../Text/TextAdjustsDynamicLayoutExample.js | 24 +- .../js/examples/Text/TextExample.android.js | 733 +----------------- 9 files changed, 143 insertions(+), 750 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 0f5f1671c8ffd6..e30166b24d3849 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -15,9 +15,11 @@ import android.os.Message; import android.text.Layout; import android.text.Spannable; +import android.text.SpannableString; import android.text.Spanned; import android.text.style.AbsoluteSizeSpan; import android.text.style.ClickableSpan; +import android.util.Log; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; @@ -44,6 +46,7 @@ import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.util.ReactFindViewUtil; +import com.facebook.react.views.text.ReactTtsSpan; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -71,6 +74,8 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper { private final View mView; private final AccessibilityLinks mAccessibilityLinks; + private final AccessibilityLinks mAccessibilitySpans; + // private final TtsSpan.MoneyBuilder mSpanned; private Handler mHandler; @@ -216,6 +221,7 @@ public void handleMessage(Message msg) { mView.setFocusable(originalFocus); ViewCompat.setImportantForAccessibility(mView, originalImportantForAccessibility); mAccessibilityLinks = (AccessibilityLinks) mView.getTag(R.id.accessibility_links); + mAccessibilitySpans = (AccessibilityLinks) mView.getTag(R.id.accessibility_spans); } @Nullable View mAccessibilityLabelledBy; @@ -228,6 +234,8 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo final String accessibilityHint = (String) host.getTag(R.id.accessibility_hint); if (accessibilityRole != null) { setRole(info, accessibilityRole, host.getContext()); + info.setHeading(true); + info.setRoleDescription("heading"); } if (accessibilityHint != null) { @@ -579,6 +587,37 @@ protected void onPopulateNodeForVirtualView( node.setBoundsInParent(getBoundsInParent(accessibleTextSpan)); node.setRoleDescription(mView.getResources().getString(R.string.link_description)); node.setClassName(AccessibilityRole.getValue(AccessibilityRole.BUTTON)); + Log.w("TESTING::ReactAccessibilityDelegate", "mAccessibilitySpans: " + (mAccessibilitySpans)); + if (mAccessibilitySpans == null) { + return; + } + final AccessibilityLinks.AccessibleLink ttsSpan = + mAccessibilitySpans.getLinkById(virtualViewId); + Log.w("TESTING::ReactAccessibilityDelegate", "ttsSpan: " + (ttsSpan)); + if (ttsSpan == null) { + return; + } + Log.w("TESTING::ReactAccessibilityDelegate", "node.getText(): " + (node.getText())); + if (mView instanceof TextView) { + TextView textView = (TextView) mView; + Log.w("TESTING::ReactAccessibilityDelegate", "textView.getText(): " + (textView.getText())); + SpannableString spannableString = new SpannableString(textView.getText()); + spannableString.setSpan(ttsSpan.span, 0, java.lang.Math.min(6, ttsSpan.end), 0); + node.setContentDescription(spannableString); + // node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + // node.setRoleDescription(mView.getResources().getString(R.string.link_description)); + // node.setHeading(true); + // node.setRoleDescription("heading"); + // node.setClassName(AccessibilityRole.getValue(AccessibilityRole.BUTTON)); + } + // String string = "my test string"; + Log.w("TESTING::ReactAccessibilityDelegate", "ttsSpan.span: " + (ttsSpan.span)); + Log.w("TESTING::ReactAccessibilityDelegate", "ttsSpan.end: " + (ttsSpan.end)); + Log.w( + "TESTING::ReactAccessibilityDelegate", + "ttsSpan.span.getArgs(): " + (ttsSpan.span.getArgs())); + // TtsSpan ttsSpan = new TtsSpan.Builder(TtsSpan.TYPE_VERBATIM).build(); + } private Rect getBoundsInParent(AccessibilityLinks.AccessibleLink accessibleLink) { @@ -647,6 +686,34 @@ protected boolean onPerformActionForVirtualView( public static class AccessibilityLinks { private final List mLinks; + public AccessibilityLinks(ReactTtsSpan[] spans, Spannable text) { + ArrayList links = new ArrayList<>(); + for (int i = 0; i < spans.length; i++) { + ReactTtsSpan span = spans[i]; + int start = text.getSpanStart(span); + int end = text.getSpanEnd(span); + // zero length spans, and out of range spans should not be included. + if (start == end || start < 0 || end < 0 || start > text.length() || end > text.length()) { + continue; + } + + final AccessibleLink link = new AccessibleLink(); + link.span = span; + link.start = start; + link.end = end; + + // ID is the reverse of what is expected, since the ClickableSpans are returned in reverse + // order due to being added in reverse order. If we don't do this, focus will move to the + // last link first and move backwards. + // + // If this approach becomes unreliable, we should instead look at their start position and + // order them manually. + link.id = spans.length - 1 - i; + links.add(link); + } + mLinks = links; + } + public AccessibilityLinks(ClickableSpan[] spans, Spannable text) { ArrayList links = new ArrayList<>(); for (int i = 0; i < spans.length; i++) { @@ -703,6 +770,7 @@ public int size() { private static class AccessibleLink { public String description; + public ReactTtsSpan span; public int start; public int end; public int id; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index cbf2967d9755ab..38d5cc110faf00 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -15,6 +15,7 @@ import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.util.Log; import android.view.Gravity; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; @@ -101,6 +102,7 @@ private static void buildSpannedFromShadowNode( boolean supportsInlineViews, Map inlineViews, int start) { + Log.w("TESTING::ReactBaseTextShadowNode", "buildSpannedFromShadowNode"); TextAttributes textAttributes; if (parentTextAttributes != null) { @@ -483,10 +485,12 @@ public void setColor(@Nullable Integer color) { @ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color") public void setBackgroundColor(@Nullable Integer color) { + Log.w("TESTING::ReactBaseTextShadowNode", "setBackgroundColor"); // Background color needs to be handled here for virtual nodes so it can be incorporated into // the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual // nodes get mapped to native views and native views get their background colors get set via // {@link BaseViewManager}. + Log.w("TESTING::ReactBaseTextShadowNode", "isVirtual(): " + (isVirtual())); if (isVirtual()) { mIsBackgroundColorSet = (color != null); if (mIsBackgroundColorSet) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index 0c8a7f0621b9ea..3688d52030670a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -94,6 +94,15 @@ public void updateExtraData(ReactTextView view, Object extraData) { ReactClickableSpan[] clickableSpans = spannable.getSpans(0, update.getText().length(), ReactClickableSpan.class); + ReactTtsSpan[] ttsSpans = spannable.getSpans(0, update.getText().length(), ReactTtsSpan.class); + if (ttsSpans.length > 0) { + view.setTag( + R.id.accessibility_spans, + new ReactAccessibilityDelegate.AccessibilityLinks(ttsSpans, spannable)); + ReactAccessibilityDelegate.resetDelegate( + view, view.isFocusable(), view.getImportantForAccessibility()); + } + if (clickableSpans.length > 0) { view.setTag( R.id.accessibility_links, diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java new file mode 100644 index 00000000000000..0466eb48acd941 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTtsSpan.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text; + +import android.os.Parcel; +import android.os.PersistableBundle; +import android.text.style.TtsSpan; + +/* + * Wraps {@link BackgroundColorSpan} as a {@link ReactSpan}. + */ +public class ReactTtsSpan extends TtsSpan implements ReactSpan { + public ReactTtsSpan(String type, PersistableBundle args) { + super(type, args); + } + + public ReactTtsSpan(Parcel src) { + super(src); + } + + public static class Builder> { + private final String mType; + private PersistableBundle mArgs = new PersistableBundle(); + + public Builder(String type) { + mType = type; + } + + public ReactTtsSpan build() { + return new ReactTtsSpan(mType, mArgs); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index e2a8c05f2f0a9a..cb88dcfb4b3bba 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -19,6 +19,7 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.util.LayoutDirection; +import android.util.Log; import android.util.LruCache; import android.view.View; import androidx.annotation.NonNull; @@ -149,9 +150,19 @@ private static void buildSpannableFromFragment( start, end, new ReactForegroundColorSpan(textAttributes.mColor))); } if (textAttributes.mIsBackgroundColorSet) { + /* ops.add( new SetSpanOperation( start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); + */ + if (Build.VERSION.SDK_INT > 21 && textAttributes.mBackgroundColor == -65536) { + Log.w( + "TESTING::TextLayoutManagerMapBuffer", + "textAttributes.mBackgroundColor: " + (textAttributes.mBackgroundColor)); + ops.add( + new SetSpanOperation( + start, end, new ReactTtsSpan.Builder(ReactTtsSpan.TYPE_VERBATIM).build())); + } } if (!Float.isNaN(textAttributes.getLetterSpacing())) { ops.add( @@ -574,7 +585,7 @@ public static WritableArray measureLines( // TODO T31905686: This class should be private public static class SetSpanOperation { protected int start, end; - protected ReactSpan what; + public ReactSpan what; public SetSpanOperation(int start, int end, ReactSpan what) { this.start = start; diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 998cb3e222caca..9d85f972b1c8c7 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -36,6 +36,8 @@ + + diff --git a/build.gradle.kts b/build.gradle.kts index 9d7ebb8e964aa1..f70907493b49d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } val reactAndroidProperties = java.util.Properties() -File("./ReactAndroid/gradle.properties").inputStream().use { reactAndroidProperties.load(it) } +File("$rootDir/ReactAndroid/gradle.properties").inputStream().use { reactAndroidProperties.load(it) } version = if (project.hasProperty("isNightly") && diff --git a/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js b/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js index cf74027259a83f..e81ed53eb31979 100644 --- a/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js +++ b/packages/rn-tester/js/examples/Text/TextAdjustsDynamicLayoutExample.js @@ -14,27 +14,19 @@ import {useState} from 'react'; export default function TextAdjustsDynamicLayoutExample(props: {}): React.Node { const [height, setHeight] = useState(20); - return ( <> - + + My number is{' '} - This is adjusting text. + accessibilityRole="link" + accessible={true} + style={{backgroundColor: 'red'}}> + please spell this text - - - -