Exemplos de Código: Capítulo 07 — Modifiers Avançados e Gestos no Compose

Especialização em Mobile Nativo com Android • Demonstrações Práticas Compiláveis


💻 Código de Demonstração: graphicsLayer, Gestos Multi-Touch e Layout Customizado

O código abaixo demonstra o controle de baixo nível de layouts e efeitos visuais no Jetpack Compose:

  1. Aplicação de graphicsLayer para transformações de rotação, escala e opacidade com aceleração por hardware (GPU).
  2. Detecção de gestos multi-touch (arraste, zoom e rotação) com pointerInput e detectTransformGestures.
  3. Criação de um Layout Customizado medindo os filhos (measurables.map { it.measure(constraints) }) e posicionando-os no canvas (layout() { placeable.placeRelative() }).
package br.com.portal.mobile.android.cap07

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TouchApp
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt

// =====================================================================
// 1. Cartão Interativo com Gestos Multi-Touch e graphicsLayer
// =====================================================================

@Composable
fun CartaoInterativoGestos(modifier: Modifier = Modifier) {
    var escala by remember { mutableFloatStateOf(1f) }
    var rotacao by remember { mutableFloatStateOf(0f) }
    var deslocamento by remember { mutableStateOf(Offset.Zero) }

    Box(
        modifier = modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background),
        contentAlignment = Alignment.Center
    ) {
        ElevatedCard(
            shape = RoundedCornerShape(24.dp),
            modifier = Modifier
                .offset { IntOffset(deslocamento.x.roundToInt(), deslocamento.y.roundToInt()) }
                .graphicsLayer(
                    scaleX = maxOf(0.6f, minOf(escala, 3f)),
                    scaleY = maxOf(0.6f, minOf(escala, 3f)),
                    rotationZ = rotacao,
                    shadowElevation = 16f
                )
                .pointerInput(Unit) {
                    // Detecta Pinça (Pinch-to-zoom), Rotação e Pan simultaneamente
                    detectTransformGestures { _, pan, zoom, rotation ->
                        escala *= zoom
                        rotacao += rotation
                        deslocamento += pan
                    }
                }
                .size(240.dp, 160.dp)
        ) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.primaryContainer)
                    .padding(16.dp),
                contentAlignment = Alignment.Center
            ) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Icon(
                        imageVector = Icons.Default.TouchApp,
                        contentDescription = null,
                        modifier = Modifier.size(48.dp),
                        tint = MaterialTheme.colorScheme.primary
                    )
                    Spacer(modifier = Modifier.height(8.dp))
                    Text(
                        text = "Arraste, Gire e Dê Zoom",
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Bold,
                        color = MaterialTheme.colorScheme.onPrimaryContainer
                    )
                }
            }
        }
    }
}

// =====================================================================
// 2. Layout Customizado: DiagonalStackLayout
// =====================================================================

/**
 * Mede e posiciona seus nós filhos em cascata diagonal decrescente.
 */
@Composable
fun DiagonalStackLayout(
    modifier: Modifier = Modifier,
    espacamentoX: Int = 30,
    espacamentoY: Int = 40,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 1. Fase de Medição: Mede cada filho com as restrições fornecidas
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // Calcula a largura e altura total acumulada necessária
        var larguraTotal = 0
        var alturaTotal = 0
        placeables.forEachIndexed { index, placeable ->
            larguraTotal = maxOf(larguraTotal, placeable.width + (index * espacamentoX))
            alturaTotal = maxOf(alturaTotal, placeable.height + (index * espacamentoY))
        }

        // 2. Fase de Posicionamento (Placement)
        layout(larguraTotal, alturaTotal) {
            var posX = 0
            var posY = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = posX, y = posY)
                posX += espacamentoX
                posY += espacamentoY
            }
        }
    }
}

// =====================================================================
// 3. Previews
// =====================================================================

@Preview(showBackground = true)
@Composable
fun DiagonalLayoutPreview() {
    MaterialTheme {
        DiagonalStackLayout(modifier = Modifier.padding(24.dp)) {
            Box(modifier = Modifier.size(100.dp, 60.dp).background(Color(0xFFE91E63), RoundedCornerShape(8.dp)))
            Box(modifier = Modifier.size(100.dp, 60.dp).background(Color(0xFF9C27B0), RoundedCornerShape(8.dp)))
            Box(modifier = Modifier.size(100.dp, 60.dp).background(Color(0xFF2196F3), RoundedCornerShape(8.dp)))
        }
    }
}