Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 700c7ca

Browse files
committed
Extract the WindowInsetsAnimation.Callback subclass into a separate class that will be lazily loaded
WindowInsetsAnimation.Callback was introduced in API level 30. This PR moves the text input plugin's WindowInsetsAnimation.Callback subclass into a class that will only be loaded if the embedding has checked for a sufficient API level. See flutter/flutter#66908
1 parent 83b9df9 commit 700c7ca

File tree

5 files changed

+181
-161
lines changed

5 files changed

+181
-161
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardM
794794
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java
795795
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java
796796
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
797+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
797798
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
798799
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
799800
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ android_java_sources = [
213213
"io/flutter/plugin/common/StandardMethodCodec.java",
214214
"io/flutter/plugin/common/StringCodec.java",
215215
"io/flutter/plugin/editing/FlutterTextUtils.java",
216+
"io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
216217
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
217218
"io/flutter/plugin/editing/TextInputPlugin.java",
218219
"io/flutter/plugin/localization/LocalizationPlugin.java",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugin.editing;
6+
7+
import android.annotation.SuppressLint;
8+
import android.annotation.TargetApi;
9+
import android.graphics.Insets;
10+
import android.view.View;
11+
import android.view.WindowInsets;
12+
import android.view.WindowInsetsAnimation;
13+
import androidx.annotation.Keep;
14+
import androidx.annotation.NonNull;
15+
import androidx.annotation.RequiresApi;
16+
import androidx.annotation.VisibleForTesting;
17+
import java.util.List;
18+
19+
// Loosely based off of
20+
// https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt
21+
//
22+
// When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call
23+
// with the final state of the IME. This initial call disrupts the animation, which
24+
// causes a flicker in the beginning.
25+
//
26+
// To fix this, this class extends WindowInsetsAnimation.Callback and implements
27+
// OnApplyWindowInsetsListener. We capture and defer the initial call to
28+
// onApplyWindowInsets while the animation completes. When the animation
29+
// finishes, we can then release the call by invoking it in the onEnd callback
30+
//
31+
// The WindowInsetsAnimation.Callback extension forwards the new state of the
32+
// IME inset from onProgress() to the framework. We also make use of the
33+
// onStart callback to detect which calls to onApplyWindowInsets would
34+
// interrupt the animation and defer it.
35+
//
36+
// By implementing OnApplyWindowInsetsListener, we are able to capture Android's
37+
// attempts to call the FlutterView's onApplyWindowInsets. When a call to onStart
38+
// occurs, we can mark any non-animation calls to onApplyWindowInsets() that
39+
// occurs between prepare and start as deferred by using this class' wrapper
40+
// implementation to cache the WindowInsets passed in and turn the current call into
41+
// a no-op. When onEnd indicates the end of the animation, the deferred call is
42+
// dispatched again, this time avoiding any flicker since the animation is now
43+
// complete.
44+
@VisibleForTesting
45+
@TargetApi(30)
46+
@RequiresApi(30)
47+
@SuppressLint({"NewApi", "Override"})
48+
@Keep
49+
class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
50+
implements View.OnApplyWindowInsetsListener {
51+
private int overlayInsetTypes;
52+
private int deferredInsetTypes;
53+
54+
private View view;
55+
private WindowInsets lastWindowInsets;
56+
// True when an animation that matches deferredInsetTypes is active.
57+
//
58+
// While this is active, this class will capture the initial window inset
59+
// sent into lastWindowInsets by flagging needsSave to true, and will hold
60+
// onto the intitial inset until the animation is completed, when it will
61+
// re-dispatch the inset change.
62+
private boolean animating = false;
63+
// When an animation begins, android sends a WindowInset with the final
64+
// state of the animation. When needsSave is true, we know to capture this
65+
// initial WindowInset.
66+
private boolean needsSave = false;
67+
68+
ImeSyncDeferringInsetsCallback(
69+
@NonNull View view, int overlayInsetTypes, int deferredInsetTypes) {
70+
super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
71+
this.overlayInsetTypes = overlayInsetTypes;
72+
this.deferredInsetTypes = deferredInsetTypes;
73+
this.view = view;
74+
}
75+
76+
void install() {
77+
view.setWindowInsetsAnimationCallback(this);
78+
view.setOnApplyWindowInsetsListener(this);
79+
}
80+
81+
void remove() {
82+
view.setWindowInsetsAnimationCallback(null);
83+
view.setOnApplyWindowInsetsListener(null);
84+
}
85+
86+
@Override
87+
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
88+
this.view = view;
89+
if (needsSave) {
90+
// Store the view and insets for us in onEnd() below. This captured inset
91+
// is not part of the animation and instead, represents the final state
92+
// of the inset after the animation is completed. Thus, we defer the processing
93+
// of this WindowInset until the animation completes.
94+
lastWindowInsets = windowInsets;
95+
needsSave = false;
96+
}
97+
if (animating) {
98+
// While animation is running, we consume the insets to prevent disrupting
99+
// the animation, which skips this implementation and calls the view's
100+
// onApplyWindowInsets directly to avoid being consumed here.
101+
return WindowInsets.CONSUMED;
102+
}
103+
104+
// If no animation is happening, pass the insets on to the view's own
105+
// inset handling.
106+
return view.onApplyWindowInsets(windowInsets);
107+
}
108+
109+
@Override
110+
public void onPrepare(WindowInsetsAnimation animation) {
111+
if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
112+
animating = true;
113+
needsSave = true;
114+
}
115+
}
116+
117+
@Override
118+
public WindowInsets onProgress(
119+
WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
120+
if (!animating || needsSave) {
121+
return insets;
122+
}
123+
boolean matching = false;
124+
for (WindowInsetsAnimation animation : runningAnimations) {
125+
if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
126+
matching = true;
127+
continue;
128+
}
129+
}
130+
if (!matching) {
131+
return insets;
132+
}
133+
WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets);
134+
// Overlay the ime-only insets with the full insets.
135+
//
136+
// The IME insets passed in by onProgress assumes that the entire animation
137+
// occurs above any present navigation and status bars. This causes the
138+
// IME inset to be too large for the animation. To remedy this, we merge the
139+
// IME inset with other insets present via a subtract + reLu, which causes the
140+
// IME inset to be overlaid with any bars present.
141+
Insets newImeInsets =
142+
Insets.of(
143+
0,
144+
0,
145+
0,
146+
Math.max(
147+
insets.getInsets(deferredInsetTypes).bottom
148+
- insets.getInsets(overlayInsetTypes).bottom,
149+
0));
150+
builder.setInsets(deferredInsetTypes, newImeInsets);
151+
// Directly call onApplyWindowInsets of the view as we do not want to pass through
152+
// the onApplyWindowInsets defined in this class, which would consume the insets
153+
// as if they were a non-animation inset change and cache it for re-dispatch in
154+
// onEnd instead.
155+
view.onApplyWindowInsets(builder.build());
156+
return insets;
157+
}
158+
159+
@Override
160+
public void onEnd(WindowInsetsAnimation animation) {
161+
if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
162+
// If we deferred the IME insets and an IME animation has finished, we need to reset
163+
// the flags
164+
animating = false;
165+
166+
// And finally dispatch the deferred insets to the view now.
167+
// Ideally we would just call view.requestApplyInsets() and let the normal dispatch
168+
// cycle happen, but this happens too late resulting in a visual flicker.
169+
// Instead we manually dispatch the most recent WindowInsets to the view.
170+
if (lastWindowInsets != null && view != null) {
171+
view.dispatchApplyWindowInsets(lastWindowInsets);
172+
}
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)