Currently, we have an app with the following requirements
android:windowSoftInputMode="adjustPan"
ViewCompat.setWindowInsetsAnimationCallback
and ViewCompat.setOnApplyWindowInsetsListener
to interact with soft keyboard visibility with smooth animation.Here is our code, when interacting with soft keyboard visibility. It works pretty well in the case, when our EditText
is not scrollable.
The animation went pretty well, when keyboard is showing and hiding.
public class MainActivity extends AppCompatActivity {
EditText editText;
LinearLayout toolbar;
FrameLayout keyboardView;
private int systemBarsHeight = 0;
private int keyboardHeightWhenVisible = 0;
private boolean keyboardVisible = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = findViewById(R.id.edit_text);
toolbar = findViewById(R.id.toolbar);
keyboardView = findViewById(R.id.keyboard_view);
final View rootView = getWindow().getDecorView().getRootView();
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
systemBarsHeight = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
keyboardVisible = imeVisible;
if (keyboardVisible) {
keyboardHeightWhenVisible = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
}
// https://stackoverflow.com/questions/75325095/how-to-use-windowinsetscompat-correctly-to-listen-to-keyboard-height-change-in-a
return ViewCompat.onApplyWindowInsets(v, insets);
});
WindowInsetsAnimationCompat.Callback callback = new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
@NonNull
@Override
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
// Find an IME animation.
WindowInsetsAnimationCompat imeAnimation = null;
for (WindowInsetsAnimationCompat animation : runningAnimations) {
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
imeAnimation = animation;
break;
}
}
if (imeAnimation != null) {
int keyboardViewHeight;
if (keyboardVisible) {
keyboardViewHeight = (int) (keyboardHeightWhenVisible * imeAnimation.getInterpolatedFraction()) - systemBarsHeight;
} else {
keyboardViewHeight = (int) (keyboardHeightWhenVisible * (1.0-imeAnimation.getInterpolatedFraction())) - systemBarsHeight;
}
keyboardViewHeight = Math.max(0, keyboardViewHeight);
ViewGroup.LayoutParams params = keyboardView.getLayoutParams();
params.height = keyboardViewHeight;
keyboardView.setLayoutParams(params);
Log.i("CHEOK", "keyboardVisible = " + keyboardVisible + ", keyboardViewHeight = " + keyboardViewHeight);
}
return insets;
}
};
ViewCompat.setWindowInsetsAnimationCallback(rootView, callback);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/edit_text"
android:padding="16dp"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top" />
<LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:background="#ffff00" />
<FrameLayout
android:id="@+id/keyboard_view"
android:background="#ff0000"
android:layout_width="match_parent"
android:layout_height="0dp" />
</LinearLayout>
Here is the outcome.
However, our app becomes "jumpy", when the content of EditText
is scrollable.
Does anyone know what is the root cause of this problem, and how we can resolve such?
A demo to demonstrate such an issue, can be downloaded from https://github.com/yccheok/programming-issue/tree/main/jumpy
Using both adjustPan
and WindowInsetsAnimation
seems to over-animating the content.
adjustPan - The activity's main window is not resized to make room for the soft keyboard. Rather, the contents of the window are automatically panned so that the current focus is never obscured by the keyboard and users can always see what they are typing. This is generally less desirable than resizing, because the user may need to close the soft keyboard to get at and interact with obscured parts of the window.
This means that the activity is pushing its upper part until it's possible to make a room for the EditText
widget (or its editable part) visible so that the user can see what they are typing.
And I do believe that Google developers provided the insets API to get the obsolete overwhelming stuff deprecated soon or later; for instance recently setting adjustResize
programmatically is now deprecated as of API Level 30 and replaced with the inset API.
More verification of the inconvenience between adjustPan
and WindowInsetsAnimation
, try the below couple of scenarios in your sample repo:
android:windowSoftInputMode="adjustPan"
ViewCompat.setWindowInsetsAnimationCallback(rootView, callback);
Either scenario will work solely without having to worry about the jumpy/bouncy layout. But in case of the scenario no. 1, the red view appears because the activity area doesn't occupy the entire window screen (this requires to have a full screen app) with WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
. This tutorial deeply targets different aspects of insets API.
What I think the cause of this jumpy/bouncy behavior is that the adjustPan
tries to do its job when the keyboard is shown; but eventually hard-coding the margins in the WindowInsetsAnimation
wins the round and makes the layout bounce back at the end of the animation.
So, it's recommended to remove that adjustPan
to keep the new fancy inset APIs up and running.
Or at least keep it at the manifest file, but disable it just before starting the animation and re-enable it again at the end of the animation (i.e., disable its panning behavior during the animation) using onPrepare()
and onEnd()
callbacks:
WindowInsetsAnimationCompat
.Callback callback = new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
@Override
public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
super.onPrepare(animation);
// disable adjustPan
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
}
@Override
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
super.onEnd(animation);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
@NonNull
@Override
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
// ... code is omitted for simplicity
}
};
But make sure that you also have a full screen app:
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);