Skip to content
CleverKeys Wiki
implemented v1.5.0 User guide

Appearance Settings Technical Specification

Source Location Reference

FactSource FileLine(s)Value
Default themeConfig.kt21THEME = "cleverkeysdark"
Portrait height defaultConfig.kt24KEYBOARD_HEIGHT_PORTRAIT = 27 (percent of screen height)
Landscape height defaultConfig.kt25KEYBOARD_HEIGHT_LANDSCAPE = 40
Border defaultsConfig.kt43-45BORDER_CONFIG = false, CUSTOM_BORDER_RADIUS = 0, CUSTOM_BORDER_LINE_WIDTH = 0
Preference readsConfig.kt743-839height, margins, opacities, border, label sizes
Row height + marginsTheme.kt294-308Theme.Computed.init
Per-key paintsTheme.kt362-390Theme.Computed.Key.init
Key frame drawingKeyboard2View.kt1729-1738drawKeyFrame
Settings UI sectionui/settings/sections/AppearanceSection.ktwhole fileevery control listed below

Overview

The appearance system manages keyboard dimensions, margins, key opacity/border styling and label sizing. It has three layers:

  1. Config reads the raw preferences and converts them into render-ready units — percentages become pixels, 0-100 opacities become 0-255 alphas, dp values become px.
  2. Theme.Computed derives per-layout geometry (row height, key margins) and builds the Paint objects for each key role from the active theme plus those Config values.
  3. Keyboard2View draws with them.

There is no separate dimension calculator, animation controller, key-popup view or prediction-bar view — those are the responsibility of the three types above plus SuggestionBar.

Key Components

ComponentFilePurpose
Keyboard2ViewKeyboard2View.ktMain rendering (key frames, labels, swipe trail)
Theme.ComputedTheme.ktRow/key geometry and per-role Paint construction
ConfigConfig.ktReads appearance preferences, converts to render units
AppearanceSectionui/settings/sections/AppearanceSection.ktThe Settings UI

Dimension Calculation

Height

Keyboard height is a percentage of screen height, not a preset enum. Config picks the portrait or landscape key by orientation (and the _unfolded variant on a foldable that reports itself unfolded):

// Config.kt:746-760
if (orientation_landscape) {
    keyboardHeightPercent = safeGetInt(
        _prefs,
        if (this.foldable_unfolded) "keyboard_height_landscape_unfolded" else "keyboard_height_landscape",
        Defaults.KEYBOARD_HEIGHT_LANDSCAPE
    )
    characterSizeScale = 1.25f
} else {
    keyboardHeightPercent = safeGetInt(
        _prefs,
        if (this.foldable_unfolded) "keyboard_height_unfolded" else "keyboard_height",
        Defaults.KEYBOARD_HEIGHT_PORTRAIT
    )
}

