#6 Floating Windows on Android: Keyboard Input

Václav HodekVáclav Hodek
5 min read

Have you ever wondered how to make those floating windows used by Facebook Heads and other apps? Have you ever wanted to use the same technology in your app? It’s easy, and I will guide you through the whole process.

I'm the author of Floating Apps; the first app of its kind on Google Play and the most popular one with over 8 million downloads. After 6 years of the development of the app, I know a bit about it. It’s sometimes tricky, and I spent months reading documentation and Android source code and experimenting. I received feedback from tens of thousands of users and see various issues on different phones with different Android versions.

Here's what I learned along the way.

Before reading this article, it's recommended to go through Floating Windows on Android 5: Moving Window.

In this article, I will teach you how to allow keyboard input in the floating window.

Almost There

In the previous articles, we created the main app, foreground service, and the floating window. We can even move the window around the screen. However, it’s still not possible to use it as the soft keyboard is not triggered, so it’s impossible to write any note.

Let's solve this last issue so we can wrap everything nicely together to get the fully functional app in the next articles.

What's The Problem?

You may remember that in the fourth article about the floating window, we add a specific flag to LayoutParams for our window:

  • FLAG_NOT_FOCUSABLE - The window won't ever get key input focus, so the user can not send key or other button events to it. Those will instead go to whatever focusable window is behind it.

The description is pretty clear. When this flag is set, our window is not going to receive any keyboard inputs. However, when this flag is not set, the floating window interfere with the apps behind it, and make it impossible to use keyboard for them.

The Solution

Obviously, the solution is not that hard to find. We just need to enable and disable this flag at the right time.

Changing the flag is a pretty simple task. Here it goes:

private fun enableKeyboard() {  
  if (windowParams.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE != 0) {  
    windowParams.flags = windowParams.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv()  
    update()  
  }  
}  

private fun disableKeyboard() {  
  if (windowParams.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE == 0) {  
    windowParams.flags = windowParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE  
    update()  
  }  
}

When Enable/Disable?

The logic is straightforward. When the window is activated - the user clicks on it - we should enable the keyboard input. When the user clicks outside of the window, we should disable it. And that's it.

Universal Solution

For a universal solution across different window layouts, we subclass LinearLayout to create a container for our window.

Fortunately, we set another flag to LayoutParams:

  • FLAG_WATCH_OUTSIDE_TOUCH - Receive events for touches that occur outside of your window.

So, with a custom ViewContainer we can monitor clicks both inside and outside the window. The full source code for such container is here:

class WindowContentLayout : LinearLayout {  

  constructor(context: Context) : super(context)  
  constructor(context: Context, attrs: AttributeSet) : super(context, attrs)  
  constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)  

  private var listener: ((activate: Boolean) -> Unit)? = null  

  fun setListener(listener: (activate: Boolean) -> Unit) {  
    this.listener = listener  
  }  

  override fun onTouchEvent(event: MotionEvent): Boolean {  
    if (event.action == MotionEvent.ACTION_DOWN) listener?.invoke(true)  
    if (event.action == MotionEvent.ACTION_OUTSIDE) listener?.invoke(false)  
    return super.onTouchEvent(event)  
  }  

  override fun onInterceptTouchEvent(event: MotionEvent): Boolean  {  
    if (event.action == MotionEvent.ACTION_DOWN) listener?.invoke(true)  
    if (event.action == MotionEvent.ACTION_OUTSIDE) listener?.invoke(false)  
    return super.onInterceptTouchEvent(event)  
  }  

}

Now, we have to use our subclassed LinearLayout in the window.xml - the layout for our window:

<com.localazy.quicknote.windows.WindowContentLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:weightSum="1">

    <!-- The content of the window omitted for brevity. -->

</com.localazy.quicknote.windows.WindowContentLayout>

And as we know that the root element is always our WindowContentLayout, we can change the line where rootView is inflated to:

private val rootView = layoutInflater.inflate(R.layout.window, null) as WindowContentLayout

With rootView being WindowContentLayout, we can react to the events and enable/disable the soft keyboard support as necessary:

rootView.setListener {  
  if (it) {  
    enableKeyboard()  
  } else {  
    disableKeyboard()  
  }  
}

All events sent to the window are intercepted, and so it's a fully universal solution working for all windows with any content.

Results

Smooth! We can use the keyboard both for the normal app running behind our window as well as in our floating note.

Source Code

The whole source code for this article is available on Github.

The Series

This article is part of the Floating Windows on Android series.

13
Subscribe to my newsletter

Read articles from Václav Hodek directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Václav Hodek
Václav Hodek