Explorar o Compose MotionLayout
O MotionLayout é um tipo de layout que ajuda você a gerenciar o movimento e a animação de widgets em seu aplicativo. Como você está aqui, deve saber um pouco sobre o MotionLayout.
O MotionLayout é uma subclasse do ConstraintLayout. Ele é usado para redimensionar, mover e animar exibições com as quais os usuários interagem.
Neste artigo, exploraremos o Compose MotionLayout. Implementaremos uma interface de usuário bem legal com animação de movimento no Jetpack compose.
Aqui está o que vamos implementar nesta postagem do blog.
Conteudo
1. Configuração básica
Crie um projeto com uma atividade vazia do jetpack compose.
Adicione a seguinte dependência
implementation "androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha13"
Criar Composable
Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { RecipeDetail() }
O MotionLayout para Compose assume:
- Dois ConstraintsSet (início e fim), uma Transição e progresso
- Um MotionScene e motionLayoutState
Vamos usar o MotionalLayout com o MotionScene
Adicionar MotionLayout em RecipeDetail
val motionState = rememberMotionLayoutState() MotionLayout( motionScene = /* our motion scene json */, motionLayoutState = motionState, modifier = Modifier .fillMaxSize() .background(LightGray) ) { ... }
Adicionar @OptIn(ExperimentalMotionApi::class)
a RecipeDetail
Antes de prosseguir, vamos dar uma olhada rápida em algumas propriedades importantes do MotionLayout
- MotionScene – Fornece informações para que o MotionLayout seja animado entre vários ConstraintSets.
- MotionLayoutState – Lê e manipula o estado de um MotionLayout Composable
Atualmente, o MotionScene suporta apenas uma sintaxe JSON.
Consulte o Wiki oficial do GitHub para aprender a sintaxe.
2. Projetar a interface do usuário e aplicar o conjunto de restrições às exibições
Crie um arquivo JSON para a cena de movimento em res/raw/motion_scene.json
{ ConstraintSets: { start: { .... }, end: { .... } } }
Aqui, o start
contém todas as restrições para o estado inicial do movimento, e o end
inclui restrições para o estado final.
Agora, adicione o conteúdo do arquivo JSON.
val context = LocalContext.current val motionScene = remember { context.resources .openRawResource(R.raw.motion_scene) .readBytes() .decodeToString() } MotionLayout( motionScene = MotionScene(content = motionScene), ) { ... }
Você pode adicionar diretamente a cadeia de caracteres da cena de movimento como conteúdo, mas, à medida que a tela e o conteúdo crescem, isso se torna complexo, portanto, para facilitar e limpar, usamos JSON.
Adicionar o Header image
Image( painter = painterResource(id = R.drawable.cake), contentDescription = "", contentScale = ContentScale.Crop, modifier = Modifier .layoutId("headerImage") )
layoutId
– A cadeia de caracteres de identificação exclusiva atribuída ao Composabletag
– Uma string para representar um grupo de Composables que pode ser afetado por uma função ConstraintLayout.
Vamos adicionar um conjunto de restrições para uma imagem de cabeçalho
start: { headerImage: { width: "spread", height: 250, top: ['parent', 'top', 0], start: ['parent', 'start', 0], end: ['parent', 'end', 0], translationY: 0, alpha: 1 } }, end: { headerImage: { width: "spread", height: 250, top: ['parent', 'top', 0], start: ['parent', 'start', 0], end: ['parent', 'end', 0], translationY: -250, alpha: 0.3, } }
Isso define o tamanho da nossa imagem de cabeçalho, suas restrições de topo, início e fim. O movimento começa com translaçãoY 0 e alfa 1 e, no final, a imagem é transladada para -250 com alfa de 0,3
Adicione o white background sheet
Box( modifier = Modifier .fillMaxHeight() .background(White, shape = RoundedCornerShape(topStart = corners, topEnd = corners)) .layoutId("contentBg") )
Defina a cena de movimento para essa visualização.
start: { ... contentBg: { width: 'spread', height: 'spread', start: ['parent', 'start',16], end: ['parent', 'end',16], top: ['parent','top', 200], bottom: ['parent','bottom'], } }, end: { ... contentBg: { width: 'spread', height: 'spread', start: ['parent', 'start'], end: ['parent', 'end'], top: ['parent','top'], bottom: ['parent','bottom'], }
Inicialmente, isso definirá uma margem de 16dp na horizontal e 200dp na parte superior. E, no final, a visualização preencherá a tela inteira.
Adicione title e subTitle com animated Divider
Text( text = "Fresh Strawberry Cake", fontSize = 22.sp, textAlign = TextAlign.Center, fontWeight = FontWeight.SemiBold, modifier = Modifier .layoutId("title") .fillMaxWidth() .padding(10.dp) ) Divider( Modifier .layoutId("titleDivider") .fillMaxWidth() .padding(horizontal = 34.dp) ) Text( text = "by John Kanell", fontSize = 16.sp, textAlign = TextAlign.Center, color = Gray, fontStyle = FontStyle.Italic, modifier = Modifier .layoutId("subTitle") .fillMaxWidth() .padding(6.dp) )
Define constrain set
start: { title: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['parent','top',200], }, titleDivider: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['title','bottom'], }, subTitle: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['titleDivider','bottom'], }, subTitleDivider: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['subTitle','bottom'], } }, end: { title: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['parent','top', 6], }, titleDivider: { width: 'spread', height: 'wrap', start: ['parent', 'start',34], end: ['parent', 'end', 34], top: ['title','bottom'], }, subTitle: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['titleDivider','bottom'], }, subTitleDivider: { visibility: 'gone', width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['subTitle','bottom'], } }
Autoexplicativo, certo!!! Para o título, definimos uma margem superior inicial de 200dp, que se torna 6dp no final. O mesmo vale para o divisor, pois definimos margens diferentes para ambos os estados. Além disso, adicionamos visibility
ao subTitleDivider
, pois não precisamos mostrá-lo quando a planilha se expande.
Adicionar os elementos restantes da interface do usuário
Text( modifier = Modifier .layoutId("date") .fillMaxWidth() .padding(6.dp), text = "September, 2022", fontSize = 16.sp, textAlign = TextAlign.Center, color = Gray ) Row( modifier = Modifier .layoutId("actions") .background(Color.DarkGray), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { ... // Three Icon buttons } Text( text = "Some long text...", modifier = Modifier.fillMaxHeight() .layoutId("text") .padding(horizontal = 16.dp), fontSize = 12.sp, )
Define constrain set
start: { date: { width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['subTitleDivider','bottom'], }, actions: { width: 'spread', height: 50, start: ['parent', 'start',16], end: ['parent', 'end',16], top: ['date','bottom'], }, text: { width: 'spread', height: 'spread', start: ['parent', 'start',16], end: ['parent', 'end',16], top: ['actions','bottom', 16], bottom: ['parent','bottom'] } }, end: { date: { visibility: 'gone', width: 'spread', height: 'wrap', start: ['parent', 'start'], end: ['parent', 'end'], top: ['subTitleDivider','bottom'], }, actions: { width: 'spread', height: 70, start: ['parent', 'start'], end: ['parent', 'end'], top: ['subTitle','bottom'], }, text: { width: 'spread', height: 'spread', start: ['parent', 'start'], end: ['parent', 'end'], top: ['actions','bottom',16], bottom: ['parent','bottom'], } }
Vamos executar o aplicativo e verificar nossa interface do usuário
Legal!!! Não é mesmo?
Agora vamos tornar seu conteúdo deslizável.
3. Adicionar transição de deslizamento
O MotionLayout nos permite manipular o deslizamento com Transitions
, que definem os parâmetros de interpolação entre dois ConstraintSets. Para obter mais detalhes sobre Transições, consulte o wiki oficial
Definiremos a propriedade onSwipe
em Transitions
. OnSwipe permite o controle de gestos com deslizamento.
As principais propriedades de onSwipe
são
anchor
– O Composable que você deseja que seu dedo rastreieside
– O lado da âncora é o Composable que seu dedo rastrearádirection
– direção do seu movimento
Vamos definir isso para o nosso elemento contentBg
Transitions: { default: { from: 'start', to: 'end', onSwipe: { anchor: 'contentBg', direction: 'up', side: 'top' }, } }
É isso e terminamos com a transição.
4. Adicionar uma propriedade personalizada para alterar a cor do background
No constraints, podemos definir a propriedade personalizada para os elementos da interface do usuário.
start: { actions: { ... custom: { background: '#444444' } } }, end{ actions: { ... custom: { background: '#9b0024' } } }
Agora, como acessar essa propriedade?
val properties = motionProperties("actions")
Vamos obter a cor e defini-la como View
Row( modifier = Modifier .layoutId("actions") .background(properties.value.color("background")), ) { ... }
Agora, execute seu aplicativo para ver o resultado.
Para concluir
O Motion Layout é atualmente uma API experimental, portanto, pode parecer um pouco complicado e não tão útil, mas é uma ferramenta potente. É fácil lidar com movimentos complexos com o MotionLayout. Vamos esperar que a tag Experimental seja removida do MotionLayout e continuar explorando-o 🍻.