Skip to content

Commit

Permalink
Move the WindowInsetsAnimation.Callback implementation to an inner cl…
Browse files Browse the repository at this point in the history
…ass to avoid Android class loader warnings

ImeSyncDeferringInsetsCallback had been a subclass of WindowInsetsAnimation.Callback,
which was introduced in Android API level 30.  The class loader on
older versions of Android was logging warnings about unresolvable
classes when loading TextInputPlugin, which holds a reference to
ImeSyncDeferringInsetsCallback.

See flutter/flutter#66908
  • Loading branch information
jason-simmons committed Dec 8, 2020
1 parent ff87b1d commit b635073
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@
@RequiresApi(30)
@SuppressLint({"NewApi", "Override"})
@Keep
class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
implements View.OnApplyWindowInsetsListener {
class ImeSyncDeferringInsetsCallback {
private int overlayInsetTypes;
private int deferredInsetTypes;

private View view;
private WindowInsets lastWindowInsets;
private AnimationCallback animationCallback;
private InsetsListener insetsListener;

// True when an animation that matches deferredInsetTypes is active.
//
// While this is active, this class will capture the initial window inset
Expand All @@ -67,16 +69,17 @@ class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback

ImeSyncDeferringInsetsCallback(
@NonNull View view, int overlayInsetTypes, int deferredInsetTypes) {
super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
this.overlayInsetTypes = overlayInsetTypes;
this.deferredInsetTypes = deferredInsetTypes;
this.view = view;
this.animationCallback = new AnimationCallback();
this.insetsListener = new InsetsListener();
}

// Add this object's event listeners to its view.
void install() {
view.setWindowInsetsAnimationCallback(this);
view.setOnApplyWindowInsetsListener(this);
view.setWindowInsetsAnimationCallback(animationCallback);
view.setOnApplyWindowInsetsListener(insetsListener);
}

// Remove this object's event listeners from its view.
Expand All @@ -85,93 +88,114 @@ void remove() {
view.setOnApplyWindowInsetsListener(null);
}

@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
this.view = view;
if (needsSave) {
// Store the view and insets for us in onEnd() below. This captured inset
// is not part of the animation and instead, represents the final state
// of the inset after the animation is completed. Thus, we defer the processing
// of this WindowInset until the animation completes.
lastWindowInsets = windowInsets;
needsSave = false;
}
if (animating) {
// While animation is running, we consume the insets to prevent disrupting
// the animation, which skips this implementation and calls the view's
// onApplyWindowInsets directly to avoid being consumed here.
return WindowInsets.CONSUMED;
}

// If no animation is happening, pass the insets on to the view's own
// inset handling.
return view.onApplyWindowInsets(windowInsets);
@VisibleForTesting
View.OnApplyWindowInsetsListener getInsetsListener() {
return insetsListener;
}

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

@Override
public WindowInsets onProgress(
WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
if (!animating || needsSave) {
return insets;
// WindowInsetsAnimation.Callback was introduced in API level 30. The callback
// subclass is separated into an inner class in order to avoid warnings from
// the Android class loader on older platforms.
private class AnimationCallback extends WindowInsetsAnimation.Callback {
AnimationCallback() {
super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
}
boolean matching = false;
for (WindowInsetsAnimation animation : runningAnimations) {

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

@Override
public WindowInsets onProgress(
WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
if (!animating || needsSave) {
return insets;
}
boolean matching = false;
for (WindowInsetsAnimation animation : runningAnimations) {
if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
matching = true;
continue;
}
}
if (!matching) {
return insets;
}
WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets);
// Overlay the ime-only insets with the full insets.
//
// The IME insets passed in by onProgress assumes that the entire animation
// occurs above any present navigation and status bars. This causes the
// IME inset to be too large for the animation. To remedy this, we merge the
// IME inset with other insets present via a subtract + reLu, which causes the
// IME inset to be overlaid with any bars present.
Insets newImeInsets =
Insets.of(
0,
0,
0,
Math.max(
insets.getInsets(deferredInsetTypes).bottom
- insets.getInsets(overlayInsetTypes).bottom,
0));
builder.setInsets(deferredInsetTypes, newImeInsets);
// Directly call onApplyWindowInsets of the view as we do not want to pass through
// the onApplyWindowInsets defined in this class, which would consume the insets
// as if they were a non-animation inset change and cache it for re-dispatch in
// onEnd instead.
view.onApplyWindowInsets(builder.build());
return insets;
}
WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets);
// Overlay the ime-only insets with the full insets.
//
// The IME insets passed in by onProgress assumes that the entire animation
// occurs above any present navigation and status bars. This causes the
// IME inset to be too large for the animation. To remedy this, we merge the
// IME inset with other insets present via a subtract + reLu, which causes the
// IME inset to be overlaid with any bars present.
Insets newImeInsets =
Insets.of(
0,
0,
0,
Math.max(
insets.getInsets(deferredInsetTypes).bottom
- insets.getInsets(overlayInsetTypes).bottom,
0));
builder.setInsets(deferredInsetTypes, newImeInsets);
// Directly call onApplyWindowInsets of the view as we do not want to pass through
// the onApplyWindowInsets defined in this class, which would consume the insets
// as if they were a non-animation inset change and cache it for re-dispatch in
// onEnd instead.
view.onApplyWindowInsets(builder.build());
return insets;

@Override
public void onEnd(WindowInsetsAnimation animation) {
if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
// If we deferred the IME insets and an IME animation has finished, we need to reset
// the flags
animating = false;

// And finally dispatch the deferred insets to the view now.
// Ideally we would just call view.requestApplyInsets() and let the normal dispatch
// cycle happen, but this happens too late resulting in a visual flicker.
// Instead we manually dispatch the most recent WindowInsets to the view.
if (lastWindowInsets != null && view != null) {
view.dispatchApplyWindowInsets(lastWindowInsets);
}
}
}
}

@Override
public void onEnd(WindowInsetsAnimation animation) {
if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
// If we deferred the IME insets and an IME animation has finished, we need to reset
// the flags
animating = false;

// And finally dispatch the deferred insets to the view now.
// Ideally we would just call view.requestApplyInsets() and let the normal dispatch
// cycle happen, but this happens too late resulting in a visual flicker.
// Instead we manually dispatch the most recent WindowInsets to the view.
if (lastWindowInsets != null && view != null) {
view.dispatchApplyWindowInsets(lastWindowInsets);
private class InsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
ImeSyncDeferringInsetsCallback.this.view = view;
if (needsSave) {
// Store the view and insets for us in onEnd() below. This captured inset
// is not part of the animation and instead, represents the final state
// of the inset after the animation is completed. Thus, we defer the processing
// of this WindowInset until the animation completes.
lastWindowInsets = windowInsets;
needsSave = false;
}
if (animating) {
// While animation is running, we consume the insets to prevent disrupting
// the animation, which skips this implementation and calls the view's
// onApplyWindowInsets directly to avoid being consumed here.
return WindowInsets.CONSUMED;
}

// If no animation is happening, pass the insets on to the view's own
// inset handling.
return view.onApplyWindowInsets(windowInsets);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1067,20 +1067,20 @@ public void ime_windowInsetsSync() {
ArgumentCaptor<FlutterRenderer.ViewportMetrics> viewportMetricsCaptor =
ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class);

imeSyncCallback.onApplyWindowInsets(testView, deferredInsets);
imeSyncCallback.onApplyWindowInsets(testView, noneInsets);
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, deferredInsets);
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, noneInsets);

verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom);
assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop);
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom);
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop);

