Skip to content

Commit c943a1b

Browse files
committed
fix: android view invalidation mechanism
1 parent 3c46f4d commit c943a1b

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)