Layout Definition & Switching
Overview
FlorisBoard uses a flexible JSON-based layout system that allows dynamic loading, caching, and switching between different keyboard layouts for various languages and input modes.
Introduction
The layout system is one of FlorisBoard's most powerful features, enabling support for 100+ keyboard layouts across multiple languages. Layouts are defined in JSON files, loaded asynchronously, cached for performance, and can be extended through the extension system.
Key Concepts
Layout Types
FlorisBoard supports multiple layout types for different input scenarios:
enum class LayoutType(val id: String) {
CHARACTERS(LayoutTypeId.CHARACTERS), // Main character layout (QWERTY, AZERTY, etc.)
CHARACTERS_MOD(LayoutTypeId.CHARACTERS_MOD), // Modifier row for character layout
EXTENSION(LayoutTypeId.EXTENSION), // Extension layouts
NUMERIC(LayoutTypeId.NUMERIC), // Numeric keypad
NUMERIC_ADVANCED(LayoutTypeId.NUMERIC_ADVANCED), // Advanced numeric with symbols
NUMERIC_ROW(LayoutTypeId.NUMERIC_ROW), // Number row above main layout
PHONE(LayoutTypeId.PHONE), // Phone number pad
PHONE2(LayoutTypeId.PHONE2), // Alternative phone layout
SYMBOLS(LayoutTypeId.SYMBOLS), // Symbol layout
SYMBOLS_MOD(LayoutTypeId.SYMBOLS_MOD), // Modifier row for symbols
SYMBOLS2(LayoutTypeId.SYMBOLS2), // Secondary symbol layout
SYMBOLS2_MOD(LayoutTypeId.SYMBOLS2_MOD); // Modifier for secondary symbols
}
Layout Arrangement
A layout arrangement is a 2D array of key definitions:
typealias LayoutArrangement = List<List<AbstractKeyData>>
Each layout is organized as:
- Rows: Horizontal rows of keys
- Keys: Individual key definitions with properties like code, label, type, popup keys
LayoutManager
The LayoutManager
is responsible for:
- Loading: Asynchronously loading layouts from JSON files
- Caching: Caching loaded layouts to avoid repeated parsing
- Merging: Combining main, modifier, and extension layouts
- Popup Mappings: Loading long-press popup key mappings
class LayoutManager(context: Context) {
private val layoutCache: HashMap<LTN, DeferredResult<CachedLayout>> = hashMapOf()
private val layoutCacheGuard: Mutex = Mutex(locked = false)
private val popupMappingCache: HashMap<ExtensionComponentName, DeferredResult<CachedPopupMapping>> = hashMapOf()
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun computeKeyboardAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
): Deferred<TextKeyboard>
}
Layout Components
@Serializable
data class LayoutArrangementComponent(
override val id: String,
override val label: String,
override val authors: List<String>,
val direction: String, // "ltr" or "rtl"
val modifier: ExtensionComponentName? = null, // Optional modifier layout
val arrangementFile: String? = null, // Path to layout JSON
) : ExtensionComponent {
fun arrangementFile(type: LayoutType) = arrangementFile ?: "layouts/${type.id}/$id.json"
}
Implementation Details
Layout JSON Structure
Basic Character Layout
[
[
{ "$": "auto_text_key", "code": 113, "label": "q" },
{ "$": "auto_text_key", "code": 100, "label": "d" },
{ "$": "auto_text_key", "code": 114, "label": "r" },
{ "$": "auto_text_key", "code": 119, "label": "w" },
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 106, "label": "j" },
{ "$": "auto_text_key", "code": 102, "label": "f" },
{ "$": "auto_text_key", "code": 117, "label": "u" },
{ "$": "auto_text_key", "code": 112, "label": "p" }
],
...
]
Modifier Layout
[
[
{ "code": -11, "label": "shift", "type": "modifier" },
{ "code": 0, "type": "placeholder" },
{ "code": -7, "label": "delete", "type": "enter_editing" }
],
[
{ "code": -202, "label": "view_symbols", "type": "system_gui" },
{ "$": "variation_selector",
"default": { "code": 44, "label": ",", "groupId": 1 },
"email": { "code": 64, "label": "@", "groupId": 1 },
"uri": { "code": 47, "label": "/", "groupId": 1 }
},
{ "code": -227, "label": "language_switch", "type": "system_gui" },
{ "code": -212, "label": "ime_ui_mode_media", "type": "system_gui" },
{ "code": 32, "label": "space" },
{ "code": 46, "label": ".", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
]
]
Layout Loading Process
private fun loadLayoutAsync(ltn: LTN?, allowNullLTN: Boolean) = ioScope.runCatchingAsync {
if (ltn == null) return@runCatchingAsync null
layoutCacheGuard.withLock {
val cached = layoutCache[ltn]
if (cached != null) {
flogDebug(LogTopic.LAYOUT_MANAGER) { "Using cache for '${ltn.name}'" }
return@withLock cached
} else {
flogDebug(LogTopic.LAYOUT_MANAGER) { "Loading '${ltn.name}'" }
val meta = keyboardManager.resources.layouts.value?.get(ltn.type)?.get(ltn.name)
?: error("No indexed entry found for ${ltn.type} - ${ltn.name}")
val ext = extensionManager.getExtensionById(ltn.name.extensionId)
?: error("Extension ${ltn.name.extensionId} not found")
val path = meta.arrangementFile(ltn.type)
val layout = async {
runCatching {
val jsonStr = ZipUtils.readFileFromArchive(appContext, ext.sourceRef!!, path).getOrThrow()
val arrangement = loadJsonAsset<LayoutArrangement>(jsonStr).getOrThrow()
CachedLayout(ltn.type, ltn.name, meta, arrangement)
}
}
layoutCache[ltn] = layout
return@withLock layout
}
}.await().getOrThrow()
}
Layout Merging
FlorisBoard merges three types of layouts:
- Main Layout: The primary character/symbol layout
- Modifier Layout: Bottom row with space, enter, mode switches
- Extension Layout: Optional number row or other extensions
fun computeKeyboardAsync(
keyboardMode: KeyboardMode,
subtype: Subtype,
): Deferred<TextKeyboard> = ioScope.async {
var main: LTN? = null
var modifier: LTN? = null
var extension: LTN? = null
when (keyboardMode) {
KeyboardMode.CHARACTERS -> {
if (prefs.keyboard.numberRow.get()) {
extension = LTN(LayoutType.NUMERIC_ROW, subtype.layoutMap.numericRow)
}
main = LTN(LayoutType.CHARACTERS, subtype.layoutMap.characters)
modifier = LTN(LayoutType.CHARACTERS_MOD, extCoreLayout("default"))
}
KeyboardMode.SYMBOLS -> {
extension = LTN(LayoutType.NUMERIC_ROW, subtype.layoutMap.numericRow)
main = LTN(LayoutType.SYMBOLS, subtype.layoutMap.symbols)
modifier = LTN(LayoutType.SYMBOLS_MOD, extCoreLayout("default"))
}
// ... other modes
}
return@async mergeLayouts(keyboardMode, subtype, main, modifier, extension)
}
Extension Metadata
{
"$": "ime.extension.keyboard",
"meta": {
"id": "org.florisboard.layouts",
"version": "0.1.0",
"title": "Default layouts",
"description": "Default layouts which are always available.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"license": "apache-2.0"
},
"layouts": {
"characters": [
{
"id": "arabic",
"label": "Arabic",
"authors": [ "HeiWiper" ],
"direction": "rtl",
"modifier": "org.florisboard.layouts:arabic"
},
{
"id": "qwerty",
"label": "QWERTY",
"authors": [ "patrickgold" ],
"direction": "ltr"
}
]
}
}
Code Examples
Creating a Custom Layout
[
[
{ "$": "auto_text_key", "code": 97, "label": "a", "popup": {
"relevant": [
{ "code": 224, "label": "à" },
{ "code": 225, "label": "á" },
{ "code": 226, "label": "â" }
]
}},
{ "$": "auto_text_key", "code": 98, "label": "b" },
{ "$": "auto_text_key", "code": 99, "label": "c" }
],
[
{ "code": 32, "label": "space" },
{ "code": 10, "label": "enter", "type": "enter_editing" }
]
]
Key Types and Special Keys
{
"code": -11,
"label": "shift",
"type": "modifier"
}
{
"code": -7,
"label": "delete",
"type": "enter_editing"
}
{
"code": -202,
"label": "view_symbols",
"type": "system_gui"
}
{
"code": 0,
"type": "placeholder"
}
Conditional Keys (Selectors)
{
"$": "case_selector",
"lower": { "code": 59, "label": ";" },
"upper": { "code": 58, "label": ":" }
}
{
"$": "variation_selector",
"default": { "code": 44, "label": "," },
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" }
}
Japanese Kana Layout
{
"$": "kana_selector",
"hira": { "code": 12396, "label": "ぬ" },
"kata": { "$": "char_width_selector",
"full": { "code": 12492, "label": "ヌ" },
"half": { "code": 65415, "label": "ヌ" }
}
}
Best Practices
1. Use Selectors for Context-Aware Keys
{
"$": "variation_selector",
"default": { "code": 46, "label": "." },
"email": { "code": 64, "label": "@" },
"uri": { "code": 47, "label": "/" }
}
2. Organize Popup Keys Logically
{
"code": 101,
"label": "e",
"popup": {
"relevant": [
{ "code": 232, "label": "è" },
{ "code": 233, "label": "é" },
{ "code": 234, "label": "ê" },
{ "code": 235, "label": "ë" }
]
}
}
3. Use Placeholders for Flexible Layouts
{ "code": 0, "type": "placeholder" }
Placeholders expand to fill available space, allowing flexible key sizing.
4. Specify Direction for RTL Languages
{
"id": "arabic",
"label": "Arabic",
"direction": "rtl",
"modifier": "org.florisboard.layouts:arabic"
}
5. Group Related Keys
{ "code": 44, "label": ",", "groupId": 1 },
{ "code": 46, "label": ".", "groupId": 2 },
{ "code": 10, "label": "enter", "groupId": 3, "type": "enter_editing" }
Groups control key sizing and spacing.
Common Patterns
Multi-Language Support
val subtype = Subtype(
id = System.currentTimeMillis(),
primaryLocale = FlorisLocale.from("fr", "FR"),
layoutMap = SubtypeLayoutMap(
characters = extCoreLayout("french/azerty"),
symbols = extCoreLayout("symbols"),
numeric = extCoreLayout("numeric")
)
)
Dynamic Layout Switching
keyboardManager.activeState.keyboardMode = when (editorInfo.inputType) {
InputType.TYPE_CLASS_NUMBER -> KeyboardMode.NUMERIC
InputType.TYPE_CLASS_PHONE -> KeyboardMode.PHONE
else -> KeyboardMode.CHARACTERS
}
Custom Modifier Layouts
{
"id": "custom_layout",
"label": "Custom",
"direction": "ltr",
"modifier": "org.florisboard.layouts:custom_modifier"
}
Troubleshooting
Layout Not Loading
Problem: Custom layout doesn't appear in keyboard.
Solutions:
- Verify JSON syntax is valid
- Check extension.json includes layout metadata
- Ensure layout file is in correct directory
- Verify extension is properly installed
- Check logs for parsing errors
Keys Not Displaying Correctly
Problem: Keys show wrong characters or labels.
Solutions:
- Verify Unicode code points are correct
- Check font supports the characters
- Ensure proper encoding (UTF-8)
- Test with different themes
Popup Keys Not Working
Problem: Long-press doesn't show popup keys.
Solutions:
- Verify popup mapping is loaded
- Check popup key definitions in JSON
- Ensure long-press duration is configured
- Verify key has popup property
Layout Caching Issues
Problem: Layout changes don't appear after modification.
Solutions:
- Clear app cache
- Force reload extension
- Restart keyboard service
- Check cache invalidation logic
Related Topics
- Internationalization - Multi-language support
- Extension System - Creating layout extensions
- Custom UI Components - Rendering layouts
- Input Processing - How key presses are handled
Next Steps
- Explore existing layouts
- Contribute layouts to the FlorisBoard repository
- Check the FlorisBoard documentation for upcoming guides on custom layouts and popup mappings
Note: This documentation is continuously being improved. Contributions are welcome!