2

Im trying to make a FLoatingActionButton that allows me to use onLongClick and gesture detection in case is draging up. Just creating the behavior of telegram or ws recording button.

Test 1 Using FloatingActionButton from Material3 dont work cause onLong click:

FloatingActionButton(
            modifier = Modifier
                .size(48.dp)
                .combinedClickable(
                    onClick = {
                        if (!textEmpty) {
                            onMessageChange(input.value.text)
                            input.value = TextFieldValue("")
                        }

                        if (recordingIsLock) {
                            stopRecord()
                        }
                    },
                    onLongClick = {
                        if (textEmpty) {
                            record()
                        }
                    }
                )
                .pointerInput(Unit) {
                    detectVerticalDragGestures(
                        onDragStart = {},
                        onDragCancel = {},
                        onDragEnd = {
                            if (!recordingIsLock) {
                                btnIndicatorHeight = 0F
                            }
                        },
                        onVerticalDrag = { change, dragAmount ->
                            // saber si no hay nada escrito
                            // saber si el drag es hacia arriba
                            // saber si esta grabando
                            if (textEmpty && change.position.y < 0 && dragAmount < 0 && isRecording) {
                                val aux = kotlin.math.abs(dragAmount)

                                btnIndicatorHeight += if (
                                    !recordingIsLock &&
                                    btnIndicatorHeight < 48
                                ) {
                                    println("!!!!")
                                    aux / 10
                                } else if (btnIndicatorHeight >= 48) {
                                    if (!recordingIsLock) {
                                        recordingIsLock = true
                                    }
                                    0F
                                } else {
                                    0F
                                }
                            }
                        })
                },
            onClick = {}
        ) {
            Icon(
                modifier = Modifier.size(24.dp),
                tint = MaterialTheme.colorScheme.background,
                imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
                contentDescription = null
            )

            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(btnIndicatorHeight.dp)
                    .background(color = MaterialTheme.colorScheme.primary),
                contentAlignment = Alignment.Center
            ) {
                if (!recordingIsLock)
                    Icon(
                        tint = Color.White,
                        imageVector = Icons.Outlined.Lock,
                        contentDescription = null
                    )
                else
                    LottieAnimation(
                        modifier = Modifier.size(30.dp),
                        composition = recordingAnimationComposition,
                        iterations = Int.MAX_VALUE
                    )
            }
        }

Test 2: Creating a custom component does not perform drag behavior.

ElevatedCard(
    modifier = modifier
        .size(48.dp)
                .combinedClickable(
                    onClick = {
                        if (!textEmpty) {
                            onMessageChange(input.value.text)
                            input.value = TextFieldValue("")
                        }

                        if (recordingIsLock) {
                            stopRecord()
                        }
                    },
                    onLongClick = {
                        if (textEmpty) {
                            record()
                        }
                    }
                )
                .pointerInput(Unit) {
                    detectVerticalDragGestures(
                        onDragStart = {},
                        onDragCancel = {},
                        onDragEnd = {
                            if (!recordingIsLock) {
                                btnIndicatorHeight = 0F
                            }
                        },
                        onVerticalDrag = { change, dragAmount ->
                            // saber si no hay nada escrito
                            // saber si el drag es hacia arriba
                            // saber si esta grabando
                            if (textEmpty && change.position.y < 0 && dragAmount < 0 && isRecording) {
                                val aux = kotlin.math.abs(dragAmount)

                                btnIndicatorHeight += if (
                                    !recordingIsLock &&
                                    btnIndicatorHeight < 48
                                ) {
                                    println("!!!!")
                                    aux / 10
                                } else if (btnIndicatorHeight >= 48) {
                                    if (!recordingIsLock) {
                                        recordingIsLock = true
                                    }
                                    0F
                                } else {
                                    0F
                                }
                            }
                        })
                }
        .background(
            color = MaterialTheme.colorScheme.surface,
            shape = RoundedCornerShape(10.dp)
        )
) {
    Box(contentAlignment = Alignment.Center) {
        component()
    }
}

1 Answers1

0

I found the way creating my custom button. It has comments in Spanish, im sure you all can translate it like I do with your answers ;) :

