#6 Floating Windows on Android: Keyboard Input
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.
- Floating Windows on Android 1: Jetpack Compose & Room
- Floating Windows on Android 2: Foreground Service
- Floating Windows on Android 3: Permissions
- Floating Windows on Android 4: Floating Window
- Floating Windows on Android 5: Moving Window
- Floating Windows on Android 6: Keyboard Input
- Floating Windows on Android 7: Boot Receiver
- Floating Windows on Android 8: The Final App
- Floating Windows on Android 9: Shortcomings
- Floating Windows on Android 10: Tips & Tricks
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