|
| 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