imeSyncCallback.onPrepare(animation);
imeSyncCallback.onApplyWindowInsets(testView, deferredInsets);
imeSyncCallback.onStart(animation, null);
imeSyncCallback.getAnimationCallback().onPrepare(animation);
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, deferredInsets);
imeSyncCallback.getAnimationCallback().onStart(animation, null);
// Only the final state call is saved, extra calls are passed on.
imeSyncCallback.onApplyWindowInsets(testView, imeInsets2);
imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, imeInsets2);

verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
// No change, as deferredInset is stored to be passed in onEnd()
Expand All @@ -1089,23 +1089,23 @@ public void ime_windowInsetsSync() {
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom);
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop);

imeSyncCallback.onProgress(imeInsets0, animationList);
imeSyncCallback.getAnimationCallback().onProgress(imeInsets0, animationList);

verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(40, viewportMetricsCaptor.getValue().viewPaddingBottom);
assertEquals(10, viewportMetricsCaptor.getValue().viewPaddingTop);
assertEquals(60, viewportMetricsCaptor.getValue().viewInsetBottom);
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop);

imeSyncCallback.onProgress(imeInsets1, animationList);
imeSyncCallback.getAnimationCallback().onProgress(imeInsets1, animationList);

verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
assertEquals(40, viewportMetricsCaptor.getValue().viewPaddingBottom);
assertEquals(10, viewportMetricsCaptor.getValue().viewPaddingTop);
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); // Cannot be negative
assertEquals(0, viewportMetricsCaptor.getValue().viewInsetTop);

imeSyncCallback.onEnd(animation);
imeSyncCallback.getAnimationCallback().onEnd(animation);

verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture());
// Values should be of deferredInsets, not imeInsets2
Expand Down

0 comments on commit b635073

Please sign in to comment.