Skip to content

Commit

Permalink
# This is a combination of 5 commits.
Browse files Browse the repository at this point in the history
# This is the 1st commit message:

Formatting

# This is the commit message flutter#2:

Suppress lint

# This is the commit message flutter#3:

Docs, linter

# This is the commit message flutter#4:

More docs

# This is the commit message #5:

Fix whitespace:
  • Loading branch information
GaryQian committed Sep 4, 2020
1 parent 0d8a5e6 commit eb423bd
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import android.util.SparseArray;
import android.view.View;
import android.view.ViewStructure;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
Expand All @@ -26,8 +28,6 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
Expand Down Expand Up @@ -60,6 +60,7 @@ public class TextInputPlugin {
// details.
private boolean isInputConnectionLocked;

@SuppressLint("NewApi")
public TextInputPlugin(
View view,
@NonNull TextInputChannel textInputChannel,
Expand All @@ -72,6 +73,9 @@ public TextInputPlugin(
afm = null;
}

// Sets up syncing ime insets with the framework, allowing
// the Flutter view to grow and shrink to accomodate Android
// controlled keyboard animations.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
int mask = 0;
if ((View.SYSTEM_UI_FLAG_HIDE_NAVIGATION & mView.getWindowSystemUiVisibility()) == 0) {
Expand All @@ -86,8 +90,8 @@ public TextInputPlugin(
mask, // Overlay
WindowInsets.Type.ime() // Deferred
);
view.setWindowInsetsAnimationCallback(imeSyncCallback);
view.setOnApplyWindowInsetsListener(imeSyncCallback);
mView.setWindowInsetsAnimationCallback(imeSyncCallback);
mView.setOnApplyWindowInsetsListener(imeSyncCallback);
}

this.textInputChannel = textInputChannel;
Expand Down Expand Up @@ -161,12 +165,32 @@ public void sendAppPrivateCommand(String action, Bundle data) {

// Loosely based off of
// https://github.com/android/user-interface-samples/blob/master/WindowInsetsAnimation/app/src/main/java/com/google/android/samples/insetsanimation/RootViewDeferringInsetsCallback.kt
// When the IME is shown or hidden, it sends an onApplyWindowInsets call with the
// final state of the IME. This defers the final call to allow the animation to
// take place before re-calling onApplyWindowInsets after animation completion.
//
// When the IME is shown or hidden, it immediately sends an onApplyWindowInsets call
// with the final state of the IME. This initial call disrupts the animation, which
// causes a flicker in the beginning.
//
// To fix this, this class extends WindowInsetsAnimation.Callback and implements
// OnApplyWindowInsetsListener. We capture and defer the initial call to
// onApplyWindowInsets while the animation completes. When the animation
// finishes, we can then release the call by invoking it in the onEnd callback
//
// The WindowInsetsAnimation.Callback extension forwards the new state of the
// IME inset from onProgress() to the framework. We also make use of the
// onPrepare and onStart callbacks to detect which calls to onApplyWindowInsets
// would interrupt the animation and defer it.
//
// By implementing OnApplyWindowInsetsListener, we are able to capture Android's
// attempts to call the FlutterView's onApplyWindowInsets. When a call to onPrepare
// and subsequently onStart occurs, we can mark any non-animation calls to
// onApplyWindowInsets() that ocurrs between prepare and start as deferred by
// using this class' custom implementation to cache the WindowInsets passed in.
// When onEnd indicates the end of the animation, the deferred call is dispatched
// again, this time avoiding any flicker since the animation is now complete.
@VisibleForTesting
@TargetApi(30)
@RequiresApi(30)
@SuppressLint({"NewApi", "Override"})
class ImeSyncDeferringInsetsCallback extends WindowInsetsAnimation.Callback
implements View.OnApplyWindowInsetsListener {
private int overlayInsetTypes;
Expand All @@ -192,8 +216,13 @@ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
lastWindowInsets = windowInsets;
}
if (deferredInsets) {
// 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);
}

Expand All @@ -214,7 +243,8 @@ public WindowInsetsAnimation.Bounds onStart(
}

@Override
public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
public WindowInsets onProgress(
WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
if (!deferredInsets) {
return insets;
}
Expand All @@ -230,6 +260,12 @@ public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation>
}
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, which causes the IME
// inset to be overlaid with any bars present.
Insets newImeInsets =
Insets.of(
0,
Expand All @@ -240,7 +276,10 @@ public WindowInsets onProgress(WindowInsets insets, List<WindowInsetsAnimation>
- insets.getInsets(overlayInsetTypes).bottom,
0));
builder.setInsets(deferredInsetTypes, newImeInsets);
// Directly call onApplyWindowInsets as we want to skip this class' version of this call.
// 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;
}
Expand Down Expand Up @@ -315,6 +354,10 @@ public void unlockPlatformViewInputConnection() {
public void destroy() {
platformViewsController.detachTextInputPlugin();
textInputChannel.setTextInputMethodHandler(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mView.setWindowInsetsAnimationCallback(null);
mView.setOnApplyWindowInsetsListener(null);
}
}

private static int inputTypeFromTextInputType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMethodCodec;
Expand All @@ -50,6 +53,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
Expand All @@ -62,6 +66,9 @@
@Config(manifest = Config.NONE, shadows = TextInputPluginTest.TestImm.class)
@RunWith(RobolectricTestRunner.class)
public class TextInputPluginTest {
@Mock FlutterJNI mockFlutterJni;
@Mock FlutterLoader mockFlutterLoader;

// Verifies the method and arguments for a captured method call.
private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs)
throws JSONException {
Expand Down Expand Up @@ -655,7 +662,7 @@ public void ime_windowInsetsSync() {
spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
when(flutterEngine.getRenderer()).thenReturn(flutterRenderer);
flutterView.attachToFlutterEngine(flutterEngine);
testView.attachToFlutterEngine(flutterEngine);

WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class);
when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime());
Expand Down

0 comments on commit eb423bd

Please sign in to comment.