Architecture of the Flick Input Controller and Popup View System in JapaneseKeyboard

The flick input controller and popup view system in JapaneseKeyboard employs a three-layer architecture that decouples gesture detection from visual feedback, allowing controllers to calculate flick directions via touch events while dedicated popup views handle the floating bubble UI.

The JapaneseKeyboard repository implements a sophisticated flick-based input mechanism for Japanese character entry on Android. Understanding the flick input controller and popup view system architecture reveals how gesture detection, state management, and visual feedback are separated into distinct layers to support multiple input modes including standard flicks, grid layouts, and custom angle detection.

Core Architecture Layers

The system is organized into three distinct layers that communicate through well-defined interfaces.

Listener and Model Layer

The foundation of the system is defined in FlickListener.kt located at core/src/main/java/com/kazumaproject/core/domain/listener/FlickListener.kt. This interface establishes the contract for flick events across the application.

UI components like TenKey register a FlickListener to receive callbacks via onFlick(gestureType, key, char?). The listener abstracts the input logic from the consumer, allowing any view to receive flick events without knowing the specifics of how gestures are detected or mapped to characters.

Controllers and Input Logic

Three specialized controllers interpret touch gestures, calculate directions using geometric operations, and drive the popup windows:

Each controller follows the same lifecycle: (a) register a touch listener on the key view via attach(view, map), (b) record the start point on ACTION_DOWN, (c) calculate direction on ACTION_MOVE, (d) update popup visuals, and (e) notify the registered FlickListener on ACTION_UP.

The visual feedback layer consists of specialized views and extension functions that render floating bubbles:

These views allow runtime theme changes and calculate their own dimensions, while controllers remain responsible only for instantiation and dismissal timing.

Flick Input Flow and Interaction

A typical interaction flows through the system as follows:

  1. Setup: A TenKey instance creates a controller and registers a FlickListener via setOnFlickListener. The controller receives a character map linking FlickDirection values to specific characters.

  2. Touch Down: The controller's attach(view, map) method stores the anchorView and displays an initial center popup using setPopUpWindowCenter or the standard flick popup view.

  3. Movement: As the finger moves, the controller calculates the FlickDirection using vector math (sqrt/atan2). It calls the appropriate popup extension (e.g., setPopUpWindowFlickRight) to reposition the window and updates the inner view's text via methods like setTextFlick* to reflect the character under the current direction.

  4. Release: On ACTION_UP, the controller determines the final direction, looks up the corresponding character from characterMap, invokes listener?.onFlick(finalDirection, character), and dismisses all PopupWindow instances.

Implementation Examples

Registering a Flick Listener on TenKey

val tenKey = findViewById<TenKey>(R.id.ten_key)

// Set the listener that receives flick events
tenKey.setOnFlickListener(object : FlickListener {
    override fun onFlick(gestureType: GestureType, key: Key, char: Char?) {
        // gestureType indicates Tap, FlickLeft, FlickTop, etc.
        // char is the character mapped to that direction
    }
})

// Optional: Enable visual flick guides
tenKey.setFlickGuideEnabled(true)

Relevant source: TenKey.setOnFlickListener and setFlickGuideEnabled in tenkey/src/main/java/com/kazumaproject/tenkey/TenKey.kt.

Attaching a Standard Flick Controller

val button = findViewById<AppCompatButton>(R.id.key_1)

// Map directions to characters
val flickMap = mapOf(
    FlickDirection.TAP to "あ",
    FlickDirection.UP to "ぁ",
    FlickDirection.DOWN to "あ",
    FlickDirection.LEFT to "か",
    FlickDirection.RIGHT to "が"
)

val flickCtrl = StandardFlickInputController(this)
flickCtrl.attach(button, flickMap)

flickCtrl.listener = object : StandardFlickInputController.StandardFlickListener {
    override fun onFlick(character: String) {
        // Insert character into text field
    }
}

Relevant source: StandardFlickInputController.attach in custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/controller/StandardFlickInputController.kt.

Using Popup Extensions Directly

val popup = PopupWindow(
    layout, 
    ViewGroup.LayoutParams.WRAP_CONTENT,
    ViewGroup.LayoutParams.WRAP_CONTENT, 
    false
)

val keyWindow = KeyWindowLayout(context)
popup.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())

// Position a right-flick bubble relative to anchorView
popup.setPopUpWindowFlickRight(context, keyWindow, anchorView)

Relevant source: Extension functions in tenkey/src/main/java/com/kazumaproject/tenkey/extensions/PopupWindowExtension.kt.

Implementing Long-Press Grid Flick

val keyView = findViewById<View>(R.id.key_5)

// Initialize with sensitivity threshold
val gridCtrl = GridFlickInputController(this, flickSensitivity = 80)

val charMap = mapOf(
    FlickDirection.TAP to "ん",
    FlickDirection.UP to "ん",
    FlickDirection.DOWN to "ゔ",
    FlickDirection.UP_LEFT_FAR to "ば",
    FlickDirection.UP_RIGHT_FAR to "ぱ"
)

gridCtrl.attach(keyView, charMap)

gridCtrl.listener = object : GridFlickInputController.GridFlickListener {
    override fun onFlick(character: String, isFlick: Boolean) {
        // isFlick distinguishes directional flicks from taps
    }
}

Relevant source: GridFlickInputController.attach and longPressJob handling in custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/controller/GridFlickInputController.kt.

Summary

  • The flick input controller and popup view system separates concerns into three layers: listener contracts, gesture controllers, and visual popup views.
  • Controllers like StandardFlickInputController use geometric calculations (sqrt/atan2) on touch events to determine FlickDirection while managing PopupWindow lifecycle.
  • The popup system provides specialized views (StandardFlickPopupView, FlickGridPopupView) and extension functions (setPopUpWindowFlickRight, etc.) for positioning and theming feedback bubbles.
  • This modular architecture allows independent replacement of gesture algorithms or visual styles without affecting the other layer.

Frequently Asked Questions

How does the controller determine which direction the user flicked?

The controller captures the touch start coordinates on ACTION_DOWN and calculates the delta on ACTION_MOVE using sqrt and atan2 to determine the angle and distance. If the distance exceeds a threshold, the angle maps to a FlickDirection enum value (such as UP, DOWN, LEFT, RIGHT, or diagonal variants).

Can I customize the appearance of the flick popup bubbles?

Yes. The popup visual style is controlled by classes like StandardFlickPopupView and DirectionalKeyPopupView, which extend Android's view system. You can modify the background shapes, text colors, and arrow directions in these files without changing the gesture detection logic in the controllers.

What is the difference between StandardFlickInputController and GridFlickInputController?

StandardFlickInputController handles single-direction flicks from a center point, showing one popup at a time. GridFlickInputController detects long-presses to display a 3×3 grid of candidates using FlickGridPopupView, allowing selection from multiple directions simultaneously. The grid controller manages multiple PopupWindow instances in a popupMap rather than a single window.

Where is the flick listener interface defined?

The FlickListener interface is defined in core/src/main/java/com/kazumaproject/core/domain/listener/FlickListener.kt. It declares the onFlick(gestureType, key, char?) method that all controllers invoke when a gesture completes, ensuring consistent communication between the input logic and UI components like TenKey.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →