bbillings
bbillings

Reputation: 21

How can I detect press+drag gestures inside one or more Jetpack Compose button objects that originiate outside of the buttons?

I am writing a client application in Jetpack Compose and Kotlin, aiming for MVVM architecture. I have a Composable view that displays a set of custom button Composable objects within a box. This app is a client for a 3rd party server, so I am limited in how button size/location data is provided and how I can handle it.

Here is abridged ButtonViewScreen code that includes a for loop to display however many ViewButtons are assigned to a given ViewScreen:

@Composable
fun ButtonViewScreen (
    navigateBack: () -> Unit
) {

  val snackbarHostState = remember { SnackbarHostState() }
  val uiState by viewModel.uiState.collectAsState

  // Get current screen width and height in dp
  val currentLocalConfig = LocalConfiguration.current
  val screenHeight = currentLocalConfig.screenHeightDp - 20
  val screenWidth = currentLocalConfig.screenWidthDp - 20

  Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) {
        Box (
            modifier = Modifier
                .fillMaxSize()
                .background(Color(deckBackgroundColor))
                .padding(top = 10.dp, bottom = 10.dp, start = 12.dp)
        )
        {
            
            if (uiState.currentButtondata.isNotEmpty()) {
                for (button in uiState.currentButtondata) {

                    ViewButton(
                        buttonId = button.buttonId,
                        allowDragPress = button.allowDragPress,
                        xOffset = (singleButtonWidth * (uiState.currentViewData.totalColumns * button.xPos) + (10 * ((uiState.currentViewData.totalColumns * button.xPos) - 1))).dp,
                        yOffset = (singleButtonHeight * (uiState.currentViewData.totalRows * button.yPos) + (10 * ((uiState.currentViewData.totalRows * button.yPos) - 1))).dp,
                        labelText = button.textLabel,
                        border = button.border,
                        buttonColor = button.backgroundColor,
                        onClick = {
                            viewModel.triggerViewButtonPress(button.buttonId)
                        },
                        onRelease = {
                            viewModel.triggerViewButtonRelease(button.buttonId)
                            if (!button.switchView.isNullOrEmpty()) {
                                viewModel.changeActiveView(button.switchView!!)
                            }
                        },
                        height = ((screenHeight * button.height) ).dp,
                        width = ((screenWidth * button.width) ).dp
                    )
                }
            }
        }
    }
}

The full code works as expected, with all the ViewButtons showing and handling onClick events.

I'm trying to figure out a way to detect when someone does a press somewhere on the parent ButtonViewScreen's Box composable and drags the press over one or more buttons. The app should trigger the onClick lambda function provided allowDragPress is true for that button.

For an example, there could be 3 or 4 buttons in a column, and the user should be able (Given allowDragPress = true) to swipe over the entire column to trigger all of the relevant onClick functions (onRelease is not required).

Hopefully this makes sense, it's a bit hard to describe without visuals.

I have attempted using multiple pointerInput modifiers in the custom buttons with detect transform and drag gestures, although they only detect gestures that originate within the button.

Upvotes: 1

Views: 861

Answers (1)

bbillings
bbillings

Reputation: 21

I ended up coming up with a bit of a hack way to make this work. I added a MutableInteractionSource to track the ID from the start of the drag gesture, and a MutableStateFlow to track the Offset coordinates. I then pass both of these to ViewButtons as additional parameters.

The state flows are updated via a pointerInput modifier with detectDragGestures for onStart and onDrag:

    @Composable
    fun ButtonViewScreen (
        navigateBack: () -> Unit
    ) {

      // New Interaction Source
      val interactionSource = remember { MutableInteractionSource() }
      val dragTouchPoint = remember { MutableStateFlow(Offset.Zero) }
    
      val snackbarHostState = remember { SnackbarHostState() }
      val uiState by viewModel.uiState.collectAsState
    
      // Get current screen width and height in dp
      val currentLocalConfig = LocalConfiguration.current
      val screenHeight = currentLocalConfig.screenHeightDp - 20
      val screenWidth = currentLocalConfig.screenWidthDp - 20
    
      Scaffold(
            snackbarHost = { SnackbarHost(snackbarHostState) }
        ) {
            Box (
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color(deckBackgroundColor))
                    .padding(top = 10.dp, bottom = 10.dp, start = 12.dp)
                    .pointerInput(Unit) {
                var interaction: DragInteraction.Start?
                detectDragGestures(
                    onDragStart = {
                        coroutineScope.launch {
                            interaction = DragInteraction.Start()
                            interaction?.run {
                                interactionSource.emit(this)
                            }

                        }
                    },
                    onDrag = { change: PointerInputChange, _: Offset ->
                        coroutineScope.launch {
                            dragTouchPoint.emit(change.position)
                        }
                    }
                )
            }
            )
            {
                
                if (uiState.currentButtondata.isNotEmpty()) {
                    for (button in uiState.currentButtondata) {
    
                        ViewButton(
                            buttonId = button.buttonId,
                            allowDragPress = button.allowDragPress,
                            xOffset = (singleButtonWidth * (uiState.currentViewData.totalColumns * button.xPos) + (10 * ((uiState.currentViewData.totalColumns * button.xPos) - 1))).dp,
                            yOffset = (singleButtonHeight * (uiState.currentViewData.totalRows * button.yPos) + (10 * ((uiState.currentViewData.totalRows * button.yPos) - 1))).dp,
                            labelText = button.textLabel,
                            border = button.border,
                            buttonColor = button.backgroundColor,
                            onClick = {
                                viewModel.triggerViewButtonPress(button.buttonId)
                            },
                            onRelease = {
                                viewModel.triggerViewButtonRelease(button.buttonId)
                                if (!button.switchView.isNullOrEmpty()) {
                                    viewModel.changeActiveView(button.switchView!!)
                                }
                            },
                            height = ((screenHeight * button.height) ).dp,
                            width = ((screenWidth * button.width) ).dp,
                            dragInteractionSource = interactionSource,
                            dragTouchPoint = dragTouchPoint
                        )
                    }
                }
            }
        }
    }

I added a conditional LaunchedEffect in the ViewButton composable that watches over the new flow values and acts accordingly when the X/Y coordinates of the Offset fall over the ViewButton instance once per gesture ID:

val lastDragInteractionId = remember { mutableStateOf("") }

// Drag Handler in ViewButton composable
    if (allowDragPress) {
        LaunchedEffect(dragTouchPoint) {

            dragTouchPoint.collect {
                    currentTouchPoint ->

                if (xMin <= currentTouchPoint.x.toInt() &&
                    currentTouchPoint.x.toInt() <= xMax &&
                    yMin <= currentTouchPoint.y.toInt() &&
                    currentTouchPoint.y.toInt() <= yMax) {

                    dragInteractionSource.interactions.collectLatest {
                                interaction ->
                            if (lastDragInteractionId.value != interaction.toString()) {
                                onClick()
                                lastDragInteractionId.value = interaction.toString()
                            }
                        }

                }
            }
        }
    }

I'm sure there's a more elegant way of handling these gestures, although this seems to be working well enough.

Upvotes: 1

Related Questions