Skip to content

Commit

Permalink
Adding support for resource-id.
Browse files Browse the repository at this point in the history
* This closes facebook#9777.
  • Loading branch information
jsdevel committed Sep 21, 2016
1 parent ebecd48 commit cd03194
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import static com.facebook.react.bridge.ReactMarkerConstants.RUN_JS_BUNDLE_START;
import static com.facebook.react.bridge.ReactMarkerConstants.SETUP_REACT_CONTEXT_END;
import static com.facebook.react.bridge.ReactMarkerConstants.SETUP_REACT_CONTEXT_START;
import static com.facebook.react.uimanager.BaseViewManager.getOriginalReactTag;
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;

/**
Expand Down Expand Up @@ -811,7 +812,7 @@ private void detachViewFromInstance(
CatalystInstance catalystInstance) {
UiThreadUtil.assertOnUiThread();
catalystInstance.getJSModule(AppRegistry.class)
.unmountApplicationComponentAtRootTag(rootView.getId());
.unmountApplicationComponentAtRootTag(getOriginalReactTag(rootView));
}

private void tearDownReactContext(ReactContext reactContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import android.view.ViewGroup;
import android.view.ViewParent;

import static com.facebook.react.uimanager.BaseViewManager.getOriginalReactTag;

/**
* This class coordinates JSResponder commands for {@link UIManagerModule}. It should be set as
* OnInterceptTouchEventListener for all newly created native views that implements
Expand Down Expand Up @@ -70,7 +72,7 @@ public boolean onInterceptTouchEvent(ViewGroup v, MotionEvent event) {
// Therefore since "UP" event is the last event in a gesture, we should just let it reach the
// original target that is a child view of {@param v}.
// http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)
return v.getId() == currentJSResponder;
return getOriginalReactTag(v) == currentJSResponder;
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import android.view.ViewGroup;

import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.concurrent.ConcurrentHashMap;

/**
* Base class that should be suitable for the majority of subclasses of {@link ViewManager}.
Expand All @@ -35,6 +37,9 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
private static final String PROP_TRANSLATE_X = "translateX";
private static final String PROP_TRANSLATE_Y = "translateY";

private static final ConcurrentHashMap<String, Integer> TEST_IDS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Integer, Integer> JS_IDS = new ConcurrentHashMap<>();

/**
* Used to locate views in end-to-end (UI) tests.
*/
Expand Down Expand Up @@ -84,6 +89,14 @@ public void setRenderToHardwareTexture(T view, boolean useHWTexture) {

@ReactProp(name = PROP_TEST_ID)
public void setTestId(T view, String testId) {
if (!TEST_IDS.containsKey(testId)) {
TEST_IDS.put(testId, view.getResources().getIdentifier(testId, "id", view.getContext().getPackageName()));
}
int mappedTestId = TEST_IDS.get(testId);
if (mappedTestId != 0) {
JS_IDS.put(System.identityHashCode(view), view.getId());
view.setId(mappedTestId);
}
view.setTag(testId);
}

Expand Down Expand Up @@ -153,6 +166,28 @@ public void setAccessibilityLiveRegion(T view, String liveRegion) {
}
}

/**
* Returns the tag originally given by the javascript bridge when this view was created prior to
* it being potentially overwritten by {@link #setTestId}.
*
* @param view
* @param <T>
* @return
*/
public static <T extends View> int getOriginalReactTag(T view) {
Integer idFromJs = JS_IDS.get(System.identityHashCode(view));
return idFromJs != null ? idFromJs.intValue() : view.getId();
}

/**
* Used internally to clear the state of test ids.
*/
@VisibleForTesting
static void resetTestState() {
JS_IDS.clear();
TEST_IDS.clear();
}

