Skip to content

Commit 4f770c9

Browse files
authored
fix: android view invalidation mechanism (#505)
Fixes issues with view invalidation by reverting the old mechanism that was replaced in earlier commit. Makes sure fragment is added to the hierarchy after view has proper size to remove possible flickering on view initialization.
1 parent 1f17d25 commit 4f770c9

File tree

1 file changed

+94
-35
lines changed

1 file changed

+94
-35
lines changed

android/src/main/java/com/google/android/react/navsdk/NavViewManager.java

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import static com.google.android.react.navsdk.Command.*;
1717

18+
import android.view.Choreographer;
1819
import android.view.View;
1920
import android.view.ViewGroup;
2021
import android.widget.FrameLayout;
@@ -43,10 +44,10 @@
4344
public class NavViewManager extends SimpleViewManager<FrameLayout> {
4445

4546
public static final String REACT_CLASS = "NavViewManager";
46-
4747
private static NavViewManager instance;
4848

4949
private final HashMap<Integer, WeakReference<IMapViewFragment>> fragmentMap = new HashMap<>();
50+
private final HashMap<Integer, Choreographer.FrameCallback> frameCallbackMap = new HashMap<>();
5051

5152
// Cache the latest options per view so deferred fragment creation uses fresh values.
5253
private final HashMap<Integer, ReadableMap> mapOptionsCache = new HashMap<>();
@@ -118,13 +119,6 @@ public void setReactContext(ReactApplicationContext reactContext) {
118119
@Override
119120
public FrameLayout createViewInstance(@NonNull ThemedReactContext reactContext) {
120121
FrameLayout frameLayout = new FrameLayout(reactContext);
121-
122-
// Add layout change listener to ensure proper layout of fragment
123-
frameLayout.addOnLayoutChangeListener(
124-
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
125-
layoutFragmentInView(frameLayout);
126-
});
127-
128122
return frameLayout;
129123
}
130124

@@ -133,20 +127,57 @@ public FrameLayout createViewInstance(@NonNull ThemedReactContext reactContext)
133127
* is necessary because React Native's layout system doesn't automatically propagate layout to
134128
* native fragments.
135129
*/
136-
private void layoutFragmentInView(FrameLayout frameLayout) {
137-
IMapViewFragment fragment = getFragmentForRoot(frameLayout);
138-
if (fragment != null && fragment.isAdded()) {
139-
View fragmentView = fragment.getView();
140-
if (fragmentView != null) {
141-
int width = frameLayout.getMeasuredWidth();
142-
int height = frameLayout.getMeasuredHeight();
143-
144-
fragmentView.measure(
145-
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
146-
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
147-
fragmentView.layout(0, 0, width, height);
148-
}
130+
private void layoutFragmentInView(FrameLayout frameLayout, @Nullable IMapViewFragment fragment) {
131+
if (fragment == null || !fragment.isAdded()) {
132+
return;
149133
}
134+
135+
int width = frameLayout.getWidth();
136+
int height = frameLayout.getHeight();
137+
if (width == 0 || height == 0) {
138+
return;
139+
}
140+
141+
View fragmentView = fragment.getView();
142+
if (fragmentView == null) {
143+
return;
144+
}
145+
146+
fragmentView.measure(
147+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
148+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
149+
fragmentView.layout(0, 0, width, height);
150+
}
151+
152+
/**
153+
* Starts a per-frame layout loop similar to the older implementation to ensure the fragment view
154+
* stays in sync with its container size.
155+
*/
156+
private void startLayoutLoop(FrameLayout view) {
157+
int viewId = view.getId();
158+
159+
// Remove existing callback if present
160+
Choreographer.FrameCallback existing = frameCallbackMap.get(viewId);
161+
if (existing != null) {
162+
Choreographer.getInstance().removeFrameCallback(existing);
163+
}
164+
165+
Choreographer.FrameCallback frameCallback =
166+
new Choreographer.FrameCallback() {
167+
@Override
168+
public void doFrame(long frameTimeNanos) {
169+
IMapViewFragment fragment = getFragmentForViewId(viewId);
170+
if (fragment != null) {
171+
layoutFragmentInView(view, fragment);
172+
Choreographer.getInstance().postFrameCallback(this);
173+
} else {
174+
frameCallbackMap.remove(viewId);
175+
}
176+
}
177+
};
178+
179+
frameCallbackMap.put(viewId, frameCallback);
180+
Choreographer.getInstance().postFrameCallback(frameCallback);
150181
}
151182

152183
/** Clean up fragment when React Native view is destroyed */
@@ -156,6 +187,11 @@ public void onDropViewInstance(@NonNull FrameLayout view) {
156187

157188
int viewId = view.getId();
158189

190+
Choreographer.FrameCallback frameCallback = frameCallbackMap.remove(viewId);
191+
if (frameCallback != null) {
192+
Choreographer.getInstance().removeFrameCallback(frameCallback);
193+
}
194+
159195
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
160196
if (activity == null) return;
161197

@@ -278,7 +314,9 @@ public void onNavigationReady() {
278314
for (WeakReference<IMapViewFragment> weakReference : fragmentMap.values()) {
279315
IMapViewFragment fragment = weakReference.get();
280316
if (fragment instanceof INavViewFragment) {
281-
((INavViewFragment) fragment).applyStylingOptions();
317+
INavViewFragment navFragment = (INavViewFragment) fragment;
318+
navFragment.setNavigationUiEnabled(true);
319+
navFragment.applyStylingOptions();
282320
}
283321
}
284322
}
@@ -604,16 +642,29 @@ private void scheduleFragmentTransaction(
604642
@NonNull FrameLayout root, @NonNull ReadableMap mapOptions) {
605643

606644
// Commit the fragment transaction after view is added to the view hierarchy.
607-
root.post(
608-
() -> {
609-
int viewId = root.getId();
610-
if (isFragmentCreated(viewId)) {
611-
return;
612-
}
613-
ReadableMap latestOptions = mapOptionsCache.get(viewId);
614-
ReadableMap optionsToUse = latestOptions != null ? latestOptions : mapOptions;
615-
commitFragmentTransaction(root, optionsToUse);
616-
});
645+
root.post(() -> tryCommitFragmentTransaction(root, mapOptions));
646+
}
647+
648+
/** Attempt to create/attach the fragment once the parent view has a real size. */
649+
private void tryCommitFragmentTransaction(
650+
@NonNull FrameLayout root, @NonNull ReadableMap initialMapOptions) {
651+
int viewId = root.getId();
652+
if (isFragmentCreated(viewId)) {
653+
return;
654+
}
655+
656+
ReadableMap latestOptions = mapOptionsCache.get(viewId);
657+
ReadableMap optionsToUse = latestOptions != null ? latestOptions : initialMapOptions;
658+
659+
int width = root.getWidth();
660+
int height = root.getHeight();
661+
if (width == 0 || height == 0) {
662+
// Wait for layout to provide a size, then retry without the per-frame choreographer loop.
663+
root.post(() -> tryCommitFragmentTransaction(root, optionsToUse));
664+
return;
665+
}
666+
667+
commitFragmentTransaction(root, optionsToUse);
617668
}
618669

619670
private void updateMapOptionValues(int viewId, @NonNull ReadableMap mapOptions) {
@@ -654,14 +705,18 @@ private void commitFragmentTransaction(
654705
if (activity == null) return;
655706
int viewId = view.getId();
656707
Fragment fragment;
708+
IMapViewFragment mapViewFragment;
657709

658710
CustomTypes.MapViewType mapViewType =
659711
EnumTranslationUtil.getMapViewTypeFromJsValue(mapOptions.getInt("mapViewType"));
660712

661713
GoogleMapOptions googleMapOptions = buildGoogleMapOptions(mapOptions);
662714

663715
if (mapViewType == CustomTypes.MapViewType.MAP) {
664-
fragment = MapViewFragment.newInstance(reactContext, viewId, googleMapOptions);
716+
MapViewFragment mapFragment =
717+
MapViewFragment.newInstance(reactContext, viewId, googleMapOptions);
718+
fragment = mapFragment;
719+
mapViewFragment = mapFragment;
665720
} else {
666721
NavViewFragment navFragment =
667722
NavViewFragment.newInstance(reactContext, viewId, googleMapOptions);
@@ -682,19 +737,23 @@ private void commitFragmentTransaction(
682737
}
683738

684739
fragment = navFragment;
740+
mapViewFragment = navFragment;
685741
}
686742

687-
fragmentMap.put(viewId, new WeakReference<IMapViewFragment>((IMapViewFragment) fragment));
743+
fragmentMap.put(viewId, new WeakReference<IMapViewFragment>(mapViewFragment));
688744

689745
activity
690746
.getSupportFragmentManager()
691747
.beginTransaction()
692748
.replace(viewId, fragment, String.valueOf(viewId))
693749
.commit();
694750

751+
// Start per-frame layout loop to keep fragment sized correctly.
752+
startLayoutLoop(view);
753+
695754
// Trigger layout after fragment is added
696755
// Post to ensure fragment transaction is complete
697-
view.post(() -> layoutFragmentInView(view));
756+
view.post(() -> layoutFragmentInView(view, mapViewFragment));
698757
}
699758

700759
public GoogleMap getGoogleMap(int viewId) {

0 commit comments

Comments
 (0)