@Composable
fun ChatSendButton(
    enableToRecord: Boolean,
    isRecording: MutableState<Boolean>,
    record: () -> Unit,
    isRecordingLock: MutableState<Boolean>,
    recordLock: () -> Unit,
    stopRecord: () -> Unit,
    cancel: () -> Unit,
    onMessageSend: () -> Unit
) {

    val coroutineScope = rememberCoroutineScope()

    val hapticFeedback = LocalHapticFeedback.current

    val screenHeight = LocalConfiguration.current.screenHeightDp
    val screenWidth = LocalConfiguration.current.screenWidthDp


    var isVerticalDragging by remember {
        mutableStateOf(false)
    }

    var verticalDragging by remember {
        mutableStateOf(0F)
    }

    var isHorizontalDragging by remember {
        mutableStateOf(false)
    }

    var horizontalDragging by remember {
        mutableStateOf(0F)
    }

    // region Handle events
    val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }

    val btnState = remember {
        mutableStateOf(ChatSendButtonState.IDLE)
    }

    var time by remember { mutableStateOf(0L) }
    val press = remember {
        PressInteraction.Press(
            Offset.Zero
        )
    }

    val events = interactionSource.interactions.collectAsStateWithLifecycle(
        PressInteraction.Cancel(press)
    )

    val pressed = remember {
        PressInteraction.Release(press)
    }

    val longPressed = remember {
        PressInteraction.Release(press)
    }

    val release = remember {
        PressInteraction.Release(press)
    }

    val cancel = remember {
        PressInteraction.Cancel(press)
    }

    var wasLong by remember {
        mutableStateOf(false)
    }

    when (events.value) {
        pressed -> {
            if (btnState.value != ChatSendButtonState.PRESSED) {
                btnState.value = ChatSendButtonState.PRESSED

                if (!wasLong) {
                    if (!enableToRecord) {
                        onMessageSend()
                    }

                    if (isRecordingLock.value) {
                        stopRecord()
                    }
                }
            }
        }

        longPressed -> {
            if (btnState.value != ChatSendButtonState.LONG_PRESSED) {
                btnState.value = ChatSendButtonState.LONG_PRESSED

                wasLong = true

                hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
                record()
            }
        }

        release -> {
            if (btnState.value != ChatSendButtonState.RELEASED) {
                btnState.value = ChatSendButtonState.RELEASED

                if (isRecording.value && !isVerticalDragging && !isRecordingLock.value && !isHorizontalDragging) stopRecord()
                wasLong = false
            }
        }

        cancel -> {
        }
    }
    // endregion Handle events

    val recordingAnimationComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.recording_lottie_anim))

    val dynamicProperties = rememberLottieDynamicProperties(
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR_FILTER,
            value = SimpleColorFilter(MaterialTheme.colorScheme.onBackground.toArgb()),
            keyPath = arrayOf("**")
        ),
    )

    var popupControl by remember { mutableStateOf(false) }

    fun showLockIndicatorMsg() {
        if (!popupControl)
            coroutineScope.launchWithContext {
                delay(300)
                if (!isVerticalDragging)
                    popupControl = true
                delay(1000)
                popupControl = false
                null
            }
    }

    Surface(
        modifier = Modifier
            .size(48.dp)
            .invertedBounceClick()
            .graphicsLayer {
                translationY = verticalDragging
                translationX = horizontalDragging
            }
            // Modificador para arrastrar el botón horizontalmente y cancelar
            .pointerInput(Unit) {
                detectHorizontalDragGestures(
                    onDragStart = {
                        isHorizontalDragging = true
                    },
                    onDragCancel = {
                        isHorizontalDragging = false
                    },
                    onDragEnd = {
                        isHorizontalDragging = false

                        horizontalDragging = 0F
                    },
                    onHorizontalDrag = { change, dragAmount ->
                        if (!isVerticalDragging || ((kotlin.math.abs(verticalDragging) * 100 / screenHeight) <= 5)) {
                            val aux = kotlin.math.abs(dragAmount)

                            if (change.position.x < 0 && dragAmount < 0) {

                                if ((kotlin.math.abs(horizontalDragging) * 100 / screenWidth) > 20) {
                                    hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)

                                    cancel()

                                    horizontalDragging = 0F
                                } else {
                                    horizontalDragging -= aux - (aux * 3 / 10)
                                }

                            } else if (horizontalDragging < 0) {

                                horizontalDragging += aux
                            }
                        }
                    })
            }

            // Modificador para arrastrar el botón verticalmente y hacer lock al grabar
            .pointerInput(Unit) {
                detectVerticalDragGestures(
                    onDragStart = {
                        isVerticalDragging = true
                    },
                    onDragCancel = {
                        isVerticalDragging = false
                    },
                    onDragEnd = {
                        isVerticalDragging = false

                        if (isRecording.value && !isRecordingLock.value) stopRecord()

                        verticalDragging = 0F
                    },
                    onVerticalDrag = { change, dragAmount ->
                        // si puede grabar y esta grabando (despues de pasar por el long click) y no esta en lock ya
                        // entonces puede arrastrar

                        if (isRecording.value && !isRecordingLock.value) {
                            val aux = kotlin.math.abs(dragAmount)

                            if (change.position.y < 0 && dragAmount < 0) {

                                if ((kotlin.math.abs(verticalDragging) * 100 / screenHeight) > 30) {
                                    hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)

                                    recordLock()

                                    verticalDragging = 0F
                                } else {
                                    verticalDragging -= aux - (aux * 3 / 10)
                                }

                            } else if (verticalDragging < 0) {

                                verticalDragging += aux
                            }
                        }
                    })
            }

            // Modificador para detectar long press y arrastre al mismo tiempo
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        val countDownTimer = object : CountDownTimer(800, 10) {
                            override fun onTick(millisUntilFinished: Long) {
                                time += 10

                                if (time == 10L) {
                                    interactionSource.tryEmit(pressed)
                                }

                                if (time >= 500) {
                                    if (!wasLong) {
                                        interactionSource.tryEmit(longPressed)
                                        showLockIndicatorMsg()
                                    }

                                    this.cancel()
                                }
                            }

                            override fun onFinish() {
                                this.cancel()
                            }
                        }
                        countDownTimer.start()

                        tryAwaitRelease()

                        interactionSource.tryEmit(release)

                        countDownTimer.cancel()

                        time = 0
                    }
                )
            }
            .clip(RoundedCornerShape(15.dp))
            .indication(interactionSource, rememberRipple())
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            if (!isRecording.value)
                Icon(
                    modifier = Modifier.size(24.dp),
                    tint = MaterialTheme.colorScheme.onBackground,
                    imageVector = if (enableToRecord) Icons.Outlined.Mic else Icons.Outlined.Send,
                    contentDescription = null
                )
            else if (isRecordingLock.value || isRecording.value)
                LottieAnimation(
                    modifier = Modifier.fillMaxSize(),
                    composition = recordingAnimationComposition,
                    iterations = Int.MAX_VALUE,
                    dynamicProperties = dynamicProperties
                )

            AnimatedVisibility(
                visible = popupControl,
                enter = fadeIn(),
                exit = fadeOut(),
            ) {
                PopupContent()
            }
        }
    }
}

