From 7a2ca28b50276da5e0620ac63886518920038dcc Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 04:33:23 +0530
Subject: [PATCH 1/6] feat: Add floating keyboard functionality to
Scribe-Android (#261)
---
.../KeyboardLanguageMappingConstants.kt | 12 +
.../be/scri/helpers/ui/KeyboardUIManager.kt | 31 +-
.../be/scri/services/GeneralKeyboardIME.kt | 643 +++++++++++++++++-
.../main/java/be/scri/helpers/KeyboardBase.kt | 3 +-
.../java/be/scri/helpers/PreferencesHelper.kt | 47 ++
.../drawable/floating_keyboard_background.xml | 9 +
app/src/main/res/drawable/ic_drag_handle.xml | 10 +
.../main/res/drawable/ic_keyboard_dismiss.xml | 9 +
.../main/res/drawable/ic_resize_corner.xml | 12 +
app/src/main/res/layout/input_method_view.xml | 145 +++-
app/src/main/res/values-night/colors.xml | 1 +
app/src/main/res/values/colors.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
13 files changed, 885 insertions(+), 39 deletions(-)
create mode 100644 app/src/main/res/drawable/floating_keyboard_background.xml
create mode 100644 app/src/main/res/drawable/ic_drag_handle.xml
create mode 100644 app/src/main/res/drawable/ic_keyboard_dismiss.xml
create mode 100644 app/src/main/res/drawable/ic_resize_corner.xml
diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
index 28984886f..9ae3997d8 100644
--- a/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
+++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLanguageMappingConstants.kt
@@ -49,4 +49,16 @@ object KeyboardLanguageMappingConstants {
"RU" to RUInterfaceVariables.PLURAL_KEY_LBL,
"SV" to SVInterfaceVariables.PLURAL_KEY_LBL,
)
+
+ val floatPlaceholder =
+ mapOf(
+ "EN" to "Float",
+ "ES" to "Flotante",
+ "DE" to "Schweben",
+ "IT" to "Fluttuante",
+ "FR" to "Flottant",
+ "PT" to "Flutuante",
+ "RU" to "Плавающая",
+ "SV" to "Flytande",
+ )
}
diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index 102bf5986..61eba85bc 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -23,6 +23,7 @@ import be.scri.R.color.white
import be.scri.databinding.InputMethodViewBinding
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
+import be.scri.helpers.KeyboardLanguageMappingConstants.floatPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.pluralPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.translatePlaceholder
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
@@ -56,6 +57,10 @@ class KeyboardUIManager(
fun onCloseClicked()
+ fun onFloatClicked()
+
+ fun isFloatingModeActive(): Boolean
+
fun onEmojiSelected(emoji: String)
fun onSuggestionClicked(suggestion: String)
@@ -73,6 +78,8 @@ class KeyboardUIManager(
fun processLinguisticSuggestions(word: String)
fun isNumericKeyboardActive(): Boolean
+
+ fun getKeyboardWidth(): Int
}
var keyboardView: KeyboardView = binding.keyboardView
@@ -80,6 +87,8 @@ class KeyboardUIManager(
// UI Elements
var pluralBtn: Button? = binding.pluralBtn
+ var floatBtn: Button? = binding.floatBtn
+ var separatorFloat: View? = binding.separatorFloat
var emojiBtnPhone1: Button? = binding.emojiBtnPhone1
var emojiSpacePhone: View? = binding.emojiSpacePhone
var emojiBtnPhone2: Button? = binding.emojiBtnPhone2
@@ -112,6 +121,7 @@ class KeyboardUIManager(
binding.translateBtn.setOnClickListener { listener.onTranslateClicked() }
binding.conjugateBtn.setOnClickListener { listener.onConjugateClicked() }
binding.pluralBtn.setOnClickListener { listener.onPluralClicked() }
+ binding.floatBtn.setOnClickListener { listener.onFloatClicked() }
binding.scribeKeyClose.setOnClickListener { listener.onCloseClicked() }
@@ -227,6 +237,8 @@ class KeyboardUIManager(
}
binding.separator1.visibility = View.GONE
+ binding.floatBtn.visibility = View.GONE
+ binding.separatorFloat.visibility = View.GONE
binding.ivInfo.visibility = View.GONE
binding.conjugateGridContainer.visibility = View.GONE
binding.keyboardView.visibility = View.VISIBLE
@@ -273,25 +285,34 @@ class KeyboardUIManager(
val buttonTextColor = if (isUserDarkMode) Color.WHITE else Color.BLACK
- listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEach { button ->
+ listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn, binding.floatBtn).forEach { button ->
button.visibility = View.VISIBLE
button.background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded)
button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.theme_scribe_blue)
button.setTextColor(buttonTextColor)
button.textSize = GeneralKeyboardIME.SUGGESTION_SIZE
+ button.isAllCaps = false
}
binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
+ binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
+
+ // Show the float button as "active" (lighter tint) when floating mode is already on
+ if (listener.isFloatingModeActive()) {
+ binding.floatBtn.backgroundTintList = ContextCompat.getColorStateList(context, R.color.light_key_color)
+ binding.floatBtn.setTextColor(ContextCompat.getColor(context, R.color.theme_scribe_blue))
+ }
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
binding.separator3.setBackgroundColor(separatorColor)
+ binding.separatorFloat.setBackgroundColor(separatorColor)
val spaceInDp = 4
val spaceInPx = (spaceInDp * context.resources.displayMetrics.density).toInt()
- listOf(binding.separator2, binding.separator3).forEach { separator ->
+ listOf(binding.separator2, binding.separator3, binding.separatorFloat).forEach { separator ->
separator.setBackgroundColor(Color.TRANSPARENT)
val params = separator.layoutParams
params.width = spaceInPx
@@ -301,6 +322,7 @@ class KeyboardUIManager(
binding.separator1.visibility = View.GONE
binding.separator2.visibility = View.VISIBLE
binding.separator3.visibility = View.VISIBLE
+ binding.separatorFloat.visibility = View.VISIBLE
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
@@ -600,7 +622,8 @@ class KeyboardUIManager(
*/
fun initializeKeyboard(xmlId: Int) {
val enterKeyType = listener.getCurrentEnterKeyType()
- keyboard = KeyboardBase(context, xmlId, enterKeyType)
+ val width = listener.getKeyboardWidth()
+ keyboard = KeyboardBase(context, xmlId, enterKeyType, width)
keyboardView.setKeyboard(keyboard!!)
keyboardView.mOnKeyboardActionListener = listener.onKeyboardActionListener()
keyboardView.requestLayout()
@@ -660,6 +683,8 @@ class KeyboardUIManager(
*/
private fun setupDefaultButtonVisibility() {
pluralBtn?.visibility = View.VISIBLE
+ floatBtn?.visibility = View.GONE
+ separatorFloat?.visibility = View.GONE
emojiBtnPhone1?.visibility = View.GONE
emojiBtnPhone2?.visibility = View.GONE
emojiBtnTablet1?.visibility = View.GONE
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index 57c7e1ca8..d4dc3441f 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -8,10 +8,15 @@ import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteException
import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.Region
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.inputmethodservice.InputMethodService
+import android.view.Choreographer
+import android.view.WindowManager
import android.os.Build
import android.text.InputType
import android.text.InputType.TYPE_CLASS_DATETIME
@@ -20,7 +25,9 @@ import android.text.InputType.TYPE_CLASS_PHONE
import android.text.InputType.TYPE_MASK_CLASS
import android.util.Log
import android.view.KeyEvent
+import android.view.MotionEvent
import android.view.View
+import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
@@ -35,6 +42,7 @@ import androidx.core.graphics.toColorInt
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import be.scri.R
import be.scri.activities.MainActivity
import be.scri.databinding.InputMethodViewBinding
@@ -232,7 +240,7 @@ abstract class GeneralKeyboardIME(
keyboardView = uiManager.keyboardView
// Initial keyboard setup.
- keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType)
+ keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth())
keyboardView?.apply {
setVibrate = getIsVibrateEnabled(applicationContext, language)
@@ -245,6 +253,12 @@ abstract class GeneralKeyboardIME(
currentState = ScribeState.IDLE
saveConjugateModeType("none")
+ viewBinding.root.post {
+ disableParentClipping(viewBinding.root)
+ }
+ initFloatingMode()
+ setupFloatingDragListener()
+
refreshUI()
return viewBinding.root
@@ -265,6 +279,8 @@ abstract class GeneralKeyboardIME(
*/
override fun onEvaluateFullscreenMode(): Boolean = false
+
+
/**
* Compute the insets for the keyboard view. This is essential for API 36+
* where the system needs to know the exact size of the keyboard to properly
@@ -272,21 +288,53 @@ abstract class GeneralKeyboardIME(
*/
override fun onComputeInsets(outInsets: Insets) {
super.onComputeInsets(outInsets)
- // Access root view via UI manager if initialized.
if (this::uiManager.isInitialized) {
val inputView = uiManager.binding.root
if (inputView.visibility == View.VISIBLE && inputView.height > 0) {
val location = IntArray(2)
inputView.getLocationInWindow(location)
- outInsets.visibleTopInsets = location[1]
- outInsets.contentTopInsets = location[1]
- outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+
+ if (isFloatingMode) {
+ // In floating mode, report zero insets so Android doesn't
+ // push app content up or render IME chrome (∨ / 🌐 buttons)
+ // below the card. The touchable region is restricted to the
+ // card bounds so taps outside reach the underlying app.
+ outInsets.visibleTopInsets = 0
+ outInsets.contentTopInsets = 0
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
+
+ val card = binding.keyboardCard
+ val density = resources.displayMetrics.density
+ if (card.width > 0 && card.height > 0) {
+ val centerX = card.left + card.width / 2f + card.translationX
+ val centerY = card.top + card.height / 2f + card.translationY
+ val visualW = card.width * card.scaleX
+ val visualH = card.height * card.scaleY
+ val left = (centerX - visualW / 2f).toInt()
+ val top = (centerY - visualH / 2f).toInt()
+ val right = (centerX + visualW / 2f).toInt()
+ val bottom = (centerY + visualH / 2f).toInt()
+
+ val rect = Rect(left, top, right, bottom)
+ // Expand touchable region slightly to allow resizing handles to be clickable
+ val margin = (25 * density).toInt()
+ rect.inset(-margin, -margin)
+ outInsets.touchableRegion.set(rect)
+ } else {
+ outInsets.touchableRegion.setEmpty()
+ }
+ } else {
+ outInsets.visibleTopInsets = location[1]
+ outInsets.contentTopInsets = location[1]
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
+ }
}
}
}
override fun onWindowShown() {
super.onWindowShown()
+ applyFloatingModeState()
applyNavBarColor()
keyboardView?.setPreview = isShowPopupOnKeypressEnabled(applicationContext, language)
keyboardView?.setVibrate = getIsVibrateEnabled(applicationContext, language)
@@ -319,7 +367,7 @@ abstract class GeneralKeyboardIME(
loadLanguageData()
- keyboard = KeyboardBase(this, keyboardXml, enterKeyType)
+ keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth())
keyboardView?.setKeyboard(keyboard!!)
if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) {
@@ -586,36 +634,66 @@ abstract class GeneralKeyboardIME(
private fun applyNavBarColor() {
val window = window?.window ?: return
- val isDarkMode = getIsDarkModeOrNot(applicationContext)
- val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color
- val color = ContextCompat.getColor(this, colorRes)
+ window.decorView.post {
+ val isDarkMode = getIsDarkModeOrNot(applicationContext)
+ val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color
+ val color = ContextCompat.getColor(this, colorRes)
- if (Build.VERSION.SDK_INT >= 35) {
WindowCompat.setDecorFitsSystemWindows(window, false)
- } else {
- window.navigationBarColor = Color.TRANSPARENT
- }
+ if (Build.VERSION.SDK_INT < 35) {
+ window.navigationBarColor = Color.TRANSPARENT
+ }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- window.isNavigationBarContrastEnforced = false
- }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ window.isNavigationBarContrastEnforced = false
+ }
- window.decorView.setBackgroundColor(color)
- val insetsController = WindowCompat.getInsetsController(window, window.decorView)
- insetsController.isAppearanceLightNavigationBars = isLightColor(color)
+ if (isFloatingMode) {
+ window.decorView.setBackgroundColor(Color.TRANSPARENT)
+ } else {
+ window.decorView.setBackgroundColor(color)
+ }
+ val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+ insetsController.isAppearanceLightNavigationBars = isLightColor(color)
+
+ if (isFloatingMode) {
+ insetsController.hide(WindowInsetsCompat.Type.navigationBars())
+ insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ @Suppress("DEPRECATION")
+ window.decorView.systemUiVisibility = (
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ )
+ } else {
+ insetsController.show(WindowInsetsCompat.Type.navigationBars())
+ @Suppress("DEPRECATION")
+ window.decorView.systemUiVisibility = 0
+ }
- if (this::uiManager.isInitialized) {
- uiManager.binding.root.setBackgroundColor(color)
+ if (this::uiManager.isInitialized) {
+ if (isFloatingMode) {
+ uiManager.binding.root.setBackgroundColor(Color.TRANSPARENT)
+ // Keep drag bar and pill color in sync with dark/light mode changes
+ val kbBgColor = ContextCompat.getColor(this, if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color)
+ uiManager.binding.floatingDragBar.setBackgroundColor(kbBgColor)
+ // Pill: dark mode → 30% white, light mode → 25% black
+ val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt()
+ uiManager.binding.floatingDragHandle.setColorFilter(pillColor)
+ } else {
+ uiManager.binding.root.setBackgroundColor(color)
+ }
- ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets ->
- val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
- val navBarHeight = insets.getInsets(insetTypes).bottom
- view.setPadding(0, 0, 0, navBarHeight)
- insets
- }
+ ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets ->
+ val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
+ val navBarHeight = insets.getInsets(insetTypes).bottom
+ val paddingBottom = if (isFloatingMode) 0 else navBarHeight
+ view.setPadding(0, 0, 0, paddingBottom)
+ insets
+ }
- uiManager.binding.root.post {
- ViewCompat.requestApplyInsets(uiManager.binding.root)
+ uiManager.binding.root.post {
+ ViewCompat.requestApplyInsets(uiManager.binding.root)
+ }
}
}
}
@@ -732,6 +810,12 @@ abstract class GeneralKeyboardIME(
moveToIdleState()
}
+ override fun onFloatClicked() {
+ toggleFloatingMode()
+ }
+
+ override fun isFloatingModeActive(): Boolean = isFloatingMode
+
override fun onEmojiSelected(emoji: String) {
if (emoji.isNotEmpty()) {
insertEmoji(emoji, currentInputConnection, emojiKeywords, emojiMaxKeywordLength)
@@ -1936,4 +2020,505 @@ abstract class GeneralKeyboardIME(
* Disables all auto-suggestions and resets the suggestion buttons to their default, inactive state.
*/
fun disableAutoSuggest() = uiManager.disableAutoSuggest(language)
+
+ // MARK: Floating Keyboard Integration
+
+ override fun getKeyboardWidth(): Int {
+ return if (isFloatingMode) {
+ val density = resources.displayMetrics.density
+ val screenWidth = resources.displayMetrics.widthPixels
+ val floatWidth = (320f * density).toInt()
+ Math.min(floatWidth, (screenWidth * 0.85f).toInt())
+ } else {
+ resources.displayMetrics.widthPixels
+ }
+ }
+
+ private fun recreateKeyboard() {
+ if (!this::uiManager.isInitialized) return
+ val xmlId = getCurrentKeyboardLayoutXML()
+ val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF
+ keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth())
+ keyboard?.setShifted(currentShiftState)
+ keyboardView?.setKeyboard(keyboard!!)
+
+ if (xmlId == R.xml.keys_symbols) {
+ uiManager.setupCurrencySymbol(language)
+ }
+ keyboardView?.invalidateAllKeys()
+ }
+
+ var isFloatingMode: Boolean = false
+ private set
+
+ private var isUpdatePending = false
+ private var lastAppliedFloatingMode: Boolean? = null
+
+ fun initFloatingMode() {
+ isFloatingMode = PreferencesHelper.getIsFloatingModeEnabled(this, language)
+ lastAppliedFloatingMode = null
+ applyFloatingModeState()
+ }
+
+ fun toggleFloatingMode() {
+ isFloatingMode = !isFloatingMode
+ PreferencesHelper.setIsFloatingModeEnabled(this, language, isFloatingMode)
+ // Reset the cached mode so applyFloatingModeState always treats this as a change
+ lastAppliedFloatingMode = null
+ applyFloatingModeState()
+ window?.window?.decorView?.requestLayout()
+ }
+
+ private fun applyFloatingModeState() {
+ if (!this::uiManager.isInitialized) return
+ val card = binding.keyboardCard
+ val dragBar = binding.floatingDragBar
+ val density = resources.displayMetrics.density
+ val root = binding.root
+ val win = window?.window
+
+ // Only recreate the keyboard when the floating mode actually changes.
+ // Calling applyFloatingModeState from onWindowShown should not rebuild
+ // the keyboard every time a text field is focused.
+ val modeChanged = lastAppliedFloatingMode != isFloatingMode
+ lastAppliedFloatingMode = isFloatingMode
+
+ val rootWidth = ViewGroup.LayoutParams.MATCH_PARENT
+ val rootHeight = if (isFloatingMode) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT
+ val rootParams = root.layoutParams ?: ViewGroup.LayoutParams(rootWidth, rootHeight)
+ rootParams.width = rootWidth
+ rootParams.height = rootHeight
+ root.layoutParams = rootParams
+ root.minimumHeight = 0
+
+ val parentViewGroup = root.parent as? ViewGroup
+ if (parentViewGroup != null) {
+ val pParams = parentViewGroup.layoutParams
+ if (pParams != null) {
+ pParams.width = rootWidth
+ pParams.height = rootHeight
+ parentViewGroup.layoutParams = pParams
+ }
+ }
+
+ if (isFloatingMode) {
+ setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING)
+ win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ win?.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ } else {
+ setBackDisposition(BACK_DISPOSITION_DEFAULT)
+ win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ win?.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ }
+
+ if (isFloatingMode) {
+ val params = card.layoutParams
+ if (params != null) {
+ params.width = getKeyboardWidth()
+ card.layoutParams = params
+ }
+
+ val scaleFactor = PreferencesHelper.getFloatingScale(this, language)
+ card.scaleX = scaleFactor
+ card.scaleY = scaleFactor
+
+ // Setup resize corner handlers
+ binding.resizeHandleTopLeft.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleTopRight.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleBottomLeft.setOnTouchListener(resizeTouchListener)
+ binding.resizeHandleBottomRight.setOnTouchListener(resizeTouchListener)
+
+ // Hide initially
+ binding.resizeHandleTopLeft.visibility = View.GONE
+ binding.resizeHandleTopRight.visibility = View.GONE
+ binding.resizeHandleBottomLeft.visibility = View.GONE
+ binding.resizeHandleBottomRight.visibility = View.GONE
+
+ val isDarkMode = getIsDarkModeOrNot(this)
+ val kbBgColorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color
+ val kbBgColor = ContextCompat.getColor(this, kbBgColorRes)
+
+ // Build a floating card background that matches the keyboard's actual theme color
+ val floatingBg = GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = 16f * density
+ setColor(kbBgColor)
+ setStroke((1f * density).toInt(), 0x40888888.toInt())
+ }
+ card.background = floatingBg
+ card.elevation = 8f * density
+ card.clipToOutline = true
+
+ // Drag bar must match the keyboard background — not the default (always-light) color
+ binding.floatingDragBar.setBackgroundColor(kbBgColor)
+ // Pill color: visible on both dark and light keyboard backgrounds
+ val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt()
+ binding.floatingDragHandle.setColorFilter(pillColor)
+
+ dragBar.visibility = View.VISIBLE
+
+ if (modeChanged) recreateKeyboard()
+
+ card.post {
+ disableParentClipping(root)
+ var storedX = PreferencesHelper.getFloatingX(this, language)
+ var storedY = PreferencesHelper.getFloatingY(this, language)
+ val currentScale = PreferencesHelper.getFloatingScale(this, language)
+
+ // Default starting position: 100dp from the bottom of the screen
+ if (storedY == 0f) storedY = 100f * density
+
+ val screenWidth = resources.displayMetrics.widthPixels
+ val screenHeight = resources.displayMetrics.heightPixels
+ val cardWidth = card.width.toFloat()
+ val cardHeight = card.height.toFloat()
+
+ if (cardWidth > 0f && cardHeight > 0f) {
+ val maxTranslationX = (screenWidth - cardWidth * currentScale) / 2f
+ val minTranslationX = -maxTranslationX
+
+ val minTranslationY = 0f
+ val maxTranslationY = screenHeight.toFloat() - cardHeight * currentScale
+
+ val targetX = storedX.coerceInSafe(minTranslationX, maxTranslationX)
+ val targetY = storedY.coerceInSafe(minTranslationY, maxTranslationY)
+
+ updateFloatingViewsPosition(targetX, targetY, currentScale)
+
+ val attr = win?.attributes
+ if (attr != null) {
+ attr.gravity = android.view.Gravity.TOP or android.view.Gravity.START
+ attr.x = 0
+ attr.y = 0
+ attr.width = ViewGroup.LayoutParams.MATCH_PARENT
+ attr.height = ViewGroup.LayoutParams.MATCH_PARENT
+ win.attributes = attr
+ }
+ root.requestLayout()
+ }
+ }
+ } else {
+ val params = card.layoutParams
+ if (params != null) {
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT
+ card.layoutParams = params
+ }
+
+ card.scaleX = 1.0f
+ card.scaleY = 1.0f
+
+ val isDarkMode = getIsDarkModeOrNot(this)
+ val kbBgColorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color
+ card.background = ColorDrawable(ContextCompat.getColor(this, kbBgColorRes))
+ card.elevation = 0f
+ card.clipToOutline = false
+
+ dragBar.visibility = View.GONE
+
+ if (modeChanged) recreateKeyboard()
+
+ // Reset translations immediately so the card doesn't sit at a stale
+ // floating position while the window re-layouts to WRAP_CONTENT.
+ card.translationX = 0f
+ card.translationY = 0f
+
+ binding.resizeHandleTopLeft.translationX = 0f
+ binding.resizeHandleTopLeft.translationY = 0f
+ binding.resizeHandleTopRight.translationX = 0f
+ binding.resizeHandleTopRight.translationY = 0f
+ binding.resizeHandleBottomLeft.translationX = 0f
+ binding.resizeHandleBottomLeft.translationY = 0f
+ binding.resizeHandleBottomRight.translationX = 0f
+ binding.resizeHandleBottomRight.translationY = 0f
+
+ // Hide resize handles
+ binding.resizeHandleTopLeft.visibility = View.GONE
+ binding.resizeHandleTopRight.visibility = View.GONE
+ binding.resizeHandleBottomLeft.visibility = View.GONE
+ binding.resizeHandleBottomRight.visibility = View.GONE
+
+ // Apply window attributes first, then force a layout pass to ensure
+ // the command options bar is fully visible after returning to docked mode.
+ val attr = win?.attributes
+ if (attr != null) {
+ attr.gravity = android.view.Gravity.BOTTOM
+ attr.x = 0
+ attr.y = 0
+ attr.width = ViewGroup.LayoutParams.MATCH_PARENT
+ attr.height = ViewGroup.LayoutParams.WRAP_CONTENT
+ win.attributes = attr
+ }
+
+ // Post a second pass to guarantee translations are zero after the
+ // window has finished resizing — FLAG_LAYOUT_NO_LIMITS removal is
+ // async and can cause a stale layout frame where the bar is clipped.
+ card.post {
+ card.translationX = 0f
+ card.translationY = 0f
+ root.requestLayout()
+ }
+ }
+ applyNavBarColor()
+ }
+
+ private fun updateFloatingViewsPosition(targetX: Float, targetY: Float, scale: Float) {
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+
+ val cardWidth = card.width.toFloat()
+ val cardHeight = card.height.toFloat()
+ if (cardWidth == 0f || cardHeight == 0f) return
+
+ card.scaleX = scale
+ card.scaleY = scale
+
+ val transX = targetX
+ val transY = (screenHeight - cardHeight * scale) / 2f - targetY
+
+ card.translationX = transX
+ card.translationY = transY
+
+ val scaleOffset = scale - 1.0f
+ val halfW = cardWidth / 2f
+ val halfH = cardHeight / 2f
+
+ binding.resizeHandleTopLeft.translationX = transX - halfW * scaleOffset
+ binding.resizeHandleTopLeft.translationY = transY - halfH * scaleOffset
+
+ binding.resizeHandleTopRight.translationX = transX + halfW * scaleOffset
+ binding.resizeHandleTopRight.translationY = transY - halfH * scaleOffset
+
+ binding.resizeHandleBottomLeft.translationX = transX - halfW * scaleOffset
+ binding.resizeHandleBottomLeft.translationY = transY + halfH * scaleOffset
+
+ binding.resizeHandleBottomRight.translationX = transX + halfW * scaleOffset
+ binding.resizeHandleBottomRight.translationY = transY + halfH * scaleOffset
+ }
+
+ private fun disableParentClipping(view: View) {
+ var p = view.parent
+ while (p is ViewGroup) {
+ p.clipChildren = false
+ p.clipToPadding = false
+ p = p.parent
+ }
+ }
+
+ private var initialX = 0f
+ private var initialY = 0f
+ private var initialTranslationX = 0f
+ private var initialTranslationY = 0f
+ private var maxTranslationX = 0f
+ private var minTranslationX = 0f
+ private var minTranslationY = 0f
+ private var maxTranslationY = 0f
+
+ private val cornerHideHandler = android.os.Handler(android.os.Looper.getMainLooper())
+ private val hideCornersRunnable = Runnable {
+ animateHideCorners()
+ }
+
+ private var initialTouchX = 0f
+ private var initialTouchY = 0f
+ private var initialScale = 1.0f
+ private var keyboardCenterX = 0f
+ private var keyboardCenterY = 0f
+ private var initialDistance = 0f
+ private var isResizing = false
+
+ private fun showCorners() {
+ cornerHideHandler.removeCallbacks(hideCornersRunnable)
+
+ val corners = listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight
+ )
+
+ for (corner in corners) {
+ corner.animate().cancel()
+ corner.alpha = 1f
+ corner.visibility = View.VISIBLE
+ }
+ }
+
+ private fun startHideCornersTimer() {
+ cornerHideHandler.removeCallbacks(hideCornersRunnable)
+ cornerHideHandler.postDelayed(hideCornersRunnable, 3000)
+ }
+
+ private fun animateHideCorners() {
+ val corners = listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight
+ )
+
+ for (corner in corners) {
+ corner.animate()
+ .alpha(0f)
+ .setDuration(300)
+ .withEndAction {
+ corner.visibility = View.GONE
+ }
+ .start()
+ }
+ }
+
+ private fun applyScaleAndPosition(scale: Float) {
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+
+ // Recover current logical Y from live translationY (inverse of updateFloatingViewsPosition formula)
+ // transY = (screenHeight - cardHeight * scale) / 2f - targetY
+ // => targetY = (screenHeight - cardHeight * scale) / 2f - transY
+ // Use the previous scale to get the consistent Y value before scale changes
+ val liveX = card.translationX
+ val liveTransY = card.translationY
+ val prevScale = card.scaleX
+ val liveY = (screenHeight - cardHeight * prevScale) / 2f - liveTransY
+
+ updateFloatingViewsPosition(liveX, liveY, scale)
+ }
+
+ private val resizeTouchListener = View.OnTouchListener { _, event ->
+ if (!isFloatingMode) return@OnTouchListener false
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isResizing = true
+ showCorners()
+
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+ initialScale = PreferencesHelper.getFloatingScale(this, language)
+
+ val card = binding.keyboardCard
+ val location = IntArray(2)
+ card.getLocationOnScreen(location)
+
+ keyboardCenterX = location[0] + card.width / 2f
+ keyboardCenterY = location[1] + card.height / 2f
+
+ initialDistance = Math.hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble()
+ ).toFloat()
+
+ true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (!isResizing) return@OnTouchListener false
+
+ val currentDistance = Math.hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble()
+ ).toFloat()
+
+ if (initialDistance > 0) {
+ var targetScale = initialScale * (currentDistance / initialDistance)
+ targetScale = targetScale.coerceIn(0.7f, 1.3f)
+ applyScaleAndPosition(targetScale)
+ }
+ true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ isResizing = false
+ startHideCornersTimer()
+
+ val finalScale = binding.keyboardCard.scaleX
+ PreferencesHelper.setFloatingScale(this, language, finalScale)
+
+ // Save live position so applyFloatingModeState restores it correctly
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+ val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
+ PreferencesHelper.setFloatingX(this, language, card.translationX)
+ PreferencesHelper.setFloatingY(this, language, liveY)
+
+ applyFloatingModeState()
+ true
+ }
+ else -> false
+ }
+ }
+
+ @android.annotation.SuppressLint("ClickableViewAccessibility")
+ fun setupFloatingDragListener() {
+ if (!this::uiManager.isInitialized) return
+
+ binding.floatingDragHandle.setOnTouchListener { _, event ->
+ if (!isFloatingMode) return@setOnTouchListener false
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ initialX = event.rawX
+ initialY = event.rawY
+
+ val displayMetrics = resources.displayMetrics
+ val screenWidth = displayMetrics.widthPixels
+ val screenHeight = displayMetrics.heightPixels
+ val cardWidth = binding.keyboardCard.width.toFloat()
+ val cardHeight = binding.keyboardCard.height.toFloat()
+ val scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+
+ maxTranslationX = (screenWidth - cardWidth * scaleFactor) / 2f
+ minTranslationX = -maxTranslationX
+
+ minTranslationY = 0f
+ maxTranslationY = screenHeight.toFloat() - cardHeight * scaleFactor
+
+ initialTranslationX = PreferencesHelper.getFloatingX(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationX, maxTranslationX)
+ initialTranslationY = PreferencesHelper.getFloatingY(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationY, maxTranslationY)
+
+ showCorners()
+ true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ val deltaX = event.rawX - initialX
+ val deltaY = event.rawY - initialY
+
+ var targetX = initialTranslationX + deltaX
+ var targetY = initialTranslationY - deltaY
+
+ targetX = targetX.coerceInSafe(minTranslationX, maxTranslationX)
+ targetY = targetY.coerceInSafe(minTranslationY, maxTranslationY)
+
+ val scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+ updateFloatingViewsPosition(targetX, targetY, scaleFactor)
+
+ true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ val deltaX = event.rawX - initialX
+ val deltaY = event.rawY - initialY
+ var finalTargetX = initialTranslationX + deltaX
+ var finalTargetY = initialTranslationY - deltaY
+ finalTargetX = finalTargetX.coerceInSafe(minTranslationX, maxTranslationX)
+ finalTargetY = finalTargetY.coerceInSafe(minTranslationY, maxTranslationY)
+
+ val scaleFactor = PreferencesHelper.getFloatingScale(this@GeneralKeyboardIME, language)
+ updateFloatingViewsPosition(finalTargetX, finalTargetY, scaleFactor)
+
+ PreferencesHelper.setFloatingX(this@GeneralKeyboardIME, language, finalTargetX)
+ PreferencesHelper.setFloatingY(this@GeneralKeyboardIME, language, finalTargetY)
+
+ binding.root.requestLayout()
+ startHideCornersTimer()
+ true
+ }
+ else -> false
+ }
+ }
+ }
+}
+
+private fun Float.coerceInSafe(bound1: Float, bound2: Float): Float {
+ val minVal = if (bound1 < bound2) bound1 else bound2
+ val maxVal = if (bound1 > bound2) bound1 else bound2
+ return this.coerceIn(minVal, maxVal)
}
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index 4af94453d..d10e1a2c3 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -411,8 +411,9 @@ class KeyboardBase {
context: Context,
@XmlRes xmlLayoutResId: Int,
enterKeyType: Int,
+ customWidth: Int? = null,
) {
- mDisplayWidth = context.resources.displayMetrics.widthPixels
+ mDisplayWidth = customWidth ?: context.resources.displayMetrics.widthPixels
mDefaultHorizontalGap = 0
mDefaultWidth = mDisplayWidth / WIDTH_DIVIDER
mDefaultHeight = mDefaultWidth
diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
index 1427bb8a0..8e7932648 100644
--- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
+++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
@@ -632,4 +632,51 @@ object PreferencesHelper {
val sharedPref = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
return sharedPref.getBoolean(INCREASE_TEXT_SIZE, false)
}
+
+ // MARK: Floating Keyboard Preferences
+
+ private const val FLOATING_MODE_ENABLED = "floating_mode_enabled"
+ private const val FLOATING_X = "floating_x"
+ private const val FLOATING_Y = "floating_y"
+ private const val FLOATING_SCALE = "floating_scale"
+
+ fun setIsFloatingModeEnabled(context: Context, language: String, enabled: Boolean) {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ sharedPref.edit { putBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), enabled) }
+ }
+
+ fun getIsFloatingModeEnabled(context: Context, language: String): Boolean {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), false)
+ }
+
+ fun setFloatingX(context: Context, language: String, x: Float) {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), x) }
+ }
+
+ fun getFloatingX(context: Context, language: String): Float {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), 0f)
+ }
+
+ fun setFloatingY(context: Context, language: String, y: Float) {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), y) }
+ }
+
+ fun getFloatingY(context: Context, language: String): Float {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), 0f)
+ }
+
+ fun setFloatingScale(context: Context, language: String, scale: Float) {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), scale) }
+ }
+
+ fun getFloatingScale(context: Context, language: String): Float {
+ val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
+ return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), 1.0f)
+ }
}
diff --git a/app/src/main/res/drawable/floating_keyboard_background.xml b/app/src/main/res/drawable/floating_keyboard_background.xml
new file mode 100644
index 000000000..4b34336d7
--- /dev/null
+++ b/app/src/main/res/drawable/floating_keyboard_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml
new file mode 100644
index 000000000..9f5cdf1c9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_drag_handle.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_keyboard_dismiss.xml b/app/src/main/res/drawable/ic_keyboard_dismiss.xml
new file mode 100644
index 000000000..f371a2722
--- /dev/null
+++ b/app/src/main/res/drawable/ic_keyboard_dismiss.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml
new file mode 100644
index 000000000..0b339a5ac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_resize_corner.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml
index f79cf8133..ea38161af 100644
--- a/app/src/main/res/layout/input_method_view.xml
+++ b/app/src/main/res/layout/input_method_view.xml
@@ -11,7 +11,20 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyboard_holder"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+
@@ -25,7 +38,7 @@
app:layout_constraintBottom_toTopOf="@id/keyboard_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="parent">
+ app:layout_constraintTop_toTopOf="parent">
+
+
@@ -225,15 +247,42 @@
android:id="@+id/plural_btn"
android:layout_width="0dp"
android:layout_height="@dimen/command_button_height"
- android:layout_marginEnd="@dimen/tiny_margin"
android:background="@drawable/cmd_key_background_rounded"
android:contentDescription="@string/command_bar"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/separator_float"
app:layout_constraintStart_toEndOf="@+id/separator_3"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 56a9fcab3..f4f15d8c0 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -23,6 +23,7 @@
@color/md_grey_800_dark
@color/you_button_background_color
@color/dark_scribe_blue
+ #FFB0B0B0
@color/dark_keyboard_bg_color
@color/dark_key_color
@color/dark_keyboard_bg_color
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index e8065be55..13555101c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -24,6 +24,7 @@
@color/dark_scribe_color
+ #FF555555
@color/light_scribe_color
#808080
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8431bc8dc..3dd17a727 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,4 +13,5 @@
Enter
Delete
Change keyboard type
+ Drag keyboard
From 6becf1b6c9afeed98fff1fce7e38bb51086a409a Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:36:03 +0530
Subject: [PATCH 2/6] style: fix ktlint issues in GeneralKeyboardIME and
PreferencesHelper
---
.../be/scri/services/GeneralKeyboardIME.kt | 235 +++++++++---------
.../java/be/scri/helpers/PreferencesHelper.kt | 44 +++-
2 files changed, 159 insertions(+), 120 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index d4dc3441f..ba975a39c 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -9,14 +9,11 @@ import android.content.Intent
import android.database.sqlite.SQLiteException
import android.graphics.Color
import android.graphics.Rect
-import android.graphics.Region
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.inputmethodservice.InputMethodService
-import android.view.Choreographer
-import android.view.WindowManager
import android.os.Build
import android.text.InputType
import android.text.InputType.TYPE_CLASS_DATETIME
@@ -28,6 +25,7 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.EditorInfo.IME_ACTION_NONE
import android.view.inputmethod.EditorInfo.IME_FLAG_NO_ENTER_ACTION
@@ -279,8 +277,6 @@ abstract class GeneralKeyboardIME(
*/
override fun onEvaluateFullscreenMode(): Boolean = false
-
-
/**
* Compute the insets for the keyboard view. This is essential for API 36+
* where the system needs to know the exact size of the keyboard to properly
@@ -293,7 +289,7 @@ abstract class GeneralKeyboardIME(
if (inputView.visibility == View.VISIBLE && inputView.height > 0) {
val location = IntArray(2)
inputView.getLocationInWindow(location)
-
+
if (isFloatingMode) {
// In floating mode, report zero insets so Android doesn't
// push app content up or render IME chrome (∨ / 🌐 buttons)
@@ -302,7 +298,7 @@ abstract class GeneralKeyboardIME(
outInsets.visibleTopInsets = 0
outInsets.contentTopInsets = 0
outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_REGION
-
+
val card = binding.keyboardCard
val density = resources.displayMetrics.density
if (card.width > 0 && card.height > 0) {
@@ -314,7 +310,7 @@ abstract class GeneralKeyboardIME(
val top = (centerY - visualH / 2f).toInt()
val right = (centerX + visualW / 2f).toInt()
val bottom = (centerY + visualH / 2f).toInt()
-
+
val rect = Rect(left, top, right, bottom)
// Expand touchable region slightly to allow resizing handles to be clickable
val margin = (25 * density).toInt()
@@ -662,7 +658,7 @@ abstract class GeneralKeyboardIME(
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
} else {
insetsController.show(WindowInsetsCompat.Type.navigationBars())
@@ -2023,8 +2019,8 @@ abstract class GeneralKeyboardIME(
// MARK: Floating Keyboard Integration
- override fun getKeyboardWidth(): Int {
- return if (isFloatingMode) {
+ override fun getKeyboardWidth(): Int =
+ if (isFloatingMode) {
val density = resources.displayMetrics.density
val screenWidth = resources.displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
@@ -2032,7 +2028,6 @@ abstract class GeneralKeyboardIME(
} else {
resources.displayMetrics.widthPixels
}
- }
private fun recreateKeyboard() {
if (!this::uiManager.isInitialized) return
@@ -2050,7 +2045,7 @@ abstract class GeneralKeyboardIME(
var isFloatingMode: Boolean = false
private set
-
+
private var isUpdatePending = false
private var lastAppliedFloatingMode: Boolean? = null
@@ -2100,7 +2095,7 @@ abstract class GeneralKeyboardIME(
parentViewGroup.layoutParams = pParams
}
}
-
+
if (isFloatingMode) {
setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING)
win?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
@@ -2139,12 +2134,13 @@ abstract class GeneralKeyboardIME(
val kbBgColor = ContextCompat.getColor(this, kbBgColorRes)
// Build a floating card background that matches the keyboard's actual theme color
- val floatingBg = GradientDrawable().apply {
- shape = GradientDrawable.RECTANGLE
- cornerRadius = 16f * density
- setColor(kbBgColor)
- setStroke((1f * density).toInt(), 0x40888888.toInt())
- }
+ val floatingBg =
+ GradientDrawable().apply {
+ shape = GradientDrawable.RECTANGLE
+ cornerRadius = 16f * density
+ setColor(kbBgColor)
+ setStroke((1f * density).toInt(), 0x40888888.toInt())
+ }
card.background = floatingBg
card.elevation = 8f * density
card.clipToOutline = true
@@ -2184,7 +2180,7 @@ abstract class GeneralKeyboardIME(
val targetY = storedY.coerceInSafe(minTranslationY, maxTranslationY)
updateFloatingViewsPosition(targetX, targetY, currentScale)
-
+
val attr = win?.attributes
if (attr != null) {
attr.gravity = android.view.Gravity.TOP or android.view.Gravity.START
@@ -2261,36 +2257,40 @@ abstract class GeneralKeyboardIME(
applyNavBarColor()
}
- private fun updateFloatingViewsPosition(targetX: Float, targetY: Float, scale: Float) {
+ private fun updateFloatingViewsPosition(
+ targetX: Float,
+ targetY: Float,
+ scale: Float,
+ ) {
val card = binding.keyboardCard
val screenHeight = resources.displayMetrics.heightPixels.toFloat()
-
+
val cardWidth = card.width.toFloat()
val cardHeight = card.height.toFloat()
if (cardWidth == 0f || cardHeight == 0f) return
-
+
card.scaleX = scale
card.scaleY = scale
-
+
val transX = targetX
val transY = (screenHeight - cardHeight * scale) / 2f - targetY
-
+
card.translationX = transX
card.translationY = transY
-
+
val scaleOffset = scale - 1.0f
val halfW = cardWidth / 2f
val halfH = cardHeight / 2f
-
+
binding.resizeHandleTopLeft.translationX = transX - halfW * scaleOffset
binding.resizeHandleTopLeft.translationY = transY - halfH * scaleOffset
-
+
binding.resizeHandleTopRight.translationX = transX + halfW * scaleOffset
binding.resizeHandleTopRight.translationY = transY - halfH * scaleOffset
-
+
binding.resizeHandleBottomLeft.translationX = transX - halfW * scaleOffset
binding.resizeHandleBottomLeft.translationY = transY + halfH * scaleOffset
-
+
binding.resizeHandleBottomRight.translationX = transX + halfW * scaleOffset
binding.resizeHandleBottomRight.translationY = transY + halfH * scaleOffset
}
@@ -2314,9 +2314,10 @@ abstract class GeneralKeyboardIME(
private var maxTranslationY = 0f
private val cornerHideHandler = android.os.Handler(android.os.Looper.getMainLooper())
- private val hideCornersRunnable = Runnable {
- animateHideCorners()
- }
+ private val hideCornersRunnable =
+ Runnable {
+ animateHideCorners()
+ }
private var initialTouchX = 0f
private var initialTouchY = 0f
@@ -2328,14 +2329,15 @@ abstract class GeneralKeyboardIME(
private fun showCorners() {
cornerHideHandler.removeCallbacks(hideCornersRunnable)
-
- val corners = listOf(
- binding.resizeHandleTopLeft,
- binding.resizeHandleTopRight,
- binding.resizeHandleBottomLeft,
- binding.resizeHandleBottomRight
- )
-
+
+ val corners =
+ listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight,
+ )
+
for (corner in corners) {
corner.animate().cancel()
corner.alpha = 1f
@@ -2349,21 +2351,22 @@ abstract class GeneralKeyboardIME(
}
private fun animateHideCorners() {
- val corners = listOf(
- binding.resizeHandleTopLeft,
- binding.resizeHandleTopRight,
- binding.resizeHandleBottomLeft,
- binding.resizeHandleBottomRight
- )
-
+ val corners =
+ listOf(
+ binding.resizeHandleTopLeft,
+ binding.resizeHandleTopRight,
+ binding.resizeHandleBottomLeft,
+ binding.resizeHandleBottomRight,
+ )
+
for (corner in corners) {
- corner.animate()
+ corner
+ .animate()
.alpha(0f)
.setDuration(300)
.withEndAction {
corner.visibility = View.GONE
- }
- .start()
+ }.start()
}
}
@@ -2384,68 +2387,73 @@ abstract class GeneralKeyboardIME(
updateFloatingViewsPosition(liveX, liveY, scale)
}
- private val resizeTouchListener = View.OnTouchListener { _, event ->
- if (!isFloatingMode) return@OnTouchListener false
+ private val resizeTouchListener =
+ View.OnTouchListener { _, event ->
+ if (!isFloatingMode) return@OnTouchListener false
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isResizing = true
+ showCorners()
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- isResizing = true
- showCorners()
-
- initialTouchX = event.rawX
- initialTouchY = event.rawY
- initialScale = PreferencesHelper.getFloatingScale(this, language)
+ initialTouchX = event.rawX
+ initialTouchY = event.rawY
+ initialScale = PreferencesHelper.getFloatingScale(this, language)
- val card = binding.keyboardCard
- val location = IntArray(2)
- card.getLocationOnScreen(location)
-
- keyboardCenterX = location[0] + card.width / 2f
- keyboardCenterY = location[1] + card.height / 2f
-
- initialDistance = Math.hypot(
- (event.rawX - keyboardCenterX).toDouble(),
- (event.rawY - keyboardCenterY).toDouble()
- ).toFloat()
-
- true
- }
- MotionEvent.ACTION_MOVE -> {
- if (!isResizing) return@OnTouchListener false
-
- val currentDistance = Math.hypot(
- (event.rawX - keyboardCenterX).toDouble(),
- (event.rawY - keyboardCenterY).toDouble()
- ).toFloat()
-
- if (initialDistance > 0) {
- var targetScale = initialScale * (currentDistance / initialDistance)
- targetScale = targetScale.coerceIn(0.7f, 1.3f)
- applyScaleAndPosition(targetScale)
+ val card = binding.keyboardCard
+ val location = IntArray(2)
+ card.getLocationOnScreen(location)
+
+ keyboardCenterX = location[0] + card.width / 2f
+ keyboardCenterY = location[1] + card.height / 2f
+
+ initialDistance =
+ Math
+ .hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble(),
+ ).toFloat()
+
+ true
}
- true
- }
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
- isResizing = false
- startHideCornersTimer()
-
- val finalScale = binding.keyboardCard.scaleX
- PreferencesHelper.setFloatingScale(this, language, finalScale)
-
- // Save live position so applyFloatingModeState restores it correctly
- val card = binding.keyboardCard
- val screenHeight = resources.displayMetrics.heightPixels.toFloat()
- val cardHeight = card.height.toFloat()
- val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
- PreferencesHelper.setFloatingX(this, language, card.translationX)
- PreferencesHelper.setFloatingY(this, language, liveY)
+ MotionEvent.ACTION_MOVE -> {
+ if (!isResizing) return@OnTouchListener false
+
+ val currentDistance =
+ Math
+ .hypot(
+ (event.rawX - keyboardCenterX).toDouble(),
+ (event.rawY - keyboardCenterY).toDouble(),
+ ).toFloat()
+
+ if (initialDistance > 0) {
+ var targetScale = initialScale * (currentDistance / initialDistance)
+ targetScale = targetScale.coerceIn(0.7f, 1.3f)
+ applyScaleAndPosition(targetScale)
+ }
+ true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ isResizing = false
+ startHideCornersTimer()
+
+ val finalScale = binding.keyboardCard.scaleX
+ PreferencesHelper.setFloatingScale(this, language, finalScale)
+
+ // Save live position so applyFloatingModeState restores it correctly
+ val card = binding.keyboardCard
+ val screenHeight = resources.displayMetrics.heightPixels.toFloat()
+ val cardHeight = card.height.toFloat()
+ val liveY = (screenHeight - cardHeight * finalScale) / 2f - card.translationY
+ PreferencesHelper.setFloatingX(this, language, card.translationX)
+ PreferencesHelper.setFloatingY(this, language, liveY)
- applyFloatingModeState()
- true
+ applyFloatingModeState()
+ true
+ }
+ else -> false
}
- else -> false
}
- }
@android.annotation.SuppressLint("ClickableViewAccessibility")
fun setupFloatingDragListener() {
@@ -2474,14 +2482,14 @@ abstract class GeneralKeyboardIME(
initialTranslationX = PreferencesHelper.getFloatingX(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationX, maxTranslationX)
initialTranslationY = PreferencesHelper.getFloatingY(this@GeneralKeyboardIME, language).coerceInSafe(minTranslationY, maxTranslationY)
-
+
showCorners()
true
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.rawX - initialX
val deltaY = event.rawY - initialY
-
+
var targetX = initialTranslationX + deltaX
var targetY = initialTranslationY - deltaY
@@ -2506,7 +2514,7 @@ abstract class GeneralKeyboardIME(
PreferencesHelper.setFloatingX(this@GeneralKeyboardIME, language, finalTargetX)
PreferencesHelper.setFloatingY(this@GeneralKeyboardIME, language, finalTargetY)
-
+
binding.root.requestLayout()
startHideCornersTimer()
true
@@ -2517,7 +2525,10 @@ abstract class GeneralKeyboardIME(
}
}
-private fun Float.coerceInSafe(bound1: Float, bound2: Float): Float {
+private fun Float.coerceInSafe(
+ bound1: Float,
+ bound2: Float,
+): Float {
val minVal = if (bound1 < bound2) bound1 else bound2
val maxVal = if (bound1 > bound2) bound1 else bound2
return this.coerceIn(minVal, maxVal)
diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
index 8e7932648..b5bde4204 100644
--- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
+++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt
@@ -640,42 +640,70 @@ object PreferencesHelper {
private const val FLOATING_Y = "floating_y"
private const val FLOATING_SCALE = "floating_scale"
- fun setIsFloatingModeEnabled(context: Context, language: String, enabled: Boolean) {
+ fun setIsFloatingModeEnabled(
+ context: Context,
+ language: String,
+ enabled: Boolean,
+ ) {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
sharedPref.edit { putBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), enabled) }
}
- fun getIsFloatingModeEnabled(context: Context, language: String): Boolean {
+ fun getIsFloatingModeEnabled(
+ context: Context,
+ language: String,
+ ): Boolean {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(FLOATING_MODE_ENABLED, language), false)
}
- fun setFloatingX(context: Context, language: String, x: Float) {
+ fun setFloatingX(
+ context: Context,
+ language: String,
+ x: Float,
+ ) {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), x) }
}
- fun getFloatingX(context: Context, language: String): Float {
+ fun getFloatingX(
+ context: Context,
+ language: String,
+ ): Float {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_X, language), 0f)
}
- fun setFloatingY(context: Context, language: String, y: Float) {
+ fun setFloatingY(
+ context: Context,
+ language: String,
+ y: Float,
+ ) {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), y) }
}
- fun getFloatingY(context: Context, language: String): Float {
+ fun getFloatingY(
+ context: Context,
+ language: String,
+ ): Float {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_Y, language), 0f)
}
- fun setFloatingScale(context: Context, language: String, scale: Float) {
+ fun setFloatingScale(
+ context: Context,
+ language: String,
+ scale: Float,
+ ) {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
sharedPref.edit { putFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), scale) }
}
- fun getFloatingScale(context: Context, language: String): Float {
+ fun getFloatingScale(
+ context: Context,
+ language: String,
+ ): Float {
val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE)
return sharedPref.getFloat(getLanguageSpecificPreferenceKey(FLOATING_SCALE, language), 1.0f)
}
From e10e7243d409215118aadb8f8d6140f96b9f4b1d Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:48:05 +0530
Subject: [PATCH 3/6] style: refine float button color and resize corner
handles position
---
.../keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt | 6 ------
app/src/main/res/drawable/ic_resize_corner.xml | 4 +++-
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index 61eba85bc..d3d901f5d 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -299,12 +299,6 @@ class KeyboardUIManager(
binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
- // Show the float button as "active" (lighter tint) when floating mode is already on
- if (listener.isFloatingModeActive()) {
- binding.floatBtn.backgroundTintList = ContextCompat.getColorStateList(context, R.color.light_key_color)
- binding.floatBtn.setTextColor(ContextCompat.getColor(context, R.color.theme_scribe_blue))
- }
-
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
binding.separator3.setBackgroundColor(separatorColor)
diff --git a/app/src/main/res/drawable/ic_resize_corner.xml b/app/src/main/res/drawable/ic_resize_corner.xml
index 0b339a5ac..868810a6d 100644
--- a/app/src/main/res/drawable/ic_resize_corner.xml
+++ b/app/src/main/res/drawable/ic_resize_corner.xml
@@ -4,9 +4,11 @@
android:viewportWidth="24"
android:viewportHeight="24">
+
+
From 8888f673236df4d4f3c237adfa93d5b51ae4c068 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Fri, 19 Jun 2026 06:25:19 +0530
Subject: [PATCH 4/6] fix: pass getKeyboardWidth() to all KeyboardBase
constructor calls to prevent keyboard cutting off on right in floating mode
---
.../keyboards/java/be/scri/services/GeneralKeyboardIME.kt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index ba975a39c..524c714ef 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -509,7 +509,7 @@ abstract class GeneralKeyboardIME(
override fun onActionUp() {
if (switchToLetters) {
keyboardMode = keyboardLetters
- keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType)
+ keyboard = KeyboardBase(this, getKeyboardLayoutXML(), enterKeyType, getKeyboardWidth())
val editorInfo = currentInputEditorInfo
if (editorInfo != null && editorInfo.inputType != InputType.TYPE_NULL && keyboard?.mShiftState != SHIFT_ON_PERMANENT) {
if (currentInputConnection.getCursorCapsMode(editorInfo.inputType) != 0) {
@@ -1294,7 +1294,7 @@ abstract class GeneralKeyboardIME(
this.keyboardMode = keyboardSymbols
getPrimarySymbolKeyboardLayoutXML()
}
- keyboard = KeyboardBase(this, keyboardXml, enterKeyType)
+ keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth())
keyboardView!!.setKeyboard(keyboard!!)
if (keyboardXml == R.xml.keys_symbols) {
handleModeChange(keyboardMode, keyboardView, this)
@@ -1322,7 +1322,7 @@ abstract class GeneralKeyboardIME(
this.keyboardMode = keyboardLetters
getKeyboardLayoutXML()
}
- keyboard = KeyboardBase(context, keyboardXml, enterKeyType)
+ keyboard = KeyboardBase(context, keyboardXml, enterKeyType, getKeyboardWidth())
if (this.keyboardMode == keyboardLetters) {
val wasShifted = keyboard?.mShiftState == SHIFT_ON_ONE_CHAR || keyboard?.mShiftState == SHIFT_ON_PERMANENT
if (wasShifted) {
From 43dcc7ecceda2fa57df0cae97e6305ca62a80230 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Wed, 24 Jun 2026 04:22:03 +0530
Subject: [PATCH 5/6] style(keyboard): rework floating keyboard design and
behavior as per review feedback
---
.../java/be/scri/helpers/KeyHandler.kt | 4 ++
.../be/scri/helpers/ui/KeyboardUIManager.kt | 46 +++++++++++++------
.../be/scri/services/GeneralKeyboardIME.kt | 1 +
.../main/java/be/scri/helpers/KeyboardBase.kt | 41 +++++++++++++++++
.../main/java/be/scri/views/KeyboardView.kt | 6 +++
.../res/drawable/ic_conjugate_command.xml | 9 ++++
.../main/res/drawable/ic_float_keyboard.xml | 9 ++++
.../main/res/drawable/ic_plural_command.xml | 14 ++++++
.../res/drawable/ic_translate_command.xml | 9 ++++
app/src/main/res/layout/input_method_view.xml | 31 +------------
10 files changed, 126 insertions(+), 44 deletions(-)
create mode 100644 app/src/main/res/drawable/ic_conjugate_command.xml
create mode 100644 app/src/main/res/drawable/ic_float_keyboard.xml
create mode 100644 app/src/main/res/drawable/ic_plural_command.xml
create mode 100644 app/src/main/res/drawable/ic_translate_command.xml
diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
index 790630911..ae8cb8a76 100644
--- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
+++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
@@ -100,6 +100,10 @@ class KeyHandler(
handleModeChangeKey()
true
}
+ KeyboardBase.KEYCODE_FLOAT_TOGGLE -> {
+ ime.toggleFloatingMode()
+ true
+ }
KeyboardBase.KEYCODE_SPACE -> handleSpaceKeyPress(previousWasLastKeySpace)
in KeyboardBase.NAVIGATION_KEYS -> {
handleNavigationKey(code)
diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index d3d901f5d..30c7a1efc 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -87,8 +87,8 @@ class KeyboardUIManager(
// UI Elements
var pluralBtn: Button? = binding.pluralBtn
- var floatBtn: Button? = binding.floatBtn
- var separatorFloat: View? = binding.separatorFloat
+ var floatBtn: Button? = null
+ var separatorFloat: View? = null
var emojiBtnPhone1: Button? = binding.emojiBtnPhone1
var emojiSpacePhone: View? = binding.emojiSpacePhone
var emojiBtnPhone2: Button? = binding.emojiBtnPhone2
@@ -121,7 +121,6 @@ class KeyboardUIManager(
binding.translateBtn.setOnClickListener { listener.onTranslateClicked() }
binding.conjugateBtn.setOnClickListener { listener.onConjugateClicked() }
binding.pluralBtn.setOnClickListener { listener.onPluralClicked() }
- binding.floatBtn.setOnClickListener { listener.onFloatClicked() }
binding.scribeKeyClose.setOnClickListener { listener.onCloseClicked() }
@@ -220,6 +219,7 @@ class KeyboardUIManager(
listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEachIndexed { index, button ->
button.visibility = View.VISIBLE
button.background = null
+ button.foreground = null
button.setTextColor(textColor)
button.text = HintUtils.getBaseAutoSuggestions(language).getOrNull(index)
button.isAllCaps = false
@@ -237,8 +237,6 @@ class KeyboardUIManager(
}
binding.separator1.visibility = View.GONE
- binding.floatBtn.visibility = View.GONE
- binding.separatorFloat.visibility = View.GONE
binding.ivInfo.visibility = View.GONE
binding.conjugateGridContainer.visibility = View.GONE
binding.keyboardView.visibility = View.VISIBLE
@@ -285,7 +283,7 @@ class KeyboardUIManager(
val buttonTextColor = if (isUserDarkMode) Color.WHITE else Color.BLACK
- listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn, binding.floatBtn).forEach { button ->
+ listOf(binding.translateBtn, binding.conjugateBtn, binding.pluralBtn).forEach { button ->
button.visibility = View.VISIBLE
button.background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded)
button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.theme_scribe_blue)
@@ -294,19 +292,36 @@ class KeyboardUIManager(
button.isAllCaps = false
}
- binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
- binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
- binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
- binding.floatBtn.text = floatPlaceholder[langAlias] ?: "Float"
+ val isFloating = listener.isFloatingModeActive()
+ if (isFloating) {
+ binding.translateBtn.text = ""
+ binding.conjugateBtn.text = ""
+ binding.pluralBtn.text = ""
+
+ binding.translateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_translate_command)
+ binding.conjugateBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_conjugate_command)
+ binding.pluralBtn.foreground = ContextCompat.getDrawable(context, R.drawable.ic_plural_command)
+
+ binding.translateBtn.foregroundGravity = android.view.Gravity.CENTER
+ binding.conjugateBtn.foregroundGravity = android.view.Gravity.CENTER
+ binding.pluralBtn.foregroundGravity = android.view.Gravity.CENTER
+ } else {
+ binding.translateBtn.text = translatePlaceholder[langAlias] ?: "Translate"
+ binding.conjugateBtn.text = conjugatePlaceholder[langAlias] ?: "Conjugate"
+ binding.pluralBtn.text = pluralPlaceholder[langAlias] ?: "Plural"
+
+ binding.translateBtn.foreground = null
+ binding.conjugateBtn.foreground = null
+ binding.pluralBtn.foreground = null
+ }
val separatorColor = (if (isUserDarkMode) GeneralKeyboardIME.DARK_THEME else GeneralKeyboardIME.LIGHT_THEME).toColorInt()
binding.separator2.setBackgroundColor(separatorColor)
binding.separator3.setBackgroundColor(separatorColor)
- binding.separatorFloat.setBackgroundColor(separatorColor)
val spaceInDp = 4
val spaceInPx = (spaceInDp * context.resources.displayMetrics.density).toInt()
- listOf(binding.separator2, binding.separator3, binding.separatorFloat).forEach { separator ->
+ listOf(binding.separator2, binding.separator3).forEach { separator ->
separator.setBackgroundColor(Color.TRANSPARENT)
val params = separator.layoutParams
params.width = spaceInPx
@@ -316,7 +331,6 @@ class KeyboardUIManager(
binding.separator1.visibility = View.GONE
binding.separator2.visibility = View.VISIBLE
binding.separator3.visibility = View.VISIBLE
- binding.separatorFloat.visibility = View.VISIBLE
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
@@ -677,8 +691,6 @@ class KeyboardUIManager(
*/
private fun setupDefaultButtonVisibility() {
pluralBtn?.visibility = View.VISIBLE
- floatBtn?.visibility = View.GONE
- separatorFloat?.visibility = View.GONE
emojiBtnPhone1?.visibility = View.GONE
emojiBtnPhone2?.visibility = View.GONE
emojiBtnTablet1?.visibility = View.GONE
@@ -687,6 +699,10 @@ class KeyboardUIManager(
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
+
+ binding.translateBtn.foreground = null
+ binding.conjugateBtn.foreground = null
+ pluralBtn?.foreground = null
}
/**
diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
index 524c714ef..33b2124b7 100644
--- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
+++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
@@ -1816,6 +1816,7 @@ abstract class GeneralKeyboardIME(
button.textSize = SUGGESTION_SIZE
button.setOnClickListener(null)
button.background = null
+ button.foreground = null
button.setTextColor(textColor)
button.setOnClickListener {
currentInputConnection?.commitText("$text ", 1)
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index d10e1a2c3..ee2f57019 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -37,6 +37,7 @@ class KeyboardBase {
val keyboardLetters: Int
fun isSearchBar(): Boolean
+ fun isFloatingModeActive(): Boolean
}
/** Horizontal gap default for all rows */
@@ -81,6 +82,7 @@ class KeyboardBase {
private const val WIDTH_DIVIDER = 10
const val KEYCODE_SHIFT = -1
const val KEYCODE_MODE_CHANGE = -2
+ const val KEYCODE_FLOAT_TOGGLE = -10
const val KEYCODE_ENTER = -4
const val KEYCODE_DELETE = -5
const val KEYCODE_SPACE = 32
@@ -591,6 +593,23 @@ class KeyboardBase {
key.gap = 0
}
+ if (!isSearchBar) {
+ if (key.code == KEYCODE_MODE_CHANGE) {
+ key.width = (mDisplayWidth * 0.115).toInt()
+ } else if (currentRow.mKeys.any { it.code == KEYCODE_FLOAT_TOGGLE }) {
+ if (key.code == ','.code) {
+ key.width = (mDisplayWidth * 0.075).toInt()
+ } else if (key.label == "_") {
+ key.width = (mDisplayWidth * 0.075).toInt()
+ } else if (key.code == KEYCODE_SPACE) {
+ val isLettersLayout = currentRow.mKeys.none { it.label == "_" }
+ if (isLettersLayout) {
+ key.width = (mDisplayWidth * 0.475).toInt()
+ }
+ }
+ }
+ }
+
mKeys!!.add(key)
if (key.code == KEYCODE_ENTER) {
val enterResourceId =
@@ -628,6 +647,28 @@ class KeyboardBase {
if (x > mMinWidth) {
mMinWidth = x
}
+ if (key.code == KEYCODE_MODE_CHANGE && !isSearchBar) {
+ val floatKey = Key(currentRow!!)
+ floatKey.code = KEYCODE_FLOAT_TOGGLE
+ floatKey.width = (mDisplayWidth * 0.08).toInt()
+ floatKey.gap = (mDisplayWidth * 0.005).toInt()
+ floatKey.x = x + floatKey.gap
+ floatKey.y = y
+ floatKey.height = currentRow.defaultHeight
+
+ val isFloating = provider?.isFloatingModeActive() == true
+ val floatResourceId = if (isFloating) {
+ R.drawable.ic_keyboard_dismiss
+ } else {
+ R.drawable.ic_float_keyboard
+ }
+ floatKey.icon = context.resources.getDrawable(floatResourceId, context.theme)
+ floatKey.icon?.setBounds(0, 0, floatKey.icon!!.intrinsicWidth, floatKey.icon!!.intrinsicHeight)
+
+ mKeys!!.add(floatKey)
+ currentRow.mKeys.add(floatKey)
+ x += floatKey.gap + floatKey.width
+ }
} else if (inRow) {
inRow = false
y += currentRow!!.defaultHeight
diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index fd9372cf5..1b5cf329b 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -746,6 +746,12 @@ class KeyboardView
}
}
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ mKeyboardChanged = true
+ invalidateAllKeys()
+ }
+
/**
* Compute the average distance between adjacent keys (horizontally and vertically)
* and square it to get the proximity threshold.
diff --git a/app/src/main/res/drawable/ic_conjugate_command.xml b/app/src/main/res/drawable/ic_conjugate_command.xml
new file mode 100644
index 000000000..e23f6748c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_conjugate_command.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_float_keyboard.xml b/app/src/main/res/drawable/ic_float_keyboard.xml
new file mode 100644
index 000000000..308a85fc9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_float_keyboard.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_plural_command.xml b/app/src/main/res/drawable/ic_plural_command.xml
new file mode 100644
index 000000000..450ca615a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plural_command.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_translate_command.xml b/app/src/main/res/drawable/ic_translate_command.xml
new file mode 100644
index 000000000..5120996c6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_translate_command.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml
index ea38161af..9f8bb26ce 100644
--- a/app/src/main/res/layout/input_method_view.xml
+++ b/app/src/main/res/layout/input_method_view.xml
@@ -251,36 +251,9 @@
android:contentDescription="@string/command_bar"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/separator_float"
- app:layout_constraintStart_toEndOf="@+id/separator_3"
- app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
-
From 77c2767a572e0e8de98607c3aa4600b792bd5f48 Mon Sep 17 00:00:00 2001
From: Prince Yadav <66916296+prince-0408@users.noreply.github.com>
Date: Wed, 24 Jun 2026 04:53:13 +0530
Subject: [PATCH 6/6] style: fix ktlint formatting and style issues in
KeyboardView, KeyboardBase, and KeyboardUIManager
---
.../java/be/scri/helpers/ui/KeyboardUIManager.kt | 1 -
.../main/java/be/scri/helpers/KeyboardBase.kt | 16 +++++++++-------
app/src/main/java/be/scri/views/KeyboardView.kt | 7 ++++++-
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
index 30c7a1efc..dd54cf087 100644
--- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
+++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
@@ -23,7 +23,6 @@ import be.scri.R.color.white
import be.scri.databinding.InputMethodViewBinding
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
-import be.scri.helpers.KeyboardLanguageMappingConstants.floatPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.pluralPlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.translatePlaceholder
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
index ee2f57019..e18aa8eec 100644
--- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt
+++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt
@@ -37,6 +37,7 @@ class KeyboardBase {
val keyboardLetters: Int
fun isSearchBar(): Boolean
+
fun isFloatingModeActive(): Boolean
}
@@ -655,16 +656,17 @@ class KeyboardBase {
floatKey.x = x + floatKey.gap
floatKey.y = y
floatKey.height = currentRow.defaultHeight
-
+
val isFloating = provider?.isFloatingModeActive() == true
- val floatResourceId = if (isFloating) {
- R.drawable.ic_keyboard_dismiss
- } else {
- R.drawable.ic_float_keyboard
- }
+ val floatResourceId =
+ if (isFloating) {
+ R.drawable.ic_keyboard_dismiss
+ } else {
+ R.drawable.ic_float_keyboard
+ }
floatKey.icon = context.resources.getDrawable(floatResourceId, context.theme)
floatKey.icon?.setBounds(0, 0, floatKey.icon!!.intrinsicWidth, floatKey.icon!!.intrinsicHeight)
-
+
mKeys!!.add(floatKey)
currentRow.mKeys.add(floatKey)
x += floatKey.gap + floatKey.width
diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt
index 1b5cf329b..16e223b96 100644
--- a/app/src/main/java/be/scri/views/KeyboardView.kt
+++ b/app/src/main/java/be/scri/views/KeyboardView.kt
@@ -746,7 +746,12 @@ class KeyboardView
}
}
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ override fun onSizeChanged(
+ w: Int,
+ h: Int,
+ oldw: Int,
+ oldh: Int,
+ ) {
super.onSizeChanged(w, h, oldw, oldh)
mKeyboardChanged = true
invalidateAllKeys()