Row height then divides that by a fixed 3.95 layout-unit constant, so a layout with more rows makes the keyboard taller rather than squashing the rows — except for numeric layouts with scale_numpad_height on and explicit XML numpad_height="true", which divide by the layout’s own height so the rows stretch to fill (#58). Omitting the bottom row does not opt a compact custom layout into numeric scaling (#90):

// Theme.kt:291-303
val heightDivisor = if (config.scale_numpad_height && layout.numpad_height) {
    layout.keysHeight
} else {
    3.95f
}
row_height = min(
    config.screenHeightPixels * config.keyboardHeightPercent / 100 / heightDivisor,
    config.screenHeightPixels / layout.keysHeight
)

Margins

Outer margins are percentages of the screen dimension, resolved per orientation with the same _portrait/_landscape/_unfolded suffix scheme (Config.get_percent_pref_oriented_width and ..._height, Config.kt:1190-1224). Bottom margins clamp to 0-30%, left/right to 0-45% each; the Settings sliders additionally cap left+right at 90% combined so the keyboard can never be squeezed to nothing.

Per-key margins are separate and multiplicative against the computed cell:

// Theme.kt:303-308
vertical_margin = config.key_vertical_margin * row_height
horizontal_margin = config.key_horizontal_margin * keyWidth
margin_top = config.marginTop + vertical_margin / 2
margin_left = horizontal_margin / 2

Key Rendering

Key frame

Every key is a rounded rect plus an optional border, from a per-role Theme.Computed.Key:

// Keyboard2View.kt:1729-1738
private fun drawKeyFrame(canvas: Canvas, x: Float, y: Float, keyW: Float, keyH: Float, tc: Theme.Computed.Key) {
    val r = tc.border_radius
    val w = tc.border_width
    val padding = w / 2f
    _tmpRect.set(x + padding, y + padding, x + keyW - padding, y + keyH - padding)
    canvas.drawRoundRect(_tmpRect, r, r, tc.bg_paint)
    if (w > 0f) {
        canvas.drawRoundRect(_tmpRect, r, r, tc.border_paint)
    }
}

There is no key-shape preference. The corner radius comes from the theme unless border_config is on, in which case both radius and line width come from the two custom sliders — the radius being a fraction of key width (the stored 0-100 int is divided by 100 at Config.kt:834), not a dp value:

// Theme.kt:369-377
if (config.borderConfig) {
    border_radius = config.customBorderRadius * keyWidth
    border_width = config.customBorderLineWidth
} else {
    border_radius = theme.keyBorderRadius
    border_width = if (activated) theme.keyBorderWidthActivated else theme.keyBorderWidth
}

bg_paint.alpha = if (activated) config.keyActivatedOpacity else config.keyOpacity

Theme.Computed.roleOf (Keyboard2View.kt:1584) selects one of five prebuilt frames — NORMAL, ACTIVATED, LOCKED, MODIFIER, SPECIAL — from the key’s kind plus pointer state.

Press feedback

Press feedback is a frame swap, not an animation: a held key renders with the ACTIVATED frame, whose background alpha is key_activated_opacity (default 80%, deliberately below the 100% at-rest key_opacity so the state change reads as press feedback rather than a pure colour shift — see the KEY_ACTIVATED_OPACITY comment at Config.kt:29-32). There is no scale/highlight animation setting and no key-preview popup above the key.

Opacity and label brightness

The four 0-100 percentage preferences are converted to 0-255 alpha once, at read time:

// Config.kt:828-831
labelBrightness = safeGetInt(_prefs, "label_brightness", Defaults.LABEL_BRIGHTNESS) * 255 / 100
keyboardOpacity = safeGetInt(_prefs, "keyboard_opacity", Defaults.KEYBOARD_OPACITY) * 255 / 100
keyOpacity = safeGetInt(_prefs, "key_opacity", Defaults.KEY_OPACITY) * 255 / 100
keyActivatedOpacity = safeGetInt(_prefs, "key_activated_opacity", Defaults.KEY_ACTIVATED_OPACITY) * 255 / 100

keyboardOpacity is applied to the view background (Keyboard2View.kt:1548); the other three land on the per-role paints (Theme.kt:377, Theme.kt:389), with label brightness packed into the label paint’s alpha bits so it multiplies every label colour uniformly.

Prediction Bar

The suggestion bar (SuggestionBar.kt) sizes itself from its content — its row height comes from Android layout and fixed dp padding, not from a height preference, and the number of visible suggestions is not capped by a preference either (the bar scrolls horizontally). Its one appearance preference is opacity, and it lives in the Input Behavior section’s Word Prediction block rather than here:

  • suggestion_bar_opacity (default 80, range 0-100) — read at Config.kt:901, applied via PreferenceUIUpdateHandler.kt:62 / PredictionViewSetup.kt:101.

See Input Behavior for that control.

Configuration

Every row below is a preference key the app actually reads. “Range” gives the Settings slider bound; where the import validator (backup/SettingsValidation.kt) accepts a wider band, both are shown.

SettingKeyDefaultRange
Themethemecleverkeysdarktheme id — see Themes
Portrait Heightkeyboard_height27slider 20-60 (% of screen height); validator 10-100
Landscape Heightkeyboard_height_landscape40slider 20-60; validator 20-65
Scale Numpad Heightscale_numpad_heighttruebool — explicitly opted-in layouts (numpad_height=true) stretch rows to the full keyboard height; bottom-row-free compact layouts retain normal row height (consumer Theme.kt; #58/#90)
Bottom Margin (Portrait)margin_bottom_portrait00-30 (% of screen height)
Bottom Margin (Landscape)margin_bottom_landscape00-30
Left Margin (Portrait)margin_left_portrait10-45 (% of screen width); UI caps left+right at 90 combined
Right Margin (Portrait)margin_right_portrait10-45, same combined cap
Left Margin (Landscape)margin_left_landscape50-45, same combined cap
Right Margin (Landscape)margin_right_landscape50-45, same combined cap
Label Brightnesslabel_brightness1000-100%
Keyboard Opacitykeyboard_opacity1000-100%
Key Opacitykey_opacity1000-100%
Activated Key Opacitykey_activated_opacity800-100%
Character Sizecharacter_size1.180.5-2.0 (SettingsRanges.CHARACTER_SIZE; UI slider 50%-200%)
Secondary Label Sizesecondary_label_size_scale1.0 (100% = unchanged)0.5-2.0 (UI slider 50%-200%)
Key Vertical Marginkey_vertical_margin1.5stored 0-5 (UI slider 0-500, divided by 100)
Key Horizontal Marginkey_horizontal_margin2.0stored 0-5 (UI slider 0-500, divided by 100)
Custom Borderborder_configfalsebool — gates the two rows below
Border Radiuscustom_border_radius0slider 0-20; validator 0-100. Stored value / 100 = fraction of key width
Border Line Widthcustom_border_line_width00-10 dp (SettingsRanges.CUSTOM_BORDER_LINE_WIDTH)

On a foldable that reports itself unfolded, Config reads _unfolded variants of the height and margin keys (keyboard_height_unfolded, margin_left_portrait_unfolded, …) with the same defaults and ranges. No Settings control writes them; they arrive through settings import.

Secondary Label Size (#133, v1.5.0)

Independent scale for the small corner (short-swipe/flick) labels, decoupled from Character Size. Default SECONDARY_LABEL_SIZE_SCALE = 1.0f (Config.kt:40); applied multiplicatively against the theme’s SUBLABEL_TEXT_SIZE_FACTOR in Keyboard2View.kt:1480:

_subLabelSize = labelBaseSize * snap.sublabelTextSize * snap.secondary_label_size_scale

Turn it down when a large Character Size makes sublabels crowd the main label.