Tutorial do Jetpack Compose: replicando o aplicativo de áudio Dribbble, parte 2

Feb 02 2023
Implementando uma IU totalmente personalizada com animações complexas: Action Panel No Exyte, tentamos contribuir com o código aberto o máximo possível, desde o lançamento de bibliotecas e componentes até a escrita de artigos e tutoriais. Um tipo de tutorial que fazemos é a replicação — pegando um componente de interface do usuário complexo, implementando-o usando novas estruturas e escrevendo um tutorial ao lado.

Implementando uma IU totalmente personalizada com animações complexas: Action Panel

Na Exyte , tentamos contribuir com o código aberto o máximo possível, desde o lançamento de bibliotecas e componentes até a escrita de artigos e tutoriais. Um tipo de tutorial que fazemos é a replicação — pegando um componente de interface do usuário complexo, implementando-o usando novas estruturas e escrevendo um tutorial ao lado. Começamos com o SwiftUI há alguns anos, mas desta vez finalmente incursionamos no Android, usando a estrutura declarativa de interface do usuário do Google: Jetpack Compose.

Este é o segundo artigo da série Dribbble Replicating. A postagem anterior demonstrou a implementação da barra de áudio animada de largura dinâmica . Este se concentrará na replicação do Painel de Ação.

Quebrando tudo

Considere como o painel foi projetado. Vejamos este painel com animações em câmera lenta:

Em cada item da lista de comentários, temos dois elementos:

  • CommentListItemque exibe as informações do comentário.
  • ActionPanel, um pequeno painel com botões de ação de comentário dentro. Ele rola CommentListItemquando o usuário clica no comentário.
  • BoxWithConstraints(
        modifier = modifier
            .fillMaxWidth()
            .background(color = MaterialTheme.colors.surface),
        contentAlignment = Alignment.Center,
    ) {
        val density = LocalDensity.current
        val maxWidth = with(density) { constraints.maxWidth.toDp() }
        val maxHeight = commentItemHeight - 16.dp
        CommentListItem(
            avatarId = comment.avatarId,
            name = comment.name,
            text = comment.text,
            date = comment.date,
            onClick = { onClick(comment) },
        )
        ActionPanel(
            modifier = Modifier
                .align(Alignment.CenterEnd)
                .padding(horizontal = 16.dp),
            maxWidth = maxWidth,
            maxHeight = maxHeight,
            isVisible = isActionsVisible,
            onActionClick = onActionClick
        )
    }
    

Quando targetStatemuda, a transição executará todas as suas animações filhas em direção aos valores de destino especificados para o novo targetState. Caso você não tenha trabalhado com animações antes, observe que labelé necessário que as ferramentas do Android Studio diferenciem as animações.

Vamos considerar a animação em expansão. Usamos transitionSpecpara definir a animação e especificar animações para cada par de initialStatee targetState. O keyframesparâmetro especifica as fases da animação com base nos valores dos instantâneos definidos em diferentes pontos no tempo durante a animação. A qualquer momento, o valor da animação será interpolado entre dois valores de quadro-chave. Neste caso, a animação de altura começa com uma certa velocidade para CORNER_DURATION, então continua em uma velocidade diferente para EXPAND_DURATION. Para informações detalhadas, você pode consultar a documentação oficial

val stateTransition = updateTransition(targetState = isVisible, label = "Visibility")
val targetWidth = stateTransition.animateDp(
    transitionSpec = {
        keyframes {
            durationMillis = CORNER_DURATION + EXPAND_DURATION
            if (targetState) {
                maxHeight at CORNER_DURATION
                maxWidth at CORNER_DURATION + EXPAND_DURATION
            } else {
                maxHeight at EXPAND_DURATION
                MIN_RADIUS.dp at EXPAND_DURATION + CORNER_DURATION
            }
        }
    },
    label = "Width"
) { value -> if (value) maxWidth else MIN_RADIUS.dp }

val targetHeight by stateTransition.animateDp(
    transitionSpec = {
        keyframes {
            durationMillis = CORNER_DURATION + EXPAND_DURATION
            if (targetState) {
                maxHeight at CORNER_DURATION
            } else {
                maxHeight at EXPAND_DURATION
                MIN_RADIUS.dp at EXPAND_DURATION + CORNER_DURATION
            }
        }
    },
    label = "Height"
) { value -> if (value) maxHeight else MIN_RADIUS.dp }

val contentAlphaProvider = remember {
    { (targetWidth.value - MIN_RADIUS.dp) / (maxWidth - MIN_RADIUS.dp) }
}