Skip to content

Commit

Permalink
Capture additional final inset states in ImeSyncDeferringInsetsCallba…
Browse files Browse the repository at this point in the history
…ck (flutter#42700)

Fixes flutter/flutter#118761.

For some context: ImeSyncDeferringInsetsCallback extends [WindowInsetsAnimation.Callback](https://developer.android.com/reference/android/view/WindowInsetsAnimation.Callback) specifically to handle insets related to keyboard animations.

When a keyboard animation happens, we get a call to onPrepare with the animation, and then to onApplyWindowInsets with [WindowInsets](https://developer.android.com/reference/android/view/WindowInsets) that represent the post animation state. For example, for hiding the keyboard, we would get WindowInsets with height of 0 for insets of type WindowInsets.Type.ime() (input method editor). We save these post-animation insets. 

We then get calls to onProgress for each frame of the animation, and use the WindowInsets passed to onProgress to build new WindowInsets based on the saved post-animation insets, but with the keyboard insets overridden by the WindowInsets passed to onProgress.

Finally, we get a call to onEnd, where we apply the saved insets.

However, there can be multiple animation running at the same time. For example, dismissing the keyboard can result in an animation associated solely with the keyboard happening, as well as an animation associated only with the navigation bar happening. And they can start at slightly different times. This can result in the situation described in this comment: flutter/flutter#118761 (comment)

Re-setting needsSave to false ensures we don't ignore any updates to the final state, ensuring correct insets in onProgress and onEnd.

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
gmackall authored Jun 13, 2023
1 parent b6bf3a6 commit 66a5761
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ private class AnimationCallback extends WindowInsetsAnimation.Callback {

@Override
public void onPrepare(WindowInsetsAnimation animation) {
needsSave = true;
if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
animating = true;
needsSave = true;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,87 @@ public void ime_windowInsetsSync_laidOutBehindNavigation_includesNavigationBars(
assertEquals(100, viewportMetricsCaptor.getValue().viewInsetBottom);
}

@Test
@TargetApi(30)
@Config(sdk = 30)
public void lastWindowInsets_updatedOnSecondOnProgressCall() {
FlutterView testView = spy(new FlutterView(Robolectric.setupActivity(Activity.class)));
when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);

TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);
testView.attachToFlutterEngine(flutterEngine);

WindowInsetsAnimation imeAnimation = mock(WindowInsetsAnimation.class);
when(imeAnimation.getTypeMask()).thenReturn(WindowInsets.Type.ime());
WindowInsetsAnimation navigationBarAnimation = mock(WindowInsetsAnimation.class);
when(navigationBarAnimation.getTypeMask()).thenReturn(WindowInsets.Type.navigationBars());

List<WindowInsetsAnimation> animationList = new ArrayList();
animationList.add(imeAnimation);
animationList.add(navigationBarAnimation);

ArgumentCaptor<FlutterRenderer.ViewportMetrics> viewportMetricsCaptor =
ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class);

WindowInsets.Builder builder = new WindowInsets.Builder();

// Set the initial insets and verify that they were set and the bottom view padding is correct
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 1000));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100));
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build());

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom);

// Call onPrepare and set the lastWindowInsets - these should be stored for the end of the
// animation instead of being applied immediately
imeSyncCallback.getAnimationCallback().onPrepare(imeAnimation);
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100));
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build());

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom);

// Call onPrepare again and apply new insets - these should overrite lastWindowInsets
imeSyncCallback.getAnimationCallback().onPrepare(navigationBarAnimation);
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0));
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build());

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom);

// Progress the animation and ensure that the navigation bar insets have not been
// subtracted from the IME insets
builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 500));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0));
imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList);

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom);

builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 250));
builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0));
imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList);

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom);

// End the animation and ensure that the bottom insets match the lastWindowInsets that we set
// during onPrepare
imeSyncCallback.getAnimationCallback().onEnd(imeAnimation);

verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom);
}

@Test
@TargetApi(30)
@Config(sdk = 30)
Expand Down

0 comments on commit 66a5761

Please sign in to comment.