Skip to content

Commit 5e5ef52

Browse files
fix(android): spinner background issues (#290)
fixes #266 fixes #235 fixes #112 In order to fix issues with buggy color/style changes to spinner's background I created custom background resource which should look identical to default one It allowed to finally have proper color/background change possibilities. With default background, if one had applied custom backgroundColor in JS, it overriden whole background for spinner, which resulted in removing dropdown icon completely. With new custom background resource, we can apply color changes directly to icon or spinner background. As a side-effect, I introduced possibility to change icon ripple color also, it is exposed as dropdownIconRippleColor prop **NOTE**: minSdkVersion was increased to 21 (now it matches with react-native core) Co-authored-by: Jesse Katsumata <[email protected]>
1 parent f389c6e commit 5e5ef52

File tree

9 files changed

+154
-45
lines changed

9 files changed

+154
-45
lines changed

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,21 @@ On Android, specifies how to display the selection items when the user taps on t
256256

257257
### `dropdownIconColor`
258258

259-
On Android, specifies color of dropdown triangle. Input value should be hexadecimal string.
259+
On Android, specifies color of dropdown triangle. Input value should be value that is accepted by react-native `processColor` function.
260260

261-
| Type | Required | Platform |
262-
| ------ | -------- | -------- |
263-
| string | No | Android |
261+
| Type | Required | Platform |
262+
| ---------- | -------- | -------- |
263+
| ColorValue | No | Android |
264+
265+
---
266+
267+
### `dropdownIconRippleColor`
268+
269+
On Android, specifies ripple color of dropdown triangle. Input value should be value that is accepted by react-native `processColor` function.
270+
271+
| Type | Required | Platform |
272+
| ---------- | -------- | -------- |
273+
| ColorValue | No | Android |
264274

265275
---
266276

@@ -338,9 +348,9 @@ Actual value on the Picker Item
338348

339349
Displayed color on the Picker Item
340350

341-
| Type | Required |
342-
| ------- | -------- |
343-
| string | no |
351+
| Type | Required |
352+
| ----------- | -------- |
353+
| ColorValue | no |
344354

345355

346356
### `fontFamily`

android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
ReactNativePicker_compileSdkVersion=28
22
ReactNativePicker_buildToolsVersion=28.0.3
33
ReactNativePicker_targetSdkVersion=28
4-
ReactNativePicker_minSdkVersion=16
4+
ReactNativePicker_minSdkVersion=21

android/src/main/java/com/reactnativecommunity/picker/ReactPicker.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99

1010
import android.content.Context;
1111
import android.content.ContextWrapper;
12+
import android.content.res.ColorStateList;
1213
import android.content.res.Resources;
13-
import android.os.Build;
14+
import android.graphics.Color;
15+
import android.graphics.PorterDuff;
16+
import android.graphics.drawable.GradientDrawable;
17+
import android.graphics.drawable.LayerDrawable;
18+
import android.graphics.drawable.RippleDrawable;
1419
import android.util.AttributeSet;
1520
import android.util.TypedValue;
1621
import android.view.View;
@@ -66,40 +71,52 @@ public interface OnFocusListener {
6671
public ReactPicker(Context context) {
6772
super(context);
6873
handleRTL(context);
74+
setSpinnerBackground();
6975
}
7076

7177
public ReactPicker(Context context, int mode) {
7278
super(context, mode);
7379
mMode = mode;
7480
handleRTL(context);
81+
setSpinnerBackground();
7582
}
7683

7784
public ReactPicker(Context context, AttributeSet attrs) {
7885
super(context, attrs);
7986
handleRTL(context);
87+
setSpinnerBackground();
8088
}
8189

8290
public ReactPicker(Context context, AttributeSet attrs, int defStyle) {
8391
super(context, attrs, defStyle);
8492
handleRTL(context);
93+
setSpinnerBackground();
8594
}
8695

8796
public ReactPicker(Context context, AttributeSet attrs, int defStyle, int mode) {
8897
super(context, attrs, defStyle, mode);
8998
mMode = mode;
9099
handleRTL(context);
100+
setSpinnerBackground();
101+
}
102+
103+
private void setSpinnerBackground() {
104+
this.setBackgroundResource(R.drawable.spinner_dropdown_background);
105+
// If there are multiple spinners rendered, for some reason, next spinners are inheriting
106+
// background color of previous spinners if there is no color provided as a style
107+
// To prevent, let's set color manually in constructor, if any value will be provided as a style,
108+
// it will override value that is set here.
109+
this.setBackgroundColor(Color.TRANSPARENT);
91110
}
92111

93112
private void handleRTL(Context context) {
94-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
95-
boolean isRTL = I18nUtil.getInstance().isRTL(context);
96-
if (isRTL) {
97-
this.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
98-
this.setTextDirection(View.TEXT_DIRECTION_RTL);
99-
} else {
100-
this.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
101-
this.setTextDirection(View.TEXT_DIRECTION_LTR);
102-
}
113+
boolean isRTL = I18nUtil.getInstance().isRTL(context);
114+
if (isRTL) {
115+
this.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
116+
this.setTextDirection(View.TEXT_DIRECTION_RTL);
117+
} else {
118+
this.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
119+
this.setTextDirection(View.TEXT_DIRECTION_LTR);
103120
}
104121
}
105122

@@ -240,6 +257,24 @@ public void setPrimaryColor(@Nullable Integer primaryColor) {
240257
mPrimaryColor = primaryColor;
241258
}
242259

260+
public void setDropdownIconColor(@Nullable int color) {
261+
LayerDrawable drawable = (LayerDrawable) this.getBackground();
262+
RippleDrawable backgroundDrawable = (RippleDrawable) drawable.findDrawableByLayerId(R.id.dropdown_icon);
263+
backgroundDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
264+
}
265+
266+
public void setDropdownIconRippleColor(@Nullable int color) {
267+
LayerDrawable drawable = (LayerDrawable) this.getBackground();
268+
RippleDrawable backgroundDrawable = (RippleDrawable) drawable.findDrawableByLayerId(R.id.dropdown_icon);
269+
backgroundDrawable.setColor(ColorStateList.valueOf(color));
270+
}
271+
272+
public void setBackgroundColor(@Nullable int color) {
273+
LayerDrawable drawable = (LayerDrawable) this.getBackground();
274+
GradientDrawable backgroundDrawable = (GradientDrawable) drawable.findDrawableByLayerId(R.id.dropdown_background);
275+
backgroundDrawable.setColor(color);
276+
}
277+
243278
@VisibleForTesting
244279
public int getMode() {
245280
return mMode;

android/src/main/java/com/reactnativecommunity/picker/ReactPickerManager.java

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
package com.reactnativecommunity.picker;
99

1010
import android.content.Context;
11-
import android.graphics.PorterDuff;
11+
import android.graphics.Color;
1212
import android.graphics.Typeface;
13-
import android.os.Build;
1413
import android.view.LayoutInflater;
1514
import android.view.View;
1615
import android.view.ViewGroup;
@@ -111,9 +110,20 @@ public void setSelected(ReactPicker view, int selected) {
111110
view.setStagedSelection(selected);
112111
}
113112

113+
@ReactProp(name = ViewProps.BACKGROUND_COLOR)
114+
@Override
115+
public void setBackgroundColor(ReactPicker view, @Nullable int color) {
116+
view.setBackgroundColor(color);
117+
}
118+
114119
@ReactProp(name = "dropdownIconColor")
115120
public void setDropdownIconColor(ReactPicker view, @Nullable int color) {
116-
view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
121+
view.setDropdownIconColor(color);
122+
}
123+
124+
@ReactProp(name = "dropdownIconRippleColor")
125+
public void setDropdownIconRippleColor(ReactPicker view, @Nullable int color) {
126+
view.setDropdownIconRippleColor(color);
117127
}
118128

119129
@ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = 1)
@@ -261,6 +271,8 @@ private View getView(int position, View convertView, ViewGroup parent, boolean i
261271
if (style != null) {
262272
if (style.hasKey("backgroundColor") && !style.isNull("backgroundColor")) {
263273
convertView.setBackgroundColor(style.getInt("backgroundColor"));
274+
} else {
275+
convertView.setBackgroundColor(Color.TRANSPARENT);
264276
}
265277

266278
if (style.hasKey("color") && !style.isNull("color")) {
@@ -285,19 +297,16 @@ private View getView(int position, View convertView, ViewGroup parent, boolean i
285297

286298
if (item.hasKey("fontFamily") && !item.isNull("fontFamily")) {
287299
Typeface face = Typeface.create(item.getString("fontFamily"), Typeface.NORMAL);
288-
// Typeface face = Typeface.create("MuseoSans-500", Typeface.NORMAL);
289300
textView.setTypeface(face);
290301
}
291302

292-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
293-
boolean isRTL = I18nUtil.getInstance().isRTL(convertView.getContext());
294-
if (isRTL) {
295-
convertView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
296-
convertView.setTextDirection(View.TEXT_DIRECTION_RTL);
297-
} else {
298-
convertView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
299-
convertView.setTextDirection(View.TEXT_DIRECTION_LTR);
300-
}
303+
boolean isRTL = I18nUtil.getInstance().isRTL(convertView.getContext());
304+
if (isRTL) {
305+
convertView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
306+
convertView.setTextDirection(View.TEXT_DIRECTION_RTL);
307+
} else {
308+
convertView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
309+
convertView.setTextDirection(View.TEXT_DIRECTION_LTR);
301310
}
302311

303312
return convertView;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="48dp"
3+
android:height="48dp"
4+
android:viewportWidth="48"
5+
android:viewportHeight="48"
6+
android:tint="?attr/colorControlNormal">
7+
<path
8+
android:fillColor="@android:color/black"
9+
android:pathData="M19,22l5,5 5,-5z"/>
10+
</vector>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
3+
<item android:id="@+id/dropdown_background">
4+
<shape android:shape="rectangle">
5+
<solid android:color="@android:color/transparent" />
6+
</shape>
7+
</item>
8+
<item
9+
android:id="@+id/dropdown_icon"
10+
android:gravity="end|center_vertical"
11+
android:color="@android:color/black"
12+
android:tint="@android:color/black">
13+
<ripple
14+
android:color="@android:color/darker_gray"
15+
android:radius="18dp">
16+
<item android:id="@android:id/mask">
17+
<shape android:shape="rectangle">
18+
<solid android:color="@android:color/white" />
19+
<corners android:radius="18dp" />
20+
</shape>
21+
</item>
22+
<item>
23+
<selector>
24+
<item android:drawable="@drawable/ic_dropdown" />
25+
</selector>
26+
</item>
27+
</ripple>
28+
</item>
29+
</layer-list>

example/src/PickerExample.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function PromptMultilinePickerExample() {
142142
function CustomDropdownArrowColorPickerExample() {
143143
return (
144144
<View>
145-
<Picker dropdownIconColor="red">
145+
<Picker dropdownIconColor="red" dropdownIconRippleColor="purple">
146146
<Item label="hello" value="key0" />
147147
<Item label="world" value="key1" />
148148
</Picker>
@@ -192,13 +192,23 @@ function ColorPickerExample() {
192192
}}
193193
onValueChange={(v) => setValue(v)}
194194
mode="dropdown">
195-
<Item label="red" color="red" value="red" />
196-
<Item label="green" color="green" value="green" />
197-
<Item label="blue" color="blue" value="blue" />
195+
<Item label="red" color="red" style={{color: 'red'}} value="red" />
196+
<Item
197+
label="green"
198+
color="green"
199+
style={{color: 'green', backgroundColor: 'red'}}
200+
value="green"
201+
/>
202+
<Item
203+
label="blue"
204+
color="blue"
205+
style={{color: 'blue', backgroundColor: 'green'}}
206+
value="blue"
207+
/>
198208
</Picker>
199209
<Text>{`Is input opened: ${isSecondFocused ? 'YES' : 'NO'}`}</Text>
200210
<Picker
201-
style={{color: value}}
211+
style={{color: value, backgroundColor: 'gray'}}
202212
selectedValue={value}
203213
onBlur={() => {
204214
setIsSecondFocused(false);

js/PickerAndroid.android.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ function PickerAndroid(props: PickerAndroidProps, ref: PickerRef): React.Node {
164164
prompt: props.prompt,
165165
selected,
166166
style: props.style,
167-
backgroundColor: props.backgroundColor,
168167
dropdownIconColor: processColor(props.dropdownIconColor),
168+
dropdownIconRippleColor: processColor(props.dropdownIconRippleColor),
169169
testID: props.testID,
170170
numberOfLines: props.numberOfLines,
171171
};

typings/Picker.d.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
2-
import {
2+
import type {
33
TextStyle,
44
StyleProp,
55
ViewProps,
66
NativeSyntheticEvent,
77
TargetedEvent,
88
} from 'react-native';
9+
import { processColor } from 'react-native';
910

1011
export type ItemValue = number | string;
1112

@@ -75,13 +76,18 @@ export interface PickerProps<T = ItemValue> extends ViewProps {
7576
/**
7677
* Used to locate this view in end-to-end tests.
7778
*/
78-
testID?: string;
79-
/**
80-
* Color of arrow for spinner dropdown in hexadecimal format
81-
* @platform android
82-
*/
83-
dropdownIconColor?: string;
84-
/**
79+
testID?: string;
80+
/**
81+
* Color of arrow for spinner dropdown in hexadecimal format
82+
* @platform android
83+
*/
84+
dropdownIconColor?: ReturnType<typeof processColor>;
85+
/**
86+
* Ripple color of spinner's arrow
87+
* @platform android
88+
*/
89+
dropdownIconRippleColor?: ReturnType<typeof processColor>;
90+
/**
8591
* On Android, used to truncate the text with an ellipsis after computing the text layout, including line wrapping,
8692
* such that the total number of lines does not exceed this number. Default is '1'
8793
* @platform android

0 commit comments

Comments
 (0)