Skip to content

Commit

Permalink
introduce focus on view for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
kelset committed Apr 6, 2022
1 parent a196e22 commit 2dafa0e
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 3 deletions.
6 changes: 6 additions & 0 deletions Libraries/NativeComponent/PlatformBaseViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName =
bubbled: 'onPointerUp',
},
},
topOnFocusChange: {
phasedRegistrationNames: {
bubbled: 'onFocusChange',
captured: 'onFocusChangeCapture',
},
},
},
validAttributes: {
// @ReactProps from BaseViewManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

package com.facebook.react.views.view;

import android.view.FocusFinder;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
Expand Down Expand Up @@ -71,10 +74,44 @@ public void removeViewAt(T parent, int index) {
}
parent.removeViewWithSubviewClippingEnabled(child);
} else {
// Prevent focus leaks due to removal of a focused View
if (parent.getChildAt(index).hasFocus()) {
giveFocusToAppropriateView(parent, parent.getChildAt(index));
}
parent.removeViewAt(index);
}
}

private void giveFocusToAppropriateView(@NonNull ViewGroup parent, @NonNull View focusedView) {
// Search for appropriate sibling
View viewToTakeFocus = null;
while (parent != null) {
// Search DOWN
viewToTakeFocus = FocusFinder.getInstance().findNextFocus(parent, focusedView, View.FOCUS_DOWN);
if (viewToTakeFocus == null) {
// Search RIGHT
viewToTakeFocus = FocusFinder.getInstance().findNextFocus(parent, focusedView, View.FOCUS_RIGHT);
if (viewToTakeFocus == null) {
// Search UP
viewToTakeFocus = FocusFinder.getInstance().findNextFocus(parent, focusedView, View.FOCUS_UP);
if (viewToTakeFocus == null) {
// Search LEFT
viewToTakeFocus = FocusFinder.getInstance().findNextFocus(parent, focusedView, View.FOCUS_LEFT);
}
}
}
if (viewToTakeFocus != null || !(parent.getParent() instanceof ViewGroup)) {
break;
}
parent = (ViewGroup) parent.getParent();
}

// Give focus to View
if (viewToTakeFocus != null) {
viewToTakeFocus.requestFocus();
}
}

@Override
public void removeAllViews(T parent) {
UiThreadUtil.assertOnUiThread();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* 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.view;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;

/**
* Event emitted by native View when it receives focus.
*/
/* package */ class ReactViewFocusEvent extends Event<ReactViewFocusEvent> {

private static final String EVENT_NAME = "topOnFocusChange";
private boolean mHasFocus;

public ReactViewFocusEvent(int viewId, boolean hasFocus) {
super(viewId);
mHasFocus = hasFocus;
}

@Override
public String getEventName() {
return EVENT_NAME;
}

@Override
public boolean canCoalesce() {
return false;
}

@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
}

private WritableMap serializeEventData() {
WritableMap eventData = Arguments.createMap();
eventData.putInt("target", getViewTag());
eventData.putBoolean("hasFocus", mHasFocus);
return eventData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.facebook.react.uimanager.Spacing;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
Expand All @@ -48,8 +49,13 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
Spacing.START,
Spacing.END,
};
private static final int CMD_HOTSPOT_UPDATE = 1;
private static final int CMD_SET_PRESSED = 2;
// Focus or blur call on native components (through NativeMethodsMixin) redirects to TextInputState.js
// which dispatches focusTextInput or blurTextInput commands. These commands are mapped to FOCUS_TEXT_INPUT=1
// and BLUR_TEXT_INPUT=2 in ReactTextInputManager, hence these constants value should be in sync with ReactTextInputManager.
private static final int FOCUS_TEXT_INPUT = 1;
private static final int BLUR_TEXT_INPUT = 2;
private static final int CMD_HOTSPOT_UPDATE = 3;
private static final int CMD_SET_PRESSED = 4;
private static final String HOTSPOT_UPDATE_KEY = "hotspotUpdate";

@ReactProp(name = "accessible")
Expand Down Expand Up @@ -120,6 +126,36 @@ public void setBorderRadius(ReactViewGroup view, int index, float borderRadius)
}
}

@Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put(
"topOnFocusChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onFocusChange","captured", "onFocusChangeCapture")))
.build();
}

@Override
protected void addEventEmitters(
final ThemedReactContext reactContext,
final ReactViewGroup reactViewGroup) {
reactViewGroup.setOnFocusChangeListener(
new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
EventDispatcher eventDispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(
new ReactViewFocusEvent(reactViewGroup.getId(), hasFocus));
}
}
);
}


@ReactProp(name = "borderStyle")
public void setBorderStyle(ReactViewGroup view, @Nullable String borderStyle) {
view.setBorderStyle(borderStyle);
Expand Down Expand Up @@ -296,7 +332,7 @@ public ReactViewGroup createViewInstance(ThemedReactContext context) {

@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED);
return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT, HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED);
}

@Override
Expand All @@ -312,6 +348,16 @@ public void receiveCommand(ReactViewGroup root, int commandId, @Nullable Readabl
handleSetPressed(root, args);
break;
}
case FOCUS_TEXT_INPUT:
{
root.requestFocus();
break;
}
case BLUR_TEXT_INPUT:
{
root.clearFocus();
break;
}
}
}

Expand All @@ -328,6 +374,16 @@ public void receiveCommand(ReactViewGroup root, String commandId, @Nullable Read
handleSetPressed(root, args);
break;
}
case "focusTextInput":
{
root.requestFocus();
break;
}
case "blurTextInput":
{
root.clearFocus();
break;
}
}
}

Expand Down

0 comments on commit 2dafa0e

Please sign in to comment.