Android how to adjust for soft keyboard
Ultimate pitfall guide for adjustResize with full screen mode
Basic
In our AndroidManifest file we can set the windowSoftInputMode
for the activity.
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustPan" >
...
</activity>
adjustPan
shift the whole page up to make room for the keyboard
adjustResize
resize the page, shrink whatever it can to display in a smaller window
See the difference here
This is the baby level basic for every Android developer.
Common Problem
Notice adjustResize
only have effect on layouts that are resizable. Like items in RelativeLayout with weight or page wrapped in ScrollView.
But if we have a ConstraintLayout with fixed margin for all its widgets, then adjustResize
simply won’t work because there is nothing to resize. (See this example)
Options we have whenadjustResize
don’t work
- use
adjustPan
- change to a layout to that support resizing (wrap it in a ScrollView)
- mark some views as
GONE
when detecting the keyboard - change some other values on the fly (font size, margin values…)
- prepare 2 layout files
- Manually do the shift up shift down ourselves
Pitfalls and AndroidBug5497
First and foremost:
Problem: How to detect soft keyboard open and close
The first result on StackOverflow is probably this one. Google didn’t provide us an API to check soft keyboard state, or callback for such events. Look at Google’s treatment to the Android soft keyboard.
On September of 2020, Google released android 11, which finally have the ability to check soft keyboard visibility. See this.
Sounds great except this feature requires API level 30, which means if we want to ship an app today, this feature is not going to work on most Android devices on the market.
In the mean while, what we can do is set a listener to our layout view to detect height changes. If the screen available height suddenly shrink by a certain amount, we can infer it is caused by the keyboard pop up.
Find a root view we like to listen to the height change. Can be our root view or the most outer view group of our activity layout. And set the listener in that view’s onGlobalLayout
Problem: adjustResize does not work in full screen mode
Why? This is a known bug in Android, a hole dug by Google many years ago. This has been reported as issue 5497 in 2009 and still is an issue in 2020.
So, it is up to us developers to deal with it. We can either dodge or patch this hole.
It is easy to dodge, don’t use full screen mode with adjustResize
. But if we cannot dodge, the best solution currently available is to use the AndroidBug5497Workaround
helper class to specifically deal with this problem.
If you search about it, you will find this possible work around here.
How AndroidBug5497Workaround works?
It is not too complicated. We are just manually triggering adjustResize
on keyboard open or close.
I paste the whole helper class here.
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity (Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
});
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard/4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}
Here is how it works.
1. Get the rootView FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
The view from findViewById(android.R.id.content)
is our root view.
The view from content.getChildAt(0)
is the view we pass into setContentView
in our Activity onCreate() method. It is the view of our whole layout, including the most outer layer constriantLayout/RelativeLayout.
2. Set a listener for the root view when the height change
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
});
What this does is getting a callback when our root view’s height just changed. Similar to the problem above of how to detect soft keyboard open or close.
3. Calculate the height change to detect if the keyboard open or close
int usableHeightNow = computeUsableHeight();
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
usableHeightNow
is the screen’s available height after the keyboard open. It becomes smaller when the keyboard is open and bigger when the keyboard is close.
usableHeightSansKeyboard
is the total screen height.
Using the total screen height to subtract the currently available screen height, that height difference is how much space is taken up. (possibly by the keyboard)
If keyboard is open, we get a big difference.
If keyboard is close, normally we get 0.
4. Reset Height and request layout
if (heightDifference > (usableHeightSansKeyboard/4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
The frameLayoutParams
has our layout’s height property, by manually updating its height and requesting a new layout. We force adjustResize
to take affect.
This should solve most problem for full screen mode with adjustResize
Most, not all?
Yup, it still has its own problem. The purposed way to detect keyboard close is just when the height difference does not reach 1/4 of the screen.
Notice our onGlobalLayout
listener get trigger not only when keyboard open or close but also on the bottom navigation bar appear or disappear.
Some phone, like the motorola XT1097, will disable the full screen mode after opening the soft keyboard. This mean the bottom navigation bar will appear and cause our height to change. It does not reach 1/4 of the screen and our listener think the keyboard just closed. (which it is not)
So lets patch the 5497 patch?
Since we are using the heightDifference greater than 1/4 of the screen to determine soft keyboard open, we can use the heightDifference less than negative 1/4 of the screen to detect the keyboard just closed.
If previously keyboard is open, available screen height is small. Now the keyboard is close, the available screen height is big. heightDifference is previous height subtract current height, (small minus big) so on keyboard close the heightDifference should be negative.
I have example here. Look at my possiblyResizeChildOfContent
method, how I use heightDifference to determine keyboard close.
Problem: How to pan the page up more
The given adjustPan
is weak. It only pan the page just high enough to not block the focused editText. This is bad user experience, ideally we want to pan the page high up more to reveal more content below the editText. Again, Google didn’t give us a way to control how much to pan up.
This StackOverflow post asked the same question, but eventually avoided the problem. Today we are solving it directly: how to pan the page up more.
we can manually shift up and down our layout view to create the pan up effect
On any view, we can use the scrollTo
method to to shift the page up.
Now is just to calculate how much distance to shift the page up. Let say we want to bring the current focused editText up to 1/4 of the screen height, then a large space below the editText will be showing.
So the distance to scroll is:
After we scroll the distance, “Where I currently am” will be at the position of “Where I want to be”.
When we want to shift up, (may be detected from keyboard open or edittext gain focus) we call this handleShiftUp
method.
We can call this method anywhere, just make sure to pass in current focusedView and mChildOfContent is the root layout
absY
is “Where I currently am”
If absY
is already above 1/4 of the screen, there is no need to shift up anymore. But if it is below 1/4 of the screen, we call scrollTo
on the root layout to shift it up to 1/4.
public void handleShiftUp(View focusedView) {
if (focusedView == null){
Toast.makeText(activity, "focusedView is null", Toast.LENGTH_SHORT).show();
return;
}
int[] location = new int[2];
focusedView.getLocationInWindow(location);
int absY = location[1];
int oneFourth = totalScreenHeight/4;
if (absY > oneFourth){
int distanceToScroll = absY - oneFourth + currentlyScrolled;
currentlyScrolled = distanceToScroll;
mChildOfContent.scrollTo(0,distanceToScroll);
Toast.makeText(activity, "Shift up " + distanceToScroll, Toast.LENGTH_SHORT).show();
}
}
To shift down, scroll the root view back to 0
public void handleShiftDown() {
currentlyScrolled = 0;
mChildOfContent.scrollTo(0,0);
}
My own example here. If you don’t want 1/4, change it accordingly.
Problem: How to pan up more in full screen mode with AdjustResize
So who on earth need such requirement?
- You have a layout that is not resizable and you don’t want to wrap it inside a scrollView
- You need full screen mode
- You want it to work with adjustPan or adjustResize and with or without full screen mode
- You cannot change your requirement
-The project I am working on have all layout dynamically generated, I don’t have access to the view ids and all views are fixed distance in constraint layout.
- Boss want full screen mode.
- The pop up library I am using require adjustResize
to work correctly.
So eventually I created this. A combination of the AndroidBug5497Workaround with modified behavior to adjustPan
instead.
The reason I write these kind of articles is to potentially help anyone facing the same problem. If I can save you 1 minute, then this article is worthwhile to me