enum class ChatSendButtonState {
    PRESSED, LONG_PRESSED, RELEASED, IDLE
}

fun Modifier.invertedBounceClick() = composed {
    var buttonState by remember { mutableStateOf(ButtonState.Idle) }
    val scale by animateFloatAsState(if (buttonState == ButtonState.Pressed) 1.6f else 1f)
    val alphaC by animateFloatAsState(if (buttonState == ButtonState.Pressed) 1.6F else 1f)

    this
        .graphicsLayer {
            scaleX = scale
            scaleY = scale
            alpha = alphaC
        }
        .clickable(interactionSource = remember { MutableInteractionSource() },
            indication = null,
            onClick = { })
        .pointerInput(buttonState) {
            awaitPointerEventScope {
                buttonState = if (buttonState == ButtonState.Pressed) {
                    waitForUpOrCancellation()
                    ButtonState.Idle
                } else {
                    awaitFirstDown(false)
                    ButtonState.Pressed
                }
            }
        }
}

@Composable
fun PopupContent() {
    Popup(
        offset = IntOffset(0, -220)
    ) {
        Column(
            horizontalAlignment = Alignment.End,
            modifier = Modifier.padding(bottom = 15.dp)
        ) {
            Box(
                modifier = Modifier
                    .alpha(0.8F)
                    .background(color = Other.greyBackground, shape = RoundedCornerShape(15.dp))
                    .padding(10.dp)
            ) {
                Z17Label(
                    text = stringResource(id = R.string.hold_to_record),
                    color = MaterialTheme.colorScheme.tertiary
                )
            }

            Canvas(
                modifier = Modifier
                    .size(60.dp)
            ) {
                val path = Path().apply {
                    moveTo(size.width / 4, 0f)
                    lineTo(size.width * 3 / 4, 0f)
                    lineTo(size.width / 2, size.height / 6)
                    close()
                }
                drawPath(
                    path = path,
                    brush = SolidColor(Other.greyBackground.copy(alpha = 0.8F))
                )
            }
        }
    }
}

@Composable
fun LockAnimationIndicator(play: Boolean) {
    val offset by rememberInfiniteTransition().animateFloat(
        initialValue = 85f,
        targetValue = 105f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 700,
                easing = FastOutSlowInEasing
            ),
            repeatMode = RepeatMode.Reverse
        )
    )

    Box(contentAlignment = Alignment.BottomCenter) {
        Z17BasePicture(
            source = R.drawable.lock_indicator,
            filterQuality = FilterQuality.High,
            contentScale = ContentScale.FillBounds,
            colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onBackground),
            modifier = Modifier
                .offset(y = if (play) -(90.dp) else 1000.dp)
                .graphicsLayer {
                    scaleY = if (play) 10F else 1F
                }
                .size(width = 25.dp, height = 10.dp)
        )

        Z17BasePicture(
            source = Icons.Outlined.KeyboardArrowUp,
            colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.background),
            modifier = Modifier
                .offset(y = if (play) -(offset.dp) else 0.dp)
                .size(24.dp)
        )
    }
}

@Composable
fun CancelAnimationIndicator(play: Boolean) {
    val offset by rememberInfiniteTransition().animateFloat(
        initialValue = 10f,
        targetValue = 45f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 400,
                easing = FastOutSlowInEasing
            ),
            repeatMode = RepeatMode.Reverse
        )
    )

    Z17BasePicture(
        source = Icons.Outlined.KeyboardArrowLeft,
        colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onBackground),
        modifier = Modifier
            .offset(x = if (play) -(offset.dp) else 0.dp)
            .size(24.dp)
    )
}

enter image description here