Skip to content

Instantly share code, notes, and snippets.

@OleksandrKucherenko
Last active May 2, 2021 18:50
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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

Usage:

    /** Special helper that allows detection of keyboard height for old API levels. */
    private KeyboardHelper.HeightProvider mKbdHeightTracker;
    /** True - if UI has ongoing animation, otherwise False. */
    /* package */ final AtomicBoolean mInAnimation = new AtomicBoolean();
    /** Reference on attached listener. */
    private View.OnLayoutChangeListener mOnLayoutListener;

    /** {@inheritDoc} */
    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
       // ....

        // listen to keyboard open/close
        getWindow().getDecorView().addOnLayoutChangeListener(mOnLayoutListener = onDecorLayoutChange());

        // enable detection of keyboard height changes for older API
        mKbdHeightTracker = KeyboardHelper.tracker(this, view /* root view of the activity */);
        mKbdHeightTracker.setCallback(this::followTheShownKeyboard);
   }

    /** {@inheritDoc} */
    @Override
    protected void onPostResume() {
        super.onPostResume();

        // presenter.resume();

        // force refresh of the position
        followTheShownKeyboard();
    }

    /** {@inheritDoc} */
    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (null != mKbdHeightTracker) {
            mKbdHeightTracker.close();
            mKbdHeightTracker = null;

            getWindow().getDecorView().removeOnLayoutChangeListener(mOnLayoutListener);
            mOnLayoutListener = null;
        }

        // presenter.destroy();
    }

    /** capture layout change event and force recalculation of UI elements position on screen. */
    @NonNull
    private View.OnLayoutChangeListener onDecorLayoutChange() {
        return (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
            followTheShownKeyboard();
        };
    }

    /** recalculate the position of the Next button on screen. */
    /* package */ boolean followTheShownKeyboard() {
        final float lastKnown = mKbdHeight;

        // save the keyboard size, for animations that can be 'in progress' now
        mKbdHeight = KeyboardHelper.getHeight(this);

        final boolean isShown = KeyboardHelper.isShown(this);

        if (!mInAnimation.get() && lastKnown != mKbdHeight) {
            // any change of keyboard height should be animated properly

            /* place for own keyboard open/close logic */
        }

        return KeyboardHelper.isShown(this);
    }

@OleksandrKucherenko
Copy link
Author

Inside AndroidManifest.xml should be also applied changes:

        <!-- Login Activity, no automatic keyboard showing. adjustResize required for tracking keyboard open/close. -->
        <activity
            android:name=".LoginActivity"
            android:launchMode="singleTask"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="stateHidden|adjustResize"
            >
                <!-- ... -->
       </activity>

stateHidden - force keyboard closing on activity start

On activity start we also should force a "full screen" mode:

    /** Enable full-screen with half-transparent status bar for activity. */
    public static void requestFullScreen(@NonNull final Activity activity) {
        // NOTES (olku): keep in mind that we apply tricks after the theme is applied.
        // AppTheme.FullScreen 

        final Window window = activity.getWindow();
        final View decorView = window.getDecorView();

        // force full screen
        window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

        // https://gist.github.com/chrisbanes/73de18faffca571f7292
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
            // layout can be shown under the status bar (https://developer.android.com/training/system-ui/status.html)
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() |
                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                    View.SYSTEM_UI_FLAG_FULLSCREEN |
                    View.SYSTEM_UI_FLAG_LOW_PROFILE /* hidden status bar */
            );
        }

        // api 21+, but less api 23
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            window.setStatusBarColor(Color.WHITE);
            // FIXME (olku): seems nothing above working for us !
        }

        darkTrayIcons(activity);
    }

    /** Make tray icons of dark color for contrast on light background. */
    @MainThread
    public static void darkTrayIcons(@NonNull final Activity activity) {
        final Window window = activity.getWindow();
        final View decorView = window.getDecorView();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            final int uiColor = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            final int newVisibility = decorView.getSystemUiVisibility() | uiColor;
            decorView.setSystemUiVisibility(newVisibility);
        }
    }

So don't forget to call requestFullScreen(activity) inside onCreate method

@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