How to Implement Neumorphic Theming with Custom Colors and Borders in Android Keyboard Views
Enable themeMode = "custom" and toggle customBorderEnable to switch between soft-raised neumorphic keys with dynamic shadows and traditional solid-color keys with borders.
The JapaneseKeyboard project implements a flexible theming system that renders keys with a tactile, three-dimensional appearance using dynamically generated drawables. By configuring user preferences and leveraging layered drawables in FlickKeyboardView.kt, you can apply neumorphic theming with custom colors and borders across the entire keyboard view. The implementation centers on the getDynamicNeumorphDrawable() method, which constructs highlight and shadow layers mathematically derived from your base color choices.
Enable Custom Theme Mode
To activate custom styling capabilities, you must first set the keyboard's theme mode to custom. In FlickKeyboardView.kt, the view checks the themeMode preference and enters a dedicated branch when the value equals "custom".
when (themeMode) {
"custom" -> {
// Custom color and border logic executes here
}
}
This trigger activates the color picker preferences and exposes the border toggle controls, allowing the subsequent logic to reference your custom color values instead of the default theme attributes.
Configure Base Colors and Border Behavior
The system reads five distinct color values from user preferences to construct the key appearance. Bold parameters include customKeyColor, customKeyTextColor, customSpecialKeyColor, customSpecialKeyTextColor, and customBorderColor. The boolean flag customBorderEnable determines which rendering path the view takes.
- When
customBorderEnableis true, the view draws a solid background color with an explicit border stroke, bypassing neumorphic effects entirely. - When
customBorderEnableis false, the view builds a compositeLayerDrawableconsisting of a neumorphic base layer and a transparent text overlay.
Build the Dynamic Neumorphic Drawable
The core visual effect originates in getDynamicNeumorphDrawable() within FlickKeyboardView.kt. This method programmatically generates a three-layer drawable that simulates physical depth through strategic color manipulation and positioning.
The method calculates derivative colors from your base input:
- Highlight color: Base color multiplied by
1.2f(20% brighter) for the top-left light source - Shadow color: Base color multiplied by
0.8f(20% darker) for the bottom-right shadow - Surface color: The original base color for the central key face
private fun getDynamicNeumorphDrawable(baseColor: Int, radius: Float): Drawable {
val highlightColor = manipulateColor(baseColor, 1.2f)
val shadowColor = manipulateColor(baseColor, 0.8f)
val offset = dpToPx(4)
val padding = dpToPx(2)
val shadowDrawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = radius
setColor(shadowColor)
}
val highlightDrawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = radius
setColor(highlightColor)
}
val surfaceDrawable = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = radius
setColor(baseColor)
}
return LayerDrawable(arrayOf(shadowDrawable, highlightDrawable, surfaceDrawable)).apply {
setLayerInset(0, offset, offset, 0, 0) // Shadow offset
setLayerInset(1, 0, 0, offset, offset) // Highlight offset
setLayerInset(2, padding, padding, padding, padding) // Surface inset
}
}
The radius parameter accepts commonCornerRadius, allowing you to maintain consistent corner roundness across all keys.
Compose the Layered Background
For the full neumorphic effect, the view combines the dynamic drawable with a SegmentedBackgroundDrawable instance. This transparent overlay handles text rendering and flick-direction highlights without obscuring the shadow layers beneath.
val neumorphDrawable = getDynamicNeumorphDrawable(
baseColor = targetBaseColor,
radius = commonCornerRadius
)
val segmentedDrawable = SegmentedBackgroundDrawable(
label = "",
baseColor = Color.TRANSPARENT,
highlightColor = targetHighlightColor,
textColor = targetTextColor,
cornerRadius = commonCornerRadius
)
val layerDrawable = LayerDrawable(arrayOf(neumorphDrawable, segmentedDrawable))
// Apply 2dp inset to align with QWERTY key spacing
val inset = dpToPx(2)
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
background = layerDrawable
The 2dp inset ensures the neumorphic layer properly aligns with the standard QWERTY keyboard layout spacing, preventing visual misalignment between adjacent keys.
Implement Solid Border Keys
When users prefer traditional flat styling with defined edges, set customBorderEnable to true. This path bypasses the layered drawable construction and applies colors directly using view methods.
setDrawableSolidColor(customKeyColor)
setTextColor(customKeyTextColor)
setBorder(customBorderColor, borderWidth)
This approach uses setBorder() to apply the customBorderColor preference as a stroke around the key, creating a clean, minimalist appearance without depth simulation.
Summary
- Set
themeMode = "custom"in keyboard preferences to enable custom color theming inFlickKeyboardView.kt - Use
customBorderEnableto toggle between solid borders (true) and neumorphic depth effects (false) - The
getDynamicNeumorphDrawable()method generates three-layer drawables with mathematically calculated highlight (×1.2) and shadow (×0.8) colors - Apply a
2dplayer inset when composing theLayerDrawableto maintain proper key spacing - Reference
customKeyColor,customSpecialKeyColor, and other preference values to personalize both standard and special function keys
Frequently Asked Questions
How do I adjust the intensity of the neumorphic shadow effect?
Modify the multiplier values passed to manipulateColor() inside getDynamicNeumorphDrawable(). The JapaneseKeyboard source uses 1.2f for highlights and 0.8f for shadows by default; increasing the highlight multiplier above 1.2f or decreasing the shadow multiplier below 0.8f amplifies the contrast and perceived depth.
Can neumorphic theming work with dark mode color schemes?
Yes, the dynamic calculation method adapts to any base color value. When a dark color serves as customKeyColor, the highlight becomes a lighter variant of that dark tone while the shadow becomes deeper, maintaining the soft-raised appearance across both light and dark themes according to the FlickKeyboardView.kt implementation.
What is the purpose of SegmentedBackgroundDrawable in this implementation?
SegmentedBackgroundDrawable functions as a transparent overlay that paints the key label text and optional flick-direction indicators on top of the neumorphic base layer. It resides in SegmentedBackgroundDrawable.kt and accepts Color.TRANSPARENT as its base fill, ensuring the underlying shadow and highlight layers remain visible while text renders crisply above them.
Why is a 2dp inset required when applying the neumorphic drawable?
The inset creates necessary padding between the edge of the key view and the neumorphic effect itself. Without this spacing, the shadow and highlight layers would bleed directly to the view boundaries, causing adjacent keys to visually collide. The 2dp value aligns the drawable with the QWERTY layout grid used throughout the JapaneseKeyboard project.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →