Explorar o Compose MotionLayout

Tempo de leitura: 6 minutes

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.

 

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

  1. MotionScene – Fornece informações para que o MotionLayout seja animado entre vários ConstraintSets.
  2. 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 Composable
  • tag – 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 rastreie
  • side – 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 🍻.