private static void setTransformProperty(View view, ReadableArray transforms) {
TransformHelper.processTransform(transforms, sTransformDecompositionArray);
MatrixMathHelper.decomposeMatrix(sTransformDecompositionArray, sMatrixDecompositionContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import static com.facebook.react.uimanager.BaseViewManager.getOriginalReactTag;

/**
* Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
* native view names used in JS and corresponding instances of {@link ViewManager}. The
Expand Down Expand Up @@ -358,7 +360,7 @@ public void manageChildren(

if (mLayoutAnimationEnabled &&
mLayoutAnimator.shouldAnimateLayout(viewToRemove) &&
arrayContains(tagsToDelete, viewToRemove.getId())) {
arrayContains(tagsToDelete, getOriginalReactTag(viewToRemove))) {
// The view will be removed and dropped by the 'delete' layout animation
// instead, so do nothing
} else {
Expand Down Expand Up @@ -512,24 +514,25 @@ protected final void addRootViewGroup(
*/
protected void dropView(View view) {
UiThreadUtil.assertOnUiThread();
if (!mRootTags.get(view.getId())) {
int originalReactTag = getOriginalReactTag(view);
if (!mRootTags.get(originalReactTag)) {
// For non-root views we notify viewmanager with {@link ViewManager#onDropInstance}
resolveViewManager(view.getId()).onDropViewInstance(view);
resolveViewManager(originalReactTag).onDropViewInstance(view);
}
ViewManager viewManager = mTagsToViewManagers.get(view.getId());
ViewManager viewManager = mTagsToViewManagers.get(originalReactTag);
if (view instanceof ViewGroup && viewManager instanceof ViewGroupManager) {
ViewGroup viewGroup = (ViewGroup) view;
ViewGroupManager viewGroupManager = (ViewGroupManager) viewManager;
for (int i = viewGroupManager.getChildCount(viewGroup) - 1; i >= 0; i--) {
View child = viewGroupManager.getChildAt(viewGroup, i);
if (mTagsToViews.get(child.getId()) != null) {
if (mTagsToViews.get(getOriginalReactTag(child)) != null) {
dropView(child);
}
}
viewGroupManager.removeAllViews(viewGroup);
}
mTagsToViews.remove(view.getId());
mTagsToViewManagers.remove(view.getId());
mTagsToViews.remove(originalReactTag);
mTagsToViewManagers.remove(originalReactTag);
}

public void removeRootView(int rootViewTag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.ReactHitSlopView;

import static com.facebook.react.uimanager.BaseViewManager.getOriginalReactTag;

/**
* Class responsible for identifying which react view should handle a given {@link MotionEvent}.
* It uses the event coordinates to traverse the view hierarchy and return a suitable view.
Expand Down Expand Up @@ -87,7 +89,7 @@ public static int findTargetTagAndCoordinatesForTouch(
float[] viewCoords,
@Nullable int[] nativeViewTag) {
UiThreadUtil.assertOnUiThread();
int targetTag = viewGroup.getId();
int targetTag = getOriginalReactTag(viewGroup);
// Store eventCoords in array so that they are modified to be relative to the targetView found.
viewCoords[0] = eventX;
viewCoords[1] = eventY;
Expand Down Expand Up @@ -224,7 +226,7 @@ private static boolean isTransformedTouchPointInView(
// ViewGroup).
if (view instanceof ReactCompoundView) {
int reactTag = ((ReactCompoundView)view).reactTagForTouch(eventCoords[0], eventCoords[1]);
if (reactTag != view.getId()) {
if (reactTag != getOriginalReactTag(view)) {
// make sure we exclude the View itself because of the PointerEvents.BOX_NONE
return view;
}
Expand Down Expand Up @@ -256,7 +258,7 @@ private static int getTouchTargetForView(View targetView, float eventX, float ev
// {@link #findTouchTargetView()}.
return ((ReactCompoundView) targetView).reactTagForTouch(eventX, eventY);
}
return targetView.getId();
return getOriginalReactTag(targetView);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ protected void init(int viewTag) {
}

/**
* TODO: Determine if this can be affected by testID.
*
* @return the view id for the view that generated this event
*/
public final int getViewTag() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;

import static com.facebook.react.uimanager.BaseViewManager.getOriginalReactTag;

public class ReactTextView extends TextView implements ReactCompoundView {

private static final ViewGroup.LayoutParams EMPTY_LAYOUT_PARAMS =
Expand Down Expand Up @@ -74,7 +76,7 @@ public void setText(ReactTextUpdate update) {
@Override
public int reactTagForTouch(float touchX, float touchY) {
Spanned text = (Spanned) getText();
int target = getId();
int target = getOriginalReactTag(this);

int x = (int) touchX;
int y = (int) touchY;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

package com.facebook.react.uimanager;

import android.content.Context;
import android.content.res.Resources;
import android.view.View;

import com.facebook.csslayout.Spacing;

import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import static org.junit.Assert.assertEquals;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

@Config(manifest= Config.NONE)
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class BaseViewManagerTest {

@Mock
Resources resources;
@Mock
Context context;
@Mock
private View view;
private BaseViewManager<View, LayoutShadowNode> sut;

private final String testID = "some-test-id";
private final int mappedTestID = 23457897;
private final int originalJsTag = 5;
private final String myPackage = "com.myApp";


@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(view.getContext()).thenReturn(context);
when(view.getResources()).thenReturn(resources);
when(view.getId()).thenReturn(originalJsTag);
when(resources.getIdentifier(eq(testID), eq("id"), eq(myPackage))).thenReturn(mappedTestID);
when(resources.getIdentifier(eq(testID), eq("id"), not(eq(myPackage)))).thenReturn(0);
sut = new ViewManagerStub();
}

@After
public void teardown() {
BaseViewManager.resetTestState();
}

@Test
public void testSetTestId_should_always_call_setTag() {
String expectedTestID1 = "asdfasdf1";
String expectedTestID2 = "asdfasdf2";
when(context.getPackageName()).thenReturn("com.foo");
sut.setTestId(view, expectedTestID1);
sut.setTestId(view, expectedTestID2);
verify(view).setTag(expectedTestID1);
verify(view).setTag(expectedTestID2);
}

@Test
public void testSetTestId_should_not_set_the_mapped_testID_on_the_view_when_a_resource_id_is_not_found() {
when(context.getPackageName()).thenReturn("com.foo");
sut.setTestId(view, testID);
verify(view, never()).setId(anyInt());
}

@Test
public void testSetTestId_should_set_the_mapped_testID_on_the_view_when_a_resource_id_is_found() {
when(context.getPackageName()).thenReturn(myPackage);
sut.setTestId(view, testID);
verify(view).setId(mappedTestID);
}

@Test
public void getOriginalReactTag_should_return_the_original_tag_set_by_js_if_no_testID_has_been_set() {
assertEquals("The original JS tag was not returned", BaseViewManager.getOriginalReactTag(view), originalJsTag);
}

@Test
public void getOriginalReactTag_should_return_the_original_tag_set_by_js_if_testID_has_been_set() {
when(context.getPackageName()).thenReturn(myPackage);
sut.setTestId(view, testID);
verify(view).setId(mappedTestID);
assertEquals("The original JS tag was not returned", BaseViewManager.getOriginalReactTag(view), originalJsTag);
}

private static class ViewManagerStub extends BaseViewManager<View, LayoutShadowNode> {
@Override
public String getName() {
return null;
}

@Override
public LayoutShadowNode createShadowNodeInstance() {
return null;
}

@Override
public Class<? extends LayoutShadowNode> getShadowNodeClass() {
return null;
}

@Override
protected View createViewInstance(ThemedReactContext reactContext) {
return null;
}

@Override
public void updateExtraData(View root, Object extraData) {

}
}
}

0 comments on commit cd03194

Please sign in to comment.