Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
ImeSyncDeferringInsetsCallback.java
Go to the documentation of this file.
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
5package io.flutter.plugin.editing;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.annotation.SuppressLint;
10import android.annotation.TargetApi;
11import android.graphics.Insets;
12import android.view.View;
13import android.view.WindowInsets;
14import android.view.WindowInsetsAnimation;
15import androidx.annotation.Keep;
16import androidx.annotation.NonNull;
17import androidx.annotation.RequiresApi;
18import androidx.annotation.VisibleForTesting;
19import java.util.List;
20
21// Loosely based off of
22// https://github.com/android/user-interface-samples/blob/main/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt
23//
24// When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call
25// with the final state of the IME. This initial call disrupts the animation, which
26// causes a flicker in the beginning.
27//
28// To fix this, this class extends WindowInsetsAnimation.Callback and implements
29// OnApplyWindowInsetsListener. We capture and defer the initial call to
30// onApplyWindowInsets while the animation completes. When the animation
31// finishes, we can then release the call by invoking it in the onEnd callback
32//
33// The WindowInsetsAnimation.Callback extension forwards the new state of the
34// IME inset from onProgress() to the framework. We also make use of the
35// onStart callback to detect which calls to onApplyWindowInsets would
36// interrupt the animation and defer it.
37//
38// By implementing OnApplyWindowInsetsListener, we are able to capture Android's
39// attempts to call the FlutterView's onApplyWindowInsets. When a call to onStart
40// occurs, we can mark any non-animation calls to onApplyWindowInsets() that
41// occurs between prepare and start as deferred by using this class' wrapper
42// implementation to cache the WindowInsets passed in and turn the current call into
43// a no-op. When onEnd indicates the end of the animation, the deferred call is
44// dispatched again, this time avoiding any flicker since the animation is now
45// complete.
46@VisibleForTesting
47@TargetApi(API_LEVELS.API_30)
48@RequiresApi(API_LEVELS.API_30)
49@SuppressLint({"NewApi", "Override"})
50@Keep
52 private final int deferredInsetTypes = WindowInsets.Type.ime();
53 private View view;
54 private WindowInsets lastWindowInsets;
55 private AnimationCallback animationCallback;
56 private InsetsListener insetsListener;
57
58 // True when an animation that matches deferredInsetTypes is active.
59 //
60 // While this is active, this class will capture the initial window inset
61 // sent into lastWindowInsets by flagging needsSave to true, and will hold
62 // onto the intitial inset until the animation is completed, when it will
63 // re-dispatch the inset change.
64 private boolean animating = false;
65 // When an animation begins, android sends a WindowInset with the final
66 // state of the animation. When needsSave is true, we know to capture this
67 // initial WindowInset.
68 //
69 // Certain actions, like dismissing the keyboard, can trigger multiple
70 // animations that are slightly offset in start time. To capture the
71 // correct final insets in these situations we update needsSave to true
72 // in each onPrepare callback, so that we save the latest final state
73 // to apply in onEnd.
74 private boolean needsSave = false;
75
76 ImeSyncDeferringInsetsCallback(@NonNull View view) {
77 this.view = view;
78 this.animationCallback = new AnimationCallback();
79 this.insetsListener = new InsetsListener();
80 }
81
82 // Add this object's event listeners to its view.
83 void install() {
84 view.setWindowInsetsAnimationCallback(animationCallback);
85 view.setOnApplyWindowInsetsListener(insetsListener);
86 }
87
88 // Remove this object's event listeners from its view.
89 void remove() {
90 view.setWindowInsetsAnimationCallback(null);
91 view.setOnApplyWindowInsetsListener(null);
92 }
93
94 @VisibleForTesting
95 View.OnApplyWindowInsetsListener getInsetsListener() {
96 return insetsListener;
97 }
98
99 @VisibleForTesting
100 WindowInsetsAnimation.Callback getAnimationCallback() {
101 return animationCallback;
102 }
103
104 // WindowInsetsAnimation.Callback was introduced in API level 30. The callback
105 // subclass is separated into an inner class in order to avoid warnings from
106 // the Android class loader on older platforms.
107 @Keep
108 private class AnimationCallback extends WindowInsetsAnimation.Callback {
109 AnimationCallback() {
110 super(WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
111 }
112
113 @Override
114 public void onPrepare(WindowInsetsAnimation animation) {
115 needsSave = true;
116 if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
117 animating = true;
118 }
119 }
120
121 @Override
122 public WindowInsets onProgress(
123 WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
124 if (!animating || needsSave) {
125 return insets;
126 }
127 boolean matching = false;
128 for (WindowInsetsAnimation animation : runningAnimations) {
129 if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
130 matching = true;
131 continue;
132 }
133 }
134 if (!matching) {
135 return insets;
136 }
137
138 // The IME insets include the height of the navigation bar. If the app isn't laid out behind
139 // the navigation bar, this causes the IME insets to be too large during the animation.
140 // To fix this, we subtract the navigationBars bottom inset if the system UI flags for laying
141 // out behind the navigation bar aren't present.
142 int excludedInsets = 0;
143 int systemUiFlags = view.getWindowSystemUiVisibility();
144 if ((systemUiFlags & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
145 && (systemUiFlags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
146 excludedInsets = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
147 }
148
149 WindowInsets.Builder builder = new WindowInsets.Builder(lastWindowInsets);
150 Insets newImeInsets =
151 Insets.of(
152 0, 0, 0, Math.max(insets.getInsets(deferredInsetTypes).bottom - excludedInsets, 0));
153 builder.setInsets(deferredInsetTypes, newImeInsets);
154
155 // Directly call onApplyWindowInsets of the view as we do not want to pass through
156 // the onApplyWindowInsets defined in this class, which would consume the insets
157 // as if they were a non-animation inset change and cache it for re-dispatch in
158 // onEnd instead.
159 view.onApplyWindowInsets(builder.build());
160 return insets;
161 }
162
163 @Override
164 public void onEnd(WindowInsetsAnimation animation) {
165 if (animating && (animation.getTypeMask() & deferredInsetTypes) != 0) {
166 // If we deferred the IME insets and an IME animation has finished, we need to reset
167 // the flags
168 animating = false;
169
170 // And finally dispatch the deferred insets to the view now.
171 // Ideally we would just call view.requestApplyInsets() and let the normal dispatch
172 // cycle happen, but this happens too late resulting in a visual flicker.
173 // Instead we manually dispatch the most recent WindowInsets to the view.
174 if (lastWindowInsets != null && view != null) {
175 view.dispatchApplyWindowInsets(lastWindowInsets);
176 }
177 }
178 }
179 }
180
181 private class InsetsListener implements View.OnApplyWindowInsetsListener {
182 @Override
183 public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
184 ImeSyncDeferringInsetsCallback.this.view = view;
185 if (needsSave) {
186 // Store the view and insets for us in onEnd() below. This captured inset
187 // is not part of the animation and instead, represents the final state
188 // of the inset after the animation is completed. Thus, we defer the processing
189 // of this WindowInset until the animation completes.
190 lastWindowInsets = windowInsets;
191 needsSave = false;
192 }
193 if (animating) {
194 // While animation is running, we consume the insets to prevent disrupting
195 // the animation, which skips this implementation and calls the view's
196 // onApplyWindowInsets directly to avoid being consumed here.
197 return WindowInsets.CONSUMED;
198 }
199
200 // If no animation is happening, pass the insets on to the view's own
201 // inset handling.
202 return view.onApplyWindowInsets(windowInsets);
203 }
204 }
205}