3
3
import android .content .Context ;
4
4
import android .content .res .ColorStateList ;
5
5
import android .content .res .Resources ;
6
- import android .gesture .Gesture ;
7
6
import android .graphics .Color ;
8
7
import android .graphics .drawable .Drawable ;
9
8
import android .os .Build ;
10
9
import android .text .SpannableString ;
11
10
import android .text .Spanned ;
12
11
import android .text .style .ForegroundColorSpan ;
13
- import android .util .Log ;
14
12
import android .view .GestureDetector ;
15
13
import android .view .Menu ;
16
14
import android .view .MenuItem ;
17
15
import android .view .MotionEvent ;
18
16
import android .view .View ;
19
- import android .view .ViewGroup ;
20
- import android .widget .PopupMenu ;
17
+ import android .view .ContextMenu ;
21
18
19
+ import androidx .annotation .NonNull ;
22
20
import androidx .core .content .res .ResourcesCompat ;
23
21
24
22
import com .facebook .react .bridge .Arguments ;
25
23
import com .facebook .react .bridge .ReactContext ;
26
- import com .facebook .react .bridge .ReactContextBaseJavaModule ;
27
24
import com .facebook .react .bridge .ReadableArray ;
28
25
import com .facebook .react .bridge .ReadableMap ;
26
+ import com .facebook .react .bridge .WritableArray ;
29
27
import com .facebook .react .bridge .WritableMap ;
30
- import com .facebook .react .touch .OnInterceptTouchEventListener ;
31
28
import com .facebook .react .uimanager .events .RCTEventEmitter ;
32
29
import com .facebook .react .views .view .ReactViewGroup ;
33
30
34
- import java .util . List ;
31
+ import java .lang . reflect . Method ;
35
32
36
33
import javax .annotation .Nullable ;
37
34
38
- public class ContextMenuView extends ReactViewGroup implements PopupMenu .OnMenuItemClickListener , PopupMenu .OnDismissListener {
39
-
40
- public class Action {
41
- String title ;
42
- boolean disabled ;
43
-
44
- public Action (String title , boolean disabled ) {
45
- this .title = title ;
46
- this .disabled = disabled ;
47
- }
48
- }
49
-
50
- PopupMenu contextMenu ;
51
-
52
- GestureDetector gestureDetector ;
35
+ public class ContextMenuView extends ReactViewGroup implements View .OnCreateContextMenuListener {
36
+ @ Nullable ReadableArray actions ;
53
37
54
38
boolean cancelled = true ;
55
39
56
40
protected boolean dropdownMenuMode = false ;
57
41
58
42
protected boolean disabled = false ;
59
43
44
+ private GestureDetector gestureDetector ;
45
+
60
46
public ContextMenuView (final Context context ) {
61
47
super (context );
62
48
63
- contextMenu = new PopupMenu (getContext (), this );
64
- contextMenu .setOnMenuItemClickListener (this );
65
- contextMenu .setOnDismissListener (this );
49
+ this .setOnCreateContextMenuListener (this );
66
50
67
51
gestureDetector = new GestureDetector (context , new GestureDetector .SimpleOnGestureListener () {
68
52
@ Override
69
53
public boolean onSingleTapConfirmed (MotionEvent e ) {
70
54
if (dropdownMenuMode && !disabled ) {
71
- contextMenu .show ();
55
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
56
+ showContextMenu (e .getX (), e .getY ());
57
+ }
72
58
}
73
59
return super .onSingleTapConfirmed (e );
74
60
}
75
61
76
62
@ Override
77
63
public void onLongPress (MotionEvent e ) {
78
64
if (!dropdownMenuMode && !disabled ) {
79
- contextMenu .show ();
65
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
66
+ showContextMenu (e .getX (), e .getY ());
67
+ }
80
68
}
81
69
}
82
70
});
@@ -100,37 +88,29 @@ public boolean onTouchEvent(MotionEvent ev) {
100
88
return true ;
101
89
}
102
90
103
- public void setActions (@ Nullable ReadableArray actions ) {
104
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
105
- contextMenu .setForceShowIcon (true );
106
- }
107
- Menu menu = contextMenu .getMenu ();
108
- menu .clear ();
91
+ @ Override
92
+ public void onCreateContextMenu (ContextMenu contextMenu , View view , ContextMenu .ContextMenuInfo contextMenuInfo ) {
93
+ contextMenu .clear ();
94
+ setMenuIconDisplay (contextMenu , true );
109
95
110
96
for (int i = 0 ; i < actions .size (); i ++) {
111
97
ReadableMap action = actions .getMap (i );
112
- @ Nullable Drawable systemIcon = getResourceWithName (getContext (), action .getString ("systemIcon" ));
113
- String title = action .getString ("title" );
114
- int order = i ;
115
- menu .add (Menu .NONE , Menu .NONE , order , title );
116
- menu .getItem (i ).setEnabled (!action .hasKey ("disabled" ) || !action .getBoolean ("disabled" ));
117
-
118
- if (action .hasKey ("systemIconColor" ) && systemIcon != null ) {
119
- int color = Color .parseColor (action .getString ("systemIconColor" ));
120
- systemIcon .setTint (color );
121
- }
122
- menu .getItem (i ).setIcon (systemIcon );
123
- if (action .hasKey ("destructive" ) && action .getBoolean ("destructive" )) {
124
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
125
- menu .getItem (i ).setIconTintList (ColorStateList .valueOf (Color .RED ));
126
- }
127
- SpannableString redTitle = new SpannableString (title );
128
- redTitle .setSpan (new ForegroundColorSpan (Color .RED ), 0 , title .length (), Spanned .SPAN_EXCLUSIVE_EXCLUSIVE );
129
- menu .getItem (i ).setTitle (redTitle );
98
+ ReadableArray childActions = action .getArray ("actions" );
99
+
100
+ // If there are child actions, this action is a submenu
101
+ if (childActions != null ) {
102
+ createContextMenuSubMenu (contextMenu , action , childActions , i );
103
+ } else {
104
+ // Otherwise its a normal menu item
105
+ createContextMenuAction (contextMenu , action , i , -1 );
130
106
}
131
107
}
132
108
}
133
109
110
+ public void setActions (@ Nullable ReadableArray actions ) {
111
+ this .actions = actions ;
112
+ }
113
+
134
114
public void setDropdownMenuMode (@ Nullable boolean enabled ) {
135
115
this .dropdownMenuMode = enabled ;
136
116
}
@@ -139,25 +119,70 @@ public void setDisabled(@Nullable boolean disabled) {
139
119
this .disabled = disabled ;
140
120
}
141
121
142
- @ Override
143
- public boolean onMenuItemClick (MenuItem menuItem ) {
144
- cancelled = false ;
145
- ReactContext reactContext = (ReactContext ) getContext ();
146
- WritableMap event = Arguments .createMap ();
147
- event .putInt ("index" , menuItem .getOrder ());
148
- event .putString ("name" , menuItem .getTitle ().toString ());
149
- reactContext .getJSModule (RCTEventEmitter .class ).receiveEvent (getId (), "onPress" , event );
150
- return false ;
122
+ private void createContextMenuSubMenu (Menu menu , ReadableMap action , ReadableArray childActions , int i ) {
123
+ String title = action .getString ("title" );
124
+ Menu parentMenu = menu .addSubMenu (title );
125
+
126
+ @ Nullable Drawable systemIcon = getResourceWithName (getContext (), action .getString ("systemIcon" ));
127
+ menu .getItem (i ).setIcon (systemIcon ); // set icon to current item.
128
+
129
+ for (int j = 0 ; j < childActions .size (); j ++) {
130
+ createContextMenuAction (parentMenu , childActions .getMap (j ), j , i );
131
+ }
132
+
133
+ parentMenu .setGroupVisible (0 , true );
151
134
}
152
135
153
- @ Override
154
- public void onDismiss (PopupMenu popupMenu ) {
155
- if (cancelled ) {
156
- ReactContext reactContext = (ReactContext ) getContext ();
157
- reactContext .getJSModule (RCTEventEmitter .class ).receiveEvent (getId (), "onCancel" , null );
136
+ private void createContextMenuAction (Menu menu , ReadableMap action , int i , int parentIndex ) {
137
+ String title = action .getString ("title" );
138
+ @ Nullable Drawable systemIcon = getResourceWithName (getContext (), action .getString ("systemIcon" ));
139
+
140
+ MenuItem item = menu .add (Menu .NONE , Menu .NONE , i , title );
141
+ item .setEnabled (!action .hasKey ("disabled" ) || !action .getBoolean ("disabled" ));
142
+
143
+ if (action .hasKey ("systemIconColor" ) && systemIcon != null ) {
144
+ int color = Color .parseColor (action .getString ("systemIconColor" ));
145
+ systemIcon .setTint (color );
146
+ }
147
+ item .setIcon (systemIcon );
148
+ if (action .hasKey ("destructive" ) && action .getBoolean ("destructive" )) {
149
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
150
+ item .setIconTintList (ColorStateList .valueOf (Color .RED ));
151
+ }
152
+ SpannableString redTitle = new SpannableString (title );
153
+ redTitle .setSpan (new ForegroundColorSpan (Color .RED ), 0 , title .length (), Spanned .SPAN_EXCLUSIVE_EXCLUSIVE );
154
+ item .setTitle (redTitle );
158
155
}
159
156
160
- cancelled = true ;
157
+ // We need a different listener for nested menus and parent menus
158
+ item .setOnMenuItemClickListener (new MenuItem .OnMenuItemClickListener () {
159
+ @ Override
160
+ public boolean onMenuItemClick (@ NonNull MenuItem menuItem ) {
161
+ cancelled = false ;
162
+ ReactContext reactContext = (ReactContext ) getContext ();
163
+ WritableMap event = Arguments .createMap ();
164
+ event .putInt ("index" , i );
165
+ event .putString ("name" , title );
166
+ if (parentIndex >= 0 ) {
167
+ WritableArray indexPath = Arguments .createArray ();
168
+ indexPath .pushInt (parentIndex );
169
+ indexPath .pushInt (i );
170
+ event .putArray ("indexPath" , indexPath );
171
+ }
172
+ reactContext .getJSModule (RCTEventEmitter .class ).receiveEvent (getId (), "onPress" , event );
173
+ return false ;
174
+ }
175
+ });
176
+ }
177
+
178
+ // Call this function after menu created. Both submenu and root menu should call this function.
179
+ private void setMenuIconDisplay (Menu contextMenu , boolean display ) {
180
+ try {
181
+ Class <?> clazz = Class .forName ("com.android.internal.view.menu.MenuBuilder" );
182
+ Method m = clazz .getDeclaredMethod ("setOptionalIconsVisible" , boolean .class );
183
+ m .setAccessible (true );
184
+ m .invoke (contextMenu , display );
185
+ } catch (Exception ignored ) {}
161
186
}
162
187
163
188
private Drawable getResourceWithName (Context context , @ Nullable String systemIcon ) {
0 commit comments