Difference Between StandardFlickInputController and CustomAngleFlickController in Japanese Keyboard

The StandardFlickInputController implements a fixed four-direction flick model with hard-coded angle thresholds, while the CustomAngleFlickController supports fully configurable angular ranges, multiple candidate maps, and long-press UI modes for complex hierarchical input.

In the kazumaproject/japanesekeyboard repository, both StandardFlickInputController and CustomAngleFlickController handle finger-drag gestures to enable flick-based character input on Android. While they share the same high-level goal of translating touch events into directional selections, they differ fundamentally in gesture detection logic, state management, and visual feedback capabilities. Understanding the architectural difference between StandardFlickInputController and CustomAngleFlickController is essential for building keyboard layouts that range from simple hiragana keys to dynamic emoji palettes.

Gesture Detection and Direction Resolution

Fixed Four-Direction Model in StandardFlickInputController

Located in StandardFlickInputController.kt, this controller enforces a rigid directional model optimized for classic Japanese keyboard layouts. It uses a hard-coded distance threshold of 65f pixels (flickThreshold) to distinguish taps from flicks. Direction resolution occurs in calculateDirection(), which maps touch angles to fixed sectors: -45° to 45° yields UP_RIGHT_FAR, 45° to 135° yields DOWN, and so on. This deterministic approach assumes exactly four flick directions per key.

Configurable Angular Ranges in CustomAngleFlickController

Found in CustomAngleFlickController.kt, this controller accepts dynamic angular mappings through setFlickRanges(). Each FlickDirection maps to a Pair<start°, span°>, enabling non-standard sectors such as octagonal or irregular layouts. While it uses the same distance-based threshold (passed as flickSensitivity in the constructor), it delegates angle-to-direction resolution to CustomAngleFlickPopupView.getDirectionForAngle(), allowing per-key customization of gesture zones.

UI Components and Visual Feedback

StandardFlickPopupView Rendering

The standard controller renders a circular StandardFlickPopupView positioned above the anchor view via showPopup(). This view displays only the character for the currently detected direction (or all characters for a tap) and offers minimal runtime styling through setPopupColors(FlickPopupColorTheme).

CustomAngleFlickPopupView with Full UI Mode

The custom controller instantiates CustomAngleFlickPopupView, which supports adjustable geometry via setPopupViewSize(orbit, centerRadius, textSize). It uniquely supports a "full UI" mode that displays the complete candidate set, triggered after a long-press via a coroutine (longPressJob) running on CoroutineScope(Dispatchers.Main + SupervisorJob()). This allows users to preview all mapping options before flicking, essential for complex inputs like emoji groups.

Map Handling and State Management

Single Map vs. Multiple Map Support

StandardFlickInputController holds a single characterMap of type Map<FlickDirection, String> with no support for dynamic switching. In contrast, CustomAngleFlickController maintains a keyMaps list (List<Map<FlickDirection, String>>). When the user flicks UP_RIGHT, the controller increments the map index and invokes setFullUIMode(true), cycling to the next candidate set. This enables hierarchical flick selections where the same physical key provides access to distinct character groups.

Lifecycle and Long-Press Behavior

The standard controller resolves input immediately upon ACTION_UP and provides no long-press handling; its cancel() method simply dismisses the popup. The custom controller manages complex state through coroutines, triggering full-UI mode after ViewConfiguration.getLongPressTimeout(). Its cancel() method terminates the SupervisorJob(), ensuring proper cleanup of background operations when keys are released or views are recycled.

Usage in FlickKeyboardView

In FlickKeyboardView.attachKeyBehavior(), the instantiation logic depends on KeyType:

  • KeyType.STANDARD_FLICK instantiates StandardFlickInputController for keys requiring deterministic four-direction input.
  • KeyType.CIRCULAR_FLICK instantiates CustomAngleFlickController, subsequently configuring theme colors via setPopupColors() and angular ranges via setFlickRanges().

Both controllers follow the same contract of receiving a View anchor and dispatching callbacks to FlickKeyboardView, but diverge precisely where customization is required.

Code Implementation Examples

Standard Flick Implementation

