Animações em React Native – Ep 6

Hoje nós olhamos para algo interessante. Nós olhamos para como recriar o incrível sorvete de boba website. Também inspirado por William‘s vídeo. Código na secção de conclusão.

Se for ao sítio Web, terá uma sensação muito melhor do que a do gif. Agora, como podemos implementar isto

Temos um elemento de texto que é estático e comum a todos. Temos círculos amarelos na parte inferior, nos quais, se tocar, a página (ou seja, o ecrã azul com gelado) mudará de página.

Para começar, o código será algo como isto

const Home = () => {
// there are stuff here, which i have omitted for simplicity.
// This is a very simplified version of what is happening.

const [Activeitem, SetActiveitem] =useState(iceCreams[0]);
let pressHandler = (index) =>{
setActiveitem(iceCreams[index]);
}

return (

<View style={styles.container}>

<MainPic
bgColor={Activeitem.backgroundColor}
picture={Activeitem.doublePicture}
key={`main_pic${Activeitem.key}`}
index={index}
count={Activeitem.key}
/>

<TextTop />

<View style={styles.circleContainer}>
{iceCreams.map((item, index) => {
return (
<CirclePic
bgColor={item.backgroundColor}
picture={item.singlePicture}
index={index}
pressHandler={pressHandler}
key={`circles_pic${item.key}_${index}`}
/>
);
})}
</View>

</View>
);
};

Aqui mapeamos muitos círculos e, ao premir um deles, o ativo é alterado e nós mudamos o mainPic.

Temos de alterar um pouco a arquitetura do <MainPic> (ecrã azul com gelado).

Temos uma pilha constituída por 2 ecrãs, um para o ecrã existente e outro para o que aparece agora.

  {stack.map((item, index) => {
return (
<MainPic
bgColor={item.backgroundColor}
picture={item.doublePicture}
key={`main_pic${item.key}`}
index={index}
count={item.key}
getCirclePos={getCirclePos}
allDataLoaded={allDataLoaded}
/>
);
})}

A pilha é definida pelo PressHandler. O último gelado é o gelado que tem os detalhes do ecrã atual antes de o tocar.

    let pressHandler = (index) => {
if (lastcream.key == index) return;
setActive(() => index);
setStack([lastcream, iceCreams[index]]);
setLastcream(iceCreams[index]);
};

Agora, nesta altura, devemos ter algo como isto.

Ideia

Agora temos de adicionar uma animação de máscara. Adicionamos uma máscara circular no carregamento. Em seguida, aumentamos o raio do círculo de modo a que o ecrã inteiro possa ser visto. Agora que temos 2 imagens principais, a nova imagem principal (azul) será revelada quando o círculo for totalmente expandido e a imagem principal antiga (verde) permanecerá lá.

Se conseguir esta imagem, penso que terá o que é preciso para a construir.

Mascaramento

Para criar uma máscara utilizamos o MaskedView e adicionamos o círculo como vista mascarada, para animar o tamanho aumentando a escala utilizamos estilos reanimados.

const MainPic = ({
bgColor,
picture,
index,
count,
allDataLoaded,
getCirclePos,
}) => {
let offset = useSharedValue(0);
useEffect(() => {
offset.value = withTiming(4, { duration: 1500 });
}, []);

const r = width / 2;

const animatedStyles3 = useAnimatedStyle(() => ({
transform: [{ scale: offset.value }],
}));

return (
<MaskedView
style={StyleSheet.absoluteFill}
maskElement={
<Animated.View
style={[
{
width: r * 2,
height: r * 2,
borderRadius: r,
},
animatedStyles3,
]}
></Animated.View>
}
>
<View style={[styles.container, { backgroundColor: bgColor }]}>
<View style={styles.image_container}>
<Animated.Image
source={picture}
style={styles.picturestyle}
/>
</View>
</View>
</MaskedView>
);
};

Agora temos a animação do círculo a aparecer. Mas há um problema, o que é que se passa?

A máscara de círculos começa sempre na mesma posição e não a partir dos círculos no fundo, de forma dinâmica.

Por isso, o que precisamos, precisamos das posições exactas do círculo para podermos alterar o ponto de partida da máscara em conformidade.

Encontrar as posições dos círculos

Utilizamos a função de medida para obter o x,y, a largura e a altura. Este é o aspeto dos círculos


const CirclePic = () => {

let circleRef = useRef(null);
let Magic = () => {
if (circleRef.current) {
circleRef.current.measure((x,y,width, height, pageX, pageY) => {
let obj = { x: pageX, y: pageY, h: height, w: width, count:index };
modifyCirclePos(index, obj);
});
}
};

return (
<TouchableWithoutFeedback onPress={() => pressHandler(index)}>
<View>
<Image
ref={circleRef}
source={picture}
onLoadEnd={() => Magic()}
/>
</View>
</TouchableWithoutFeedback>
);
};

A função Magic faz o truque.

Alterar o ponto de partida da máscara

Agora passamos esta informação para a Imagem Principal e alteramos o ponto de partida ajustando os Valores de Margem. Passamos este valor para a deslocação como {x: valor, y: valor, w: valor, h: valor}

<MaskedView
style={StyleSheet.absoluteFill}
maskElement={
<Animated.View
style={[
{
width: r * 2,
height: r * 2,
borderRadius: r,
},
displacement && displacement.x
? {
marginLeft:
displacement.x - r + displacement.w / 2,
}
: null,
displacement && displacement.y
? {
marginTop:
displacement.y - r + displacement.h / 2,
}
: null,
animatedStyles3,
]}
></Animated.View>
}
>

Temos funções em Home.js que tratam da passagem de dados dos Círculos para o Pic Principal sem causar condições de corrida (que pode consultar no código) e existem algumas limitações que são apresentadas no readme.md juntamente com as instruções sobre como corrigi-los.

Agora, o produto final terá o seguinte aspeto

O vídeo do Vimeo para melhores fps

O que é que gostaria que eu fizesse a seguir? Isto foi demasiado complexo ou fácil? Quer que eu forneça mais contexto de código aqui? Comente as suas ideias