Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.itsaky.androidide

import android.annotation.SuppressLint
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlin.math.sqrt

internal class DraggableTouchListener(
context: Context,
private val calculator: FabPositionCalculator,
private val onSavePosition: (x: Float, y: Float) -> Unit,
private val onShowTooltip: () -> Unit
) : View.OnTouchListener {

private var initialX = 0f
private var initialY = 0f
private var initialTouchX = 0f
private var initialTouchY = 0f
private var isDragging = false
private var isLongPressed = false

private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop

private val gestureDetector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) {
if (!isDragging) {
isLongPressed = true
onShowTooltip()
}
}
}
)

@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
val parentView = v.parent as? ViewGroup ?: return false
val fab = v as? FloatingActionButton ?: return false

gestureDetector.onTouchEvent(event)

return when (event.action) {
MotionEvent.ACTION_DOWN -> handleActionDown(fab, event)
MotionEvent.ACTION_MOVE -> handleActionMove(fab, parentView, event)
MotionEvent.ACTION_UP -> handleActionUp(fab)
MotionEvent.ACTION_CANCEL -> handleActionCancel()
else -> false
}
}

private fun handleActionDown(fab: FloatingActionButton, event: MotionEvent): Boolean {
initialX = fab.x
initialY = fab.y
initialTouchX = event.rawX
initialTouchY = event.rawY
isDragging = false
isLongPressed = false
return true
}

private fun handleActionMove(fab: FloatingActionButton, parentView: ViewGroup, event: MotionEvent): Boolean {
val dX = event.rawX - initialTouchX
val dY = event.rawY - initialTouchY

if (!isDragging && isDraggingThresholdReached(dX, dY)) {
isDragging = true
}

if (isDragging) {
val safeBounds = calculator.getSafeDraggingBounds(parentView, fab)
fab.x = (initialX + dX).coerceIn(safeBounds.left.toFloat(), safeBounds.right.toFloat())
fab.y = (initialY + dY).coerceIn(safeBounds.top.toFloat(), safeBounds.bottom.toFloat())
}
return true
}

private fun handleActionUp(fab: FloatingActionButton): Boolean {
if (isDragging) {
onSavePosition(fab.x, fab.y)
} else if (!isLongPressed) {
fab.performClick()
}
return true
}

private fun handleActionCancel(): Boolean {
isDragging = false
isLongPressed = false
return true
}

private fun isDraggingThresholdReached(dX: Float, dY: Float): Boolean {
return sqrt((dX * dX + dY * dY).toDouble()) > touchSlop
}
}
108 changes: 108 additions & 0 deletions common-ui/src/main/java/com/itsaky/androidide/FabPositionCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.itsaky.androidide

import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.floatingactionbutton.FloatingActionButton

internal class FabPositionCalculator {

/**
* Calculate safe bounds for FAB positioning, accounting for system UI elements.
* Returns a Rect with the safe dragging area (left, top, right, bottom).
*/
fun getSafeDraggingBounds(parentView: ViewGroup, fabView: FloatingActionButton): Rect {
val defaultMargin = SizeUtils.dp2px(16f)
val margins = resolvePhysicalMargins(parentView, fabView, defaultMargin)

val insets = ViewCompat.getRootWindowInsets(parentView)
?.getInsets(WindowInsetsCompat.Type.systemBars())

val insetLeft = insets?.left ?: 0
val insetTop = insets?.top ?: 0
val insetRight = insets?.right ?: 0
val insetBottom = insets?.bottom ?: 0

return Rect(
insetLeft + margins.left,
insetTop + margins.top,
(parentView.width - fabView.width - insetRight - margins.right)
.coerceAtLeast(insetLeft + margins.left),
(parentView.height - fabView.height - insetBottom - margins.bottom)
.coerceAtLeast(insetTop + margins.top)
)
}

/**
* Validates if the given position is within safe bounds.
* If not, clamps it to the nearest valid position within the safe area.
*/
fun validateAndCorrectPosition(
x: Float,
y: Float,
parentView: ViewGroup,
fabView: FloatingActionButton
): Pair<Float, Float> {
val safeBounds = getSafeDraggingBounds(parentView, fabView)

val correctedX = x.coerceIn(safeBounds.left.toFloat(), safeBounds.right.toFloat())
val correctedY = y.coerceIn(safeBounds.top.toFloat(), safeBounds.bottom.toFloat())

return correctedX to correctedY
}

fun toRatio(value: Float, min: Int, availableSpace: Float): Float {
if (availableSpace > 0f) {
return ((value - min) / availableSpace).coerceIn(0f, 1f)
}
return 0f
}

fun fromRatio(ratio: Float, min: Int, availableSpace: Float): Float {
if (availableSpace > 0f) {
return min + (availableSpace * ratio.coerceIn(0f, 1f))
}
return min.toFloat()
}

private fun resolvePhysicalMargins(
parentView: ViewGroup,
fabView: FloatingActionButton,
defaultMargin: Int
): PhysicalMargins {
val layoutParams = fabView.layoutParams as? ViewGroup.MarginLayoutParams
?: return PhysicalMargins(
left = defaultMargin,
top = defaultMargin,
right = defaultMargin,
bottom = defaultMargin
)

val isRtl = parentView.layoutDirection == View.LAYOUT_DIRECTION_RTL
val start = layoutParams.marginStart.takeIf { it >= 0 }
val end = layoutParams.marginEnd.takeIf { it >= 0 }

val (resolvedLeft, resolvedRight) = if (isRtl) {
end to start
} else {
start to end
}

return PhysicalMargins(
left = resolvedLeft ?: layoutParams.leftMargin,
top = layoutParams.topMargin,
right = resolvedRight ?: layoutParams.rightMargin,
bottom = layoutParams.bottomMargin
)
}

private data class PhysicalMargins(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.itsaky.androidide

import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

internal class FabPositionRepository(private val context: Context) {
private companion object {
const val FAB_PREFS = "FabPrefs"
const val KEY_FAB_X_RATIO = "fab_x_ratio"
const val KEY_FAB_Y_RATIO = "fab_y_ratio"
}

private val prefs: SharedPreferences
get() = context.getSharedPreferences(FAB_PREFS, Context.MODE_PRIVATE)

fun savePositionRatios(xRatio: Float, yRatio: Float) {
prefs.edit().apply {
putFloat(KEY_FAB_X_RATIO, xRatio)
putFloat(KEY_FAB_Y_RATIO, yRatio)
apply()
}
}

suspend fun readPositionRatios(): Pair<Float, Float> = withContext(Dispatchers.IO) {
prefs.getFloat(KEY_FAB_X_RATIO, -1f) to prefs.getFloat(KEY_FAB_Y_RATIO, -1f)
}
}
Loading
Loading