// Minimal setup for hiragana input
val keyView: View = /* key button */
val flickMap = mapOf(
    FlickDirection.TAP to "あ",
    FlickDirection.UP to "い",
    FlickDirection.DOWN to "う",
    FlickDirection.UP_RIGHT_FAR to "え",
    FlickDirection.UP_LEFT_FAR to "お"
)

StandardFlickInputController(context).apply {
    listener = object : StandardFlickInputController.StandardFlickListener {
        override fun onFlick(character: String) {
            keyboardListener.onKey(character, isFlick = true)
        }
    }
    attach(keyView, flickMap, SegmentedBackgroundDrawable(...))
}

Custom Angle Flick Implementation

// Advanced setup for emoji groups with multiple maps
val emojiMaps = listOf(
    mapOf(FlickDirection.UP to "😀", FlickDirection.DOWN to "😁"),
    mapOf(FlickDirection.UP to "😎", FlickDirection.DOWN to "🤩"),
    mapOf(FlickDirection.UP to "🧐", FlickDirection.DOWN to "🤓")
)

val customRanges = mapOf(
    FlickDirection.UP to Pair(225f, 90f),
    FlickDirection.UP_RIGHT_FAR to Pair(315f, 90f),
    FlickDirection.DOWN to Pair(45f, 90f),
    FlickDirection.UP_LEFT_FAR to Pair(135f, 90f)
)

CustomAngleFlickController(context, flickSensitivity = 80).apply {
    setPopupColors(FlickPopupColorTheme(/* colors */))
    setPopupViewSize(orbit = 170f, centerRadius = 64f, textSize = 55f)
    setFlickRanges(customRanges)

    listener = object : CustomAngleFlickController.FlickListener {
        override fun onFlick(direction: FlickDirection, character: String) {
            keyboardListener.onKey(character, isFlick = direction != FlickDirection.TAP)
        }
        override fun onStateChanged(view: View, newMap: Map<FlickDirection, String>) {
            // Handle map transition UI
        }
        override fun onFlickDirectionChanged(newDirection: FlickDirection) {
            // Update directional indicators
        }
    }
    attach(keyView, emojiMaps)
}

Summary

  • Gesture Model: Standard uses fixed four-direction angles; Custom supports configurable angular ranges via setFlickRanges().
  • Map Support: Standard binds a single characterMap; Custom maintains a keyMaps list for hierarchical input.
  • Visual Feedback: Standard shows minimal popup UI; Custom supports adjustable orbit sizes and full-UI mode after long-press.
  • Lifecycle: Standard has immediate ACTION_UP resolution; Custom uses CoroutineScope with longPressJob for delayed state transitions.
  • Use Case: Standard suffices for hiragana/katakana keys; Custom is required for emoji palettes and complex symbol inputs.

Frequently Asked Questions

Which controller should I use for a standard Japanese hiragana layout?

Use StandardFlickInputController defined in StandardFlickInputController.kt. It provides the exact four-direction model (up, down, up-right, up-left) required for standard Japanese flick input without the overhead of configurable ranges or coroutine-based state management.

How does CustomAngleFlickController handle multiple character sets?

It stores candidates in a keyMaps: List<Map<FlickDirection, String>> property. When the user flicks UP_RIGHT, the controller increments the internal map index and calls onStateChanged() on its listener, cycling to the next candidate set while optionally displaying the full UI to preview all available characters.

Can I modify the angle thresholds in StandardFlickInputController?

No. StandardFlickInputController uses hard-coded angle boundaries in calculateDirection() (e.g., -45° to 45° for UP_RIGHT_FAR) and a fixed flickThreshold of 65f pixels. To implement custom angular zones, you must use CustomAngleFlickController and supply ranges via setFlickRanges().

What triggers the long-press mode in CustomAngleFlickController?

A coroutine launched in onTouchEvent() waits for ViewConfiguration.getLongPressTimeout() milliseconds. Upon completion, it invokes setFullUIMode(true) on the CustomAngleFlickPopupView, expanding the popup to display all candidates in the current map and allowing the user to preview options before committing to a flick direction.

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 →