Skip to content

Instantly share code, notes, and snippets.

@OleksandrKucherenko
Last active May 2, 2021 18:50
Show Gist options
  • Save OleksandrKucherenko/61b98244529ea5764a188d2bd4e86139 to your computer and use it in GitHub Desktop.
Save OleksandrKucherenko/61b98244529ea5764a188d2bd4e86139 to your computer and use it in GitHub Desktop.
Helper class that help to track opening/close of virtual keyboard in activity/fragment.
// Author: Oleksandr Kucherenko, 2016-present
package your.package.android.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.*;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.common.base.Preconditions.checkNotNull;
/** Utility methods for manipulating the virtual keyboard visibility. */
@SuppressWarnings("UnusedReturnValue")
@MainThread
public class KeyboardHelper {
/** To speed-up execution, cache the reflected method. */
private static Method sVisibleHeight;
/** Stored height of the keyboard. */
private static final AtomicInteger sKbdHeight = new AtomicInteger();
/** Do we have part of the screen reserved for navigation bar or we have a hardware buttons for navigation. */
private static final AtomicBoolean sHasSoftNavigation = new AtomicBoolean();
/** Is Immersive already calculated and cached or not. */
private static final AtomicBoolean sCachedSoftNavigation = new AtomicBoolean();
/** Extract service instance from context. */
@Nullable
@SuppressWarnings("unchecked")
public static <T> T service(@NonNull final Context context, @NonNull final String name) {
return (T) context.getSystemService(name);
}
/** Show virtual keyboard on screen. */
public static boolean showKeyboard(@NonNull final Activity context) {
// Alternative:
// final Window window = context.getWindow();
// window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
final InputMethodManager im = get(context);
if (im != null) {
final IBinder token = context.getWindow().getDecorView().getWindowToken();
im.toggleSoftInputFromWindow(token, InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
return true;
}
return false;
}
/** Hide virtual keyboard if displayed one. */
public static boolean hideKeyboard(@NonNull final Activity context) {
final InputMethodManager im = get(context);
if (im != null) {
final IBinder token = context.getWindow().getDecorView().getWindowToken();
return im.hideSoftInputFromWindow(token, 0);
}
return false;
}
/** Show virtual keyboard on screen. */
public static boolean showKeyboard(@NonNull final View view, @NonNull Context context) {
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
return true;
}
return false;
}
/** Hide virtual keyboard on screen. */
public static boolean hideKeyboard(@NonNull final IBinder token, @NonNull Context context) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);
return true;
}
return false;
}
/** Is keyboard shown to user or not. */
public static boolean isShown(@NonNull final Context context) {
return getHeight(context) > 0;
}
/** Get Keyboard height. */
public static int getHeight(@NonNull final Context context) {
final InputMethodManager input = get(context);
if (null == input) return sKbdHeight.get();
try {
// target: API 16, v4.1, Jelly Bean
// method available from API 5.0.0
if (null == sVisibleHeight) {
final Class<?> clazz = input.getClass();
sVisibleHeight = clazz.getMethod("getInputMethodWindowVisibleHeight");
}
final int height = (int) sVisibleHeight.invoke(input);
sKbdHeight.set(height);
} catch (final Throwable ignored) {
}
return sKbdHeight.get();
}
/** Get input service instance. */
@Nullable
public static InputMethodManager get(@NonNull final Context context) {
return service(context, Context.INPUT_METHOD_SERVICE);
}
/** Create instance of keyboard height tracker for older APIs. */
@NonNull
public static HeightProvider tracker(@NonNull final Activity owner, @NonNull final View vwRoot) {
return new HeightProvider(owner, vwRoot);
}
/** Is navigation bar in hardware or software mode. */
@SuppressLint("NewApi")
public static boolean hasSoftwareNavigationBar(@NonNull final Context ctx) {
if (!sCachedSoftNavigation.getAndSet(true)) {
boolean result = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final Display d = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
final DisplayMetrics realDisplayMetrics = new DisplayMetrics();
final DisplayMetrics displayMetrics = new DisplayMetrics();
d.getRealMetrics(realDisplayMetrics);
d.getMetrics(displayMetrics);
final int realHeight = realDisplayMetrics.heightPixels;
final int realWidth = realDisplayMetrics.widthPixels;
final int displayHeight = displayMetrics.heightPixels;
final int displayWidth = displayMetrics.widthPixels;
// is part of the screen reserved for software navigation bar?!
result = (realWidth > displayWidth) || (realHeight > displayHeight);
}
sHasSoftNavigation.set(result);
}
return sHasSoftNavigation.get();
}
/**
* Registered window will track the keyboard and update static variable of the Keyboard Helper class.
*
* @see <a href="https://github.com/siebeprojects/samples-keyboardheight">Source of Idea</a>
*/
public static class HeightProvider extends PopupWindow {
/** Reference on parent/owner activity. */
private final Activity mActivity;
/** Reference on activity content root. */
private final View mParentView;
/** Reference on popup view/layout. */
private final View mPopupView;
/** Reference on listener. */
private final ViewTreeObserver.OnGlobalLayoutListener mListener;
/** callback reference. */
@Nullable
private Callbacks mCallback;
/** Major constructor. */
private HeightProvider(@NonNull final Activity owner, @NonNull final View vwRoot) {
super(owner);
mActivity = owner;
// setup the popup
setContentView(mPopupView = new LinearLayout(owner));
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
setWidth(0);
setHeight(WindowManager.LayoutParams.MATCH_PARENT);
mParentView = mActivity.findViewById(android.R.id.content);
mListener = this::measureKbdHeight;
mPopupView.getViewTreeObserver().addOnGlobalLayoutListener(mListener);
// schedule start of HeightProvider after onPostResume processing
vwRoot.post(this::start);
}
/** Measure keyboard height. */
/* package */ void measureKbdHeight() {
final Point screenSize = new Point();
mActivity.getWindowManager().getDefaultDisplay().getSize(screenSize);
final Rect rect = new Rect();
mPopupView.getWindowVisibleDisplayFrame(rect);
final int keyboardHeight = screenSize.y - rect.bottom;
final int oldHeight = sKbdHeight.getAndSet(keyboardHeight);
if (oldHeight != keyboardHeight && null != mCallback) {
mCallback.onKeyboardHeightChanged();
}
}
/** Assign callback. */
public void setCallback(@Nullable final Callbacks callbacks) {
mCallback = callbacks;
}
/** Enable popup window tracking. */
/* package */ void start() {
// its a delayed call, activity can be destroyed
if (mActivity.isFinishing()) {
close();
return;
}
setBackgroundDrawable(new ColorDrawable(0));
showAtLocation(mParentView, Gravity.NO_GRAVITY, 0, 0);
}
/** close popup window. */
public void close() {
mParentView.getViewTreeObserver().removeOnGlobalLayoutListener(mListener);
dismiss();
}
}
/** Callback for tracking keyboard height changes. */
public interface Callbacks {
/** Triggered when detected keyboard height change. */
void onKeyboardHeightChanged();
}
}
@OleksandrKucherenko
Copy link
Author

App styles:

values-v19/styles.xml:

    <!-- Full screen activities -->
    <style name="AppTheme.FullScreen" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- for older API only those flags enable transparency -->
        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
        <item name="android:windowTranslucentStatus">true</item>

        <!-- Make status bar transparent/translucent -->
        <item name="android:statusBarColor">@android:color/transparent</item>

        <!-- Force status bar icons gray color on white background (api23+) -->
        <item name="android:windowLightStatusBar" tools:targetApi="23">true</item>
</style>

values-v21/styles.xml:

    <!-- Full screen activities -->
    <style name="AppTheme.FullScreen" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Make status bar transparent/translucent -->
        <item name="android:statusBarColor">@android:color/transparent</item>

        <!-- Force status bar icons gray color on white background (api23+) -->
        <item name="android:windowLightStatusBar" tools:targetApi="23">true</item>
   </style>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment