mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat: selectionHandleColor prop on Android (#41092)
Summary: This PR addresses the problem raised in the https://github.com/facebook/react-native/issues/41004 issue. The current logic is that `selectionColor` on iOS sets the color of the selection, handles, and cursor. On Android it looks similar, while it doesn't change the color of the handles if the API level is higher than 27. In addition, on Android there was an option to set the color of the cursor by `cursorColor` prop, but it didn't work if the `selectionCursor` was set. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Make same behavior of the `selectionColor` prop on Android as iOS [ANDROID] [ADDED] - Introduced `selectionHandleColor` as a separate prop [ANDROID] [CHANGED] - Allowing `cursorColor` and `selectionHandleColor` to override `selectionColor` on Android Pull Request resolved: https://github.com/facebook/react-native/pull/41092 Test Plan: Manual tests in rn-tester: ### `selectionColor` same as iOS, sets selection, handles and cursor color _There is a way to set only "rectangle" color by setting other props as null_  ### `selectionHandleColor`  ### `cursorColor`  Reviewed By: NickGerleman Differential Revision: D51253298 Pulled By: javache fbshipit-source-id: 290284aa38c6ba0aa6998b937258788ce6376431
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6b89dc1b97
commit
1e68e48534
+8
@@ -485,6 +485,11 @@ export type NativeProps = $ReadOnly<{|
|
||||
*/
|
||||
selectionColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* The text selection handle color.
|
||||
*/
|
||||
selectionHandleColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* The start and end of the text input's selection. Set start and end to
|
||||
* the same value to position the cursor.
|
||||
@@ -692,6 +697,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
|
||||
fontStyle: true,
|
||||
textShadowOffset: true,
|
||||
selectionColor: {process: require('../../StyleSheet/processColor').default},
|
||||
selectionHandleColor: {
|
||||
process: require('../../StyleSheet/processColor').default,
|
||||
},
|
||||
placeholderTextColor: {
|
||||
process: require('../../StyleSheet/processColor').default,
|
||||
},
|
||||
|
||||
@@ -336,6 +336,14 @@ export interface TextInputAndroidProps {
|
||||
*/
|
||||
cursorColor?: ColorValue | null | undefined;
|
||||
|
||||
/**
|
||||
* When provided it will set the color of the selection handles when highlighting text.
|
||||
* Unlike the behavior of `selectionColor` the handle color will be set independently
|
||||
* from the color of the text selection box.
|
||||
* @platform android
|
||||
*/
|
||||
selectionHandleColor?: ColorValue | null | undefined;
|
||||
|
||||
/**
|
||||
* Determines whether the individual fields in your app should be included in a
|
||||
* view structure for autofill purposes on Android API Level 26+. Defaults to auto.
|
||||
|
||||
@@ -332,6 +332,14 @@ type AndroidProps = $ReadOnly<{|
|
||||
*/
|
||||
cursorColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* When provided it will set the color of the selection handles when highlighting text.
|
||||
* Unlike the behavior of `selectionColor` the handle color will be set independently
|
||||
* from the color of the text selection box.
|
||||
* @platform android
|
||||
*/
|
||||
selectionHandleColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* When `false`, if there is a small amount of space available around a text input
|
||||
* (e.g. landscape orientation on a phone), the OS may choose to have the user edit
|
||||
|
||||
@@ -917,6 +917,12 @@ export type Props = $ReadOnly<{|
|
||||
*/
|
||||
selectionColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* The text selection handle color.
|
||||
* @platform android
|
||||
*/
|
||||
selectionHandleColor?: ?ColorValue,
|
||||
|
||||
/**
|
||||
* If `true`, all text will automatically be selected on focus.
|
||||
*/
|
||||
@@ -1111,6 +1117,9 @@ function InternalTextInput(props: Props): React.Node {
|
||||
id,
|
||||
tabIndex,
|
||||
selection: propsSelection,
|
||||
selectionColor,
|
||||
selectionHandleColor,
|
||||
cursorColor,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -1506,7 +1515,15 @@ function InternalTextInput(props: Props): React.Node {
|
||||
if (childCount > 1) {
|
||||
children = <Text>{children}</Text>;
|
||||
}
|
||||
|
||||
// For consistency with iOS set cursor/selectionHandle color as selectionColor
|
||||
const colorProps = {
|
||||
selectionColor,
|
||||
selectionHandleColor:
|
||||
selectionHandleColor === undefined
|
||||
? selectionColor
|
||||
: selectionHandleColor,
|
||||
cursorColor: cursorColor === undefined ? selectionColor : cursorColor,
|
||||
};
|
||||
textInput = (
|
||||
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up
|
||||
* exactly with the props for TextInput. This will need to get fixed */
|
||||
@@ -1520,6 +1537,7 @@ function InternalTextInput(props: Props): React.Node {
|
||||
// $FlowFixMe[incompatible-type] - Figure out imperative + forward refs.
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
{...colorProps}
|
||||
{...eventHandlers}
|
||||
accessibilityState={_accessibilityState}
|
||||
accessibilityLabelledBy={_accessibilityLabelledBy}
|
||||
|
||||
+95
-44
@@ -168,14 +168,11 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
|
||||
private static final String KEYBOARD_TYPE_URI = "url";
|
||||
private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0];
|
||||
private static final int UNSET = -1;
|
||||
private static final String[] DRAWABLE_FIELDS = {
|
||||
"mCursorDrawable", "mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"
|
||||
private static final String[] DRAWABLE_HANDLE_RESOURCES = {
|
||||
"mTextSelectHandleLeftRes", "mTextSelectHandleRightRes", "mTextSelectHandleRes"
|
||||
};
|
||||
private static final String[] DRAWABLE_RESOURCES = {
|
||||
"mCursorDrawableRes",
|
||||
"mTextSelectHandleLeftRes",
|
||||
"mTextSelectHandleRightRes",
|
||||
"mTextSelectHandleRes"
|
||||
private static final String[] DRAWABLE_HANDLE_FIELDS = {
|
||||
"mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"
|
||||
};
|
||||
|
||||
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
|
||||
@@ -524,20 +521,78 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
|
||||
} else {
|
||||
view.setHighlightColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
setCursorColor(view, color);
|
||||
@ReactProp(name = "selectionHandleColor", customType = "Color")
|
||||
public void setSelectionHandleColor(ReactEditText view, @Nullable Integer color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Drawable drawableCenter = view.getTextSelectHandle().mutate();
|
||||
Drawable drawableLeft = view.getTextSelectHandleLeft().mutate();
|
||||
Drawable drawableRight = view.getTextSelectHandleRight().mutate();
|
||||
if (color != null) {
|
||||
BlendModeColorFilter filter = new BlendModeColorFilter(color, BlendMode.SRC_IN);
|
||||
drawableCenter.setColorFilter(filter);
|
||||
drawableLeft.setColorFilter(filter);
|
||||
drawableRight.setColorFilter(filter);
|
||||
} else {
|
||||
drawableCenter.clearColorFilter();
|
||||
drawableLeft.clearColorFilter();
|
||||
drawableRight.clearColorFilter();
|
||||
}
|
||||
view.setTextSelectHandle(drawableCenter);
|
||||
view.setTextSelectHandleLeft(drawableLeft);
|
||||
view.setTextSelectHandleRight(drawableRight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Based on https://github.com/facebook/react-native/pull/31007
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The following code uses reflection to change handles color on Android 8.1 and below.
|
||||
for (int i = 0; i < DRAWABLE_HANDLE_RESOURCES.length; i++) {
|
||||
try {
|
||||
Field drawableResourceField =
|
||||
view.getClass().getDeclaredField(DRAWABLE_HANDLE_RESOURCES[i]);
|
||||
drawableResourceField.setAccessible(true);
|
||||
int resourceId = drawableResourceField.getInt(view);
|
||||
|
||||
// The view has no handle drawable.
|
||||
if (resourceId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate();
|
||||
if (color != null) {
|
||||
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
drawable.clearColorFilter();
|
||||
}
|
||||
|
||||
Field editorField = TextView.class.getDeclaredField("mEditor");
|
||||
editorField.setAccessible(true);
|
||||
Object editor = editorField.get(view);
|
||||
|
||||
Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_HANDLE_FIELDS[i]);
|
||||
cursorDrawableField.setAccessible(true);
|
||||
cursorDrawableField.set(editor, drawable);
|
||||
} catch (NoSuchFieldException ex) {
|
||||
} catch (IllegalAccessException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "cursorColor", customType = "Color")
|
||||
public void setCursorColor(ReactEditText view, @Nullable Integer color) {
|
||||
if (color == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Drawable cursorDrawable = view.getTextCursorDrawable();
|
||||
if (cursorDrawable != null) {
|
||||
cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
|
||||
if (color != null) {
|
||||
cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
|
||||
} else {
|
||||
cursorDrawable.clearColorFilter();
|
||||
}
|
||||
view.setTextCursorDrawable(cursorDrawable);
|
||||
}
|
||||
return;
|
||||
@@ -552,39 +607,35 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
|
||||
|
||||
// The evil code that follows uses reflection to achieve this on Android 8.1 and below.
|
||||
// Based on https://tinyurl.com/3vff8lyu https://tinyurl.com/vehggzs9
|
||||
for (int i = 0; i < DRAWABLE_RESOURCES.length; i++) {
|
||||
try {
|
||||
Field drawableResourceField = TextView.class.getDeclaredField(DRAWABLE_RESOURCES[i]);
|
||||
drawableResourceField.setAccessible(true);
|
||||
int resourceId = drawableResourceField.getInt(view);
|
||||
try {
|
||||
Field drawableCursorField = view.getClass().getDeclaredField("mCursorDrawableRes");
|
||||
drawableCursorField.setAccessible(true);
|
||||
int resourceId = drawableCursorField.getInt(view);
|
||||
|
||||
// The view has no cursor drawable.
|
||||
if (resourceId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId);
|
||||
|
||||
Drawable drawableCopy = drawable.mutate();
|
||||
drawableCopy.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
Field editorField = TextView.class.getDeclaredField("mEditor");
|
||||
editorField.setAccessible(true);
|
||||
Object editor = editorField.get(view);
|
||||
|
||||
Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_FIELDS[i]);
|
||||
cursorDrawableField.setAccessible(true);
|
||||
if (DRAWABLE_RESOURCES[i] == "mCursorDrawableRes") {
|
||||
Drawable[] drawables = {drawableCopy, drawableCopy};
|
||||
cursorDrawableField.set(editor, drawables);
|
||||
} else {
|
||||
cursorDrawableField.set(editor, drawableCopy);
|
||||
}
|
||||
} catch (NoSuchFieldException ex) {
|
||||
// Ignore errors to avoid crashing if these private fields don't exist on modified
|
||||
// or future android versions.
|
||||
} catch (IllegalAccessException ex) {
|
||||
// The view has no cursor drawable.
|
||||
if (resourceId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate();
|
||||
if (color != null) {
|
||||
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
drawable.clearColorFilter();
|
||||
}
|
||||
|
||||
Field editorField = TextView.class.getDeclaredField("mEditor");
|
||||
editorField.setAccessible(true);
|
||||
Object editor = editorField.get(view);
|
||||
|
||||
Field cursorDrawableField = editor.getClass().getDeclaredField("mCursorDrawable");
|
||||
cursorDrawableField.setAccessible(true);
|
||||
Drawable[] drawables = {drawable, drawable};
|
||||
cursorDrawableField.set(editor, drawables);
|
||||
} catch (NoSuchFieldException ex) {
|
||||
// Ignore errors to avoid crashing if these private fields don't exist on modified
|
||||
// or future android versions.
|
||||
} catch (IllegalAccessException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -134,6 +134,10 @@ AndroidTextInputProps::AndroidTextInputProps(
|
||||
"selectionColor",
|
||||
sourceProps.selectionColor,
|
||||
{})),
|
||||
selectionHandleColor(CoreFeatures::enablePropIteratorSetter? sourceProps.selectionHandleColor : convertRawProp(context, rawProps,
|
||||
"selectionHandleColor",
|
||||
sourceProps.selectionHandleColor,
|
||||
{})),
|
||||
value(CoreFeatures::enablePropIteratorSetter? sourceProps.value : convertRawProp(context, rawProps, "value", sourceProps.value, {})),
|
||||
defaultValue(CoreFeatures::enablePropIteratorSetter? sourceProps.defaultValue : convertRawProp(context, rawProps,
|
||||
"defaultValue",
|
||||
@@ -347,6 +351,7 @@ void AndroidTextInputProps::setProp(
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(placeholderTextColor);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(secureTextEntry);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(selectionColor);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(selectionHandleColor);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(defaultValue);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(selectTextOnFocus);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior);
|
||||
@@ -446,6 +451,7 @@ folly::dynamic AndroidTextInputProps::getDynamic() const {
|
||||
props["placeholderTextColor"] = toAndroidRepr(placeholderTextColor);
|
||||
props["secureTextEntry"] = secureTextEntry;
|
||||
props["selectionColor"] = toAndroidRepr(selectionColor);
|
||||
props["selectionHandleColor"] = toAndroidRepr(selectionHandleColor);
|
||||
props["value"] = value;
|
||||
props["defaultValue"] = defaultValue;
|
||||
props["selectTextOnFocus"] = selectTextOnFocus;
|
||||
|
||||
+1
@@ -100,6 +100,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps {
|
||||
SharedColor placeholderTextColor{};
|
||||
bool secureTextEntry{false};
|
||||
SharedColor selectionColor{};
|
||||
SharedColor selectionHandleColor{};
|
||||
std::string value{};
|
||||
std::string defaultValue{};
|
||||
bool selectTextOnFocus{false};
|
||||
|
||||
+6
@@ -62,6 +62,12 @@ TextInputProps::TextInputProps(
|
||||
"selectionColor",
|
||||
sourceProps.selectionColor,
|
||||
{})),
|
||||
selectionHandleColor(convertRawProp(
|
||||
context,
|
||||
rawProps,
|
||||
"selectionHandleColor",
|
||||
sourceProps.selectionHandleColor,
|
||||
{})),
|
||||
underlineColorAndroid(convertRawProp(
|
||||
context,
|
||||
rawProps,
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ class TextInputProps final : public ViewProps, public BaseTextProps {
|
||||
*/
|
||||
const SharedColor cursorColor{};
|
||||
const SharedColor selectionColor{};
|
||||
const SharedColor selectionHandleColor{};
|
||||
// TODO: Rename to `tintColor` and make universal.
|
||||
const SharedColor underlineColorAndroid{};
|
||||
|
||||
|
||||
@@ -196,10 +196,20 @@ const examples: Array<RNTesterModuleExample> = [
|
||||
</Text>
|
||||
</TextInput>
|
||||
<TextInput
|
||||
defaultValue="Highlight Color is red"
|
||||
defaultValue="Selection Color is red"
|
||||
selectionColor={'red'}
|
||||
style={styles.singleLine}
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue="Selection handles are red"
|
||||
selectionHandleColor={'red'}
|
||||
style={styles.singleLine}
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue="Cursor Color is red"
|
||||
cursorColor={'red'}
|
||||
style={styles.singleLine}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
@@ -470,7 +480,7 @@ const examples: Array<RNTesterModuleExample> = [
|
||||
'next',
|
||||
];
|
||||
const returnKeyLabels = ['Compile', 'React Native'];
|
||||
const examples = returnKeyTypes.map(type => {
|
||||
const returnKeyExamples = returnKeyTypes.map(type => {
|
||||
return (
|
||||
<TextInput
|
||||
key={type}
|
||||
@@ -492,7 +502,7 @@ const examples: Array<RNTesterModuleExample> = [
|
||||
});
|
||||
return (
|
||||
<View>
|
||||
{examples}
|
||||
{returnKeyExamples}
|
||||
{types}
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user