Animação e gestos¶
Esta página cobre as duas famílias de widgets que dão vida e tato à sua UI no
tempest-core: os widgets de animação (Animated, AnimatedList, Hero,
Shimmer, Skeleton) e os widgets de gesto (GestureDetector,
PanHandler, ScaleHandler, DoubleTapHandler, Draggable, DragTarget,
Dismissible, ReorderableList, InteractiveViewer).
Todos seguem a mesma filosofia do core: eles descrevem a intenção de forma declarativa (uma duração, uma curva, um handler tipado) e a interpolação ou a detecção do ponteiro roda no engine, não nos renderizadores. Os leaf renderers (Qt / Compose) só realizam o resultado — o core nunca toca em pixels. 🚀
O que você aprende aqui
- Como
Animatedmonta um filho já interpolado por frame por umAnimationController+Tween. - Como
AnimatedListanima entrada/saída com duração + curva por direção. - Como o
Herocasa uma geometria entre telas numa transição de elemento compartilhado. - Como
ShimmereSkeletonviram placeholder de carregamento com um sweep de gradiente. - Quais gestos reais cada handler reporta — tap, pan/fling, pinch/rotação, drag-and-drop, swipe-to-dismiss, reordenar e pan+zoom — e o evento tipado que cada um entrega.
Animação¶
Os widgets de animação carregam só a metadata que o renderizador precisa para
realizar o movimento. A interpolação em si mora no core: um
AnimationController avança um valor normalizado (0.0..1.0) no relógio de frames
do app e um Tween interpola entre dois extremos com esse valor. O renderizador
leaf sempre vê o props final do frame atual.
Animated¶
Um wrapper de um filho só, cujo style o view já interpolou a cada frame. Você
dá um controller (que avança o valor) e, opcionalmente, os estilos de começo e
fim; o renderizador apenas monta o filho já no alvo do frame.
from tempest_core import Animated, Container, Text
from tempest_core import AnimationController
from tempest_core import Curve, Style
# O controller avança de 0.0 a 1.0 em 0.3s com uma ease simétrica.
controller = AnimationController(duration_s=0.3, curve=Curve.EASE_IN_OUT)
card = Animated(
child=Container(child=Text(content="Olá")),
controller=controller,
style_begin=Style(opacity=0.0), # (1)!
style_end=Style(opacity=1.0),
)
- Quando
style_beginéNone, o própriostyledo filho vira o ponto de partida. Só ostyle_endjá basta para um fade-in.
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget |
(obrigatório) | O widget embrulhado, montado com o estilo interpolado do frame. |
controller |
Any |
None |
O AnimationController que dirige a interpolação (tipado Any para evitar ciclo de import com o módulo de animação do core). |
style_begin |
Any |
None |
O Style em value == 0.0, ou None para usar o próprio estilo do filho como início. |
style_end |
Any |
None |
O Style em value == 1.0, ou None. |
A interpolação mora no core, não no renderizador
O view lê o value do controller, interpola o Tween e dobra o resultado
no Style do filho — então o Qt só monta o filho normalmente. Os campos
controller / style_begin / style_end ficam no nó para introspecção e
paridade entre dispositivos, mas não são consumidos pelo mount path do
renderizador Qt (uma divergência Qt-vs-Compose documentada).
Escolha a curva pela sensação, não pelo nome
O AnimationController aceita qualquer Curve — LINEAR (velocidade
constante), EASE_IN (acelera), EASE_OUT (desacelera), EASE_IN_OUT
(ease simétrica), EASE (o default do CSS), BOUNCE (quica ao final) e
ELASTIC (oscila como mola). Cada leaf mapeia para sua curva nativa
(QEasingCurve no Qt, Easing no Compose); o próprio core aproxima cada uma
para o relógio de teste/simulador conseguir interpolar sem renderizador.
AnimatedList¶
Um container flex que anima os filhos conforme eles entram e saem. Ele
posiciona os filhos como um Column/Row, mas numa mudança estrutural (um patch
Insert/Remove) o filho afetado é animado em vez de aparecer/sumir
instantaneamente.
from tempest_core import AnimatedList, Container, Text
from tempest_core import Curve, FlexDirection
lista = AnimatedList(
direction=FlexDirection.COLUMN,
children=[
Container(child=Text(content="Item 1")),
Container(child=Text(content="Item 2")),
],
enter_duration_ms=300,
exit_duration_ms=300,
enter_curve=Curve.EASE_OUT, # entra desacelerando
exit_curve=Curve.EASE_IN, # sai acelerando
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
direction |
FlexDirection |
COLUMN |
A direção do eixo principal (coluna ou linha). |
children |
list[Widget] |
[] |
Os filhos ordenados. |
enter_duration_ms |
int |
300 |
Duração da animação de entrada, em milissegundos. |
exit_duration_ms |
int |
300 |
Duração da animação de saída, em milissegundos. |
enter_curve |
Curve |
EASE_OUT |
A curva de easing aplicada à entrada. |
exit_curve |
Curve |
EASE_IN |
A curva de easing aplicada à saída. |
Entrada e saída têm curva e duração próprias
Os defaults — EASE_OUT para entrar, EASE_IN para sair — seguem a
convenção de Material Motion: elementos que chegam desaceleram até o
repouso, e elementos que partem aceleram para fora da tela. O Qt realiza
isso com um QPropertyAnimation na opacidade e altura máxima do filho; o
renderizador de dispositivo embrulha cada filho num AnimatedVisibility (uma
divergência documentada).
Hero¶
Marca uma subárvore como elemento compartilhado para uma transição de tela.
Quando duas telas de um Navigator têm um Hero com o mesmo hero_tag, o
renderizador interpola a geometria da subárvore marcada ao longo da transição de
rota.
from tempest_core import Hero, Image
# Na tela de lista: a miniatura.
miniatura = Hero(hero_tag="foto-42", child=Image(src="foto-42-thumb.jpg"))
# Na tela de detalhe: a mesma tag, imagem maior — a geometria interpola entre as duas.
detalhe = Hero(hero_tag="foto-42", child=Image(src="foto-42-full.jpg"))
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
hero_tag |
str |
(obrigatório) | A identidade do elemento compartilhado (precisa casar entre as telas). |
child |
Widget |
(obrigatório) | O widget embrulhado que participa da transição. |
A hero_tag precisa ser única dentro de cada tela
A transição casa um Hero de origem com um de destino pela tag. Duas
subárvores com a mesma tag na mesma tela deixam o pareamento ambíguo. O Qt
anima a geometria com um QPropertyAnimation; o Compose usa
SharedTransitionLayout + Modifier.sharedElement (uma divergência
documentada).
Shimmer¶
Um placeholder de carregamento que varre um destaque de gradiente sobre um
filho. Embrulha um filho (normalmente um layout de esqueleto) e anima uma faixa
diagonal indo de base_color até highlight_color e voltando, em loop — o
clássico "conteúdo carregando".
from tempest_core import Shimmer, Column, Skeleton
from tempest_core import Color
carregando = Shimmer(
child=Column(
children=[
Skeleton(height=16.0),
Skeleton(height=16.0, width=180.0),
]
),
base_color=Color(r=224, g=224, b=224), # tom de repouso
highlight_color=Color(r=245, g=245, b=245), # tom do destaque em movimento
duration_ms=1200,
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget |
(obrigatório) | O widget que o shimmer pinta por cima. |
base_color |
Color |
Color(224, 224, 224) |
O tom de repouso do gradiente. |
highlight_color |
Color |
Color(245, 245, 245) |
O tom do destaque em movimento. |
duration_ms |
int |
1200 |
A duração de uma varredura completa, em milissegundos. |
Shimmer é o wrapper; Skeleton é a folha
Use Shimmer para pintar o sweep sobre um layout inteiro de placeholders, e
Skeleton para cada retângulo individual (linha de texto, avatar) dentro
dele. O Qt dirige o gradiente com um loop de repaint via QTimer; o
renderizador de dispositivo usa InfiniteTransition +
Brush.linearGradient (uma divergência documentada).
Skeleton¶
A variante sem filho do Shimmer: um único retângulo arredondado que varre o
gradiente, usado para ocupar o lugar de uma linha de texto ou avatar enquanto o
conteúdo real carrega.
from tempest_core import Skeleton
# Uma linha de texto de largura fixa.
linha = Skeleton(width=200.0, height=16.0, radius=4.0)
# Um avatar quadrado, tingido pela família de cor "primary" no tema ativo.
avatar = Skeleton(width=48.0, height=48.0, radius=24.0, color_scheme="primary")
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
width |
float \| None |
None |
A largura fixa em pixels lógicos, ou None para flexar. |
height |
float \| None |
None |
A altura fixa em pixels lógicos, ou None para flexar. |
radius |
float |
4.0 |
O raio de canto em pixels lógicos. |
base_color |
Color |
Color(224, 224, 224) |
O tom de repouso do gradiente. |
highlight_color |
Color |
Color(245, 245, 245) |
O tom do destaque em movimento. |
duration_ms |
int |
1200 |
A duração de uma varredura completa, em milissegundos. |
color_scheme |
str |
"neutral" |
A família de papéis M3 com que o renderizador pode tingir os tons do shimmer. |
color_scheme é resolvido pelo renderizador contra o tema ativo
O engine só carrega o prop color_scheme; o renderizador o resolve contra
o tema ativo (H4). O padrão "neutral" dá o clássico shimmer cinza. Passe
"primary" / "secondary" / etc. para um placeholder tingido que combine com
a superfície onde ele mora.
Gestos¶
GestureDetector é o primitivo base de gesto do framework: um container de um
filho só que renderiza o filho intacto mas observa o ponteiro sobre ele,
transformando sequências de press/drag/release em eventos tipados. Os widgets
de gesto avançados especializam esse contrato para interações mais ricas.
Envolva conteúdo não interativo
Gestos funcionam melhor em volta de conteúdo que não consome o ponteiro
(um card, uma imagem, uma linha de texto). Um filho que já trata o ponteiro
sozinho (por exemplo um Button) mantém o próprio tratamento — um limite v1
documentado. Ambos os leaf renderers realizam o mesmo contrato: Qt via
filtros de evento de ponteiro / QGraphicsView / QDrag; Compose via
Modifier.pointerInput / SwipeToDismissBox / graphicsLayer.
GestureDetector¶
O detector base: reporta tap, double-tap, long-press e swipe direcional sobre seu
filho. Cada handler é opcional e pode ser síncrono ou async.
from tempest_core import GestureDetector, Container, Text
card = GestureDetector(
child=Container(child=Text(content="Toque, segure ou deslize")),
on_tap=lambda e: print("tap"),
on_double_tap=lambda e: print("double tap"),
on_long_press=lambda e: print("long press"),
on_swipe=lambda e: print("swipe", e.direction), # (1)!
)
- O
SwipeEventcarrega a direção cardinal dominante e o deslocamento total (veja a Referência da API).
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget sobre o qual os gestos são detectados. |
on_tap |
TapHandler \| None |
None |
Handler de um toque único (recebe TapEvent). |
on_double_tap |
TapHandler \| None |
None |
Handler de um toque duplo (recebe TapEvent). |
on_long_press |
LongPressHandler \| None |
None |
Handler de um press mantido além do limiar (recebe LongPressEvent). |
on_swipe |
SwipeHandler \| None |
None |
Handler de um swipe direcional (recebe SwipeEvent). |
PanHandler¶
Reporta um pan contínuo: conforme o ponteiro arrasta sobre o filho, o
renderizador entrega deltas por frame e, na soltura, a velocidade do fling, num
PanEvent.
from tempest_core import PanHandler, Container, Text
arrastavel = PanHandler(
child=Container(child=Text(content="Arraste-me")),
on_pan=lambda e: app.set_state(x=e.dx, y=e.dy), # (1)!
)
- O handler é chamado por frame durante o arrasto; use os deltas para mover o conteúdo e a velocidade final para um fling.
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget sobre o qual o pan é detectado. |
on_pan |
EventHandler \| None |
None |
Handler do gesto de pan (recebe PanEvent). |
PanHandler e ScaleHandler são widgets, não os aliases de handler
Os símbolos públicos PanHandler e ScaleHandler re-exportados de
tempest_core.widgets são os widgets desta página. Eles sombreiam de
propósito os TypeAliases de handler de mesmo nome, que ficam privados ao
módulo base. Por isso a coluna Tipo do on_pan acima diz
EventHandler | None: é um callable (sync ou async) que recebe o
PanEvent, não o widget.
ScaleHandler¶
Reporta pinch (escala + rotação) e um double-tap. O on_scale recebe um
ScaleEvent com a escala acumulada, o ponto focal e a rotação; o double-tap é o
par comum para resetar o zoom.
from tempest_core import ScaleHandler, Image
foto = ScaleHandler(
child=Image(src="mapa.png"),
on_scale=lambda e: app.set_state(scale=e.scale),
on_double_tap=lambda e: app.set_state(scale=1.0), # reseta o zoom
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget sobre o qual os gestos são detectados. |
on_scale |
EventHandler \| None |
None |
Handler do pinch (recebe ScaleEvent com escala acumulada, ponto focal e rotação). |
on_double_tap |
TapHandler \| None |
None |
Handler de um toque duplo (recebe TapEvent; par comum para resetar o zoom). |
DoubleTapHandler¶
O caso mais enxuto: só um toque duplo. Útil para "curtir com dois toques" ou um atalho de zoom sem o custo do pinch.
from tempest_core import DoubleTapHandler, Image
curtivel = DoubleTapHandler(
child=Image(src="post.jpg"),
on_double_tap=lambda e: app.set_state(curtido=True),
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget sobre o qual o toque duplo é detectado. |
on_double_tap |
TapHandler \| None |
None |
Handler de um toque duplo (recebe TapEvent). |
Draggable¶
Um filho que pode ser pego e arrastado até um DragTarget. O drag_data é
um rótulo opaco carregado até o alvo de soltura via DragEvent.data, para o alvo
identificar o que caiu nele.
from tempest_core import Draggable, Container, Text
carta = Draggable(
child=Container(child=Text(content="Rei de Espadas")),
drag_data="rei-espadas", # identifica o item no alvo
on_drag=lambda e: print("soltei em", e.data), # (1)!
)
- O
on_dragdispara quando o arrasto termina, com os dados carregados e a posição de soltura.
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget que o usuário arrasta. |
drag_data |
str |
"" |
Um rótulo opaco levado ao alvo via DragEvent.data, para identificar o que caiu nele. |
on_drag |
DragHandler \| None |
None |
Handler disparado quando o arrasto termina (recebe DragEvent com os dados e a posição de soltura). |
DragTarget¶
Um filho que aceita um Draggable solto sobre ele. O on_drop dispara
quando um draggable é liberado sobre o alvo, recebendo o DragEvent com os dados
do item.
from tempest_core import DragTarget, Container, Text
pilha = DragTarget(
child=Container(child=Text(content="Solte cartas aqui")),
on_drop=lambda e: app.jogar_carta(e.data), # (1)!
)
Draggable+DragTargetformam o par de drag-and-drop: odrag_datado primeiro chega noDragEvent.datado segundo.
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget que age como região de soltura. |
on_drop |
DragHandler \| None |
None |
Handler disparado quando um draggable é solto sobre este alvo (recebe DragEvent com os dados do item). |
Dismissible¶
Um filho que pode ser deslizado para fora para dispensá-lo (swipe-to-delete).
O direction define a direção de swipe que dispara a dispensa; o on_dismiss
dispara quando o swipe passa do limiar.
from tempest_core import Dismissible, Container, Text, SwipeDirection
item = Dismissible(
child=Container(child=Text(content="Deslize para apagar")),
direction=SwipeDirection.LEFT, # dispensa ao deslizar para a esquerda
on_dismiss=lambda e: app.remover_item(),
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget sobre o qual o gesto de dispensa é detectado. |
direction |
SwipeDirection |
LEFT |
A direção de swipe que dispara a dispensa (LEFT / RIGHT / UP / DOWN). |
on_dismiss |
DismissHandler \| None |
None |
Handler disparado quando o swipe passa do limiar (recebe DismissEvent; reusa o tipo de evento de dispensa de overlay). |
O on_dismiss reusa o DismissEvent dos overlays
Não há um evento novo para o swipe-to-dismiss: ele reusa o DismissEvent
também usado quando um overlay é fechado. Uma superfície de evento a menos
para o core carregar.
ReorderableList¶
Uma lista vertical cujos itens podem ser arrastados para uma nova ordem. O
handler tipicamente muta a lista de apoio
(items.insert(to_index, items.pop(from_index))) e re-renderiza; uma lista de
filhos com key estável então diffa para um patch Reorder.
from tempest_core import ReorderableList, Container, Text
def reordenar(e):
itens.insert(e.to_index, itens.pop(e.from_index))
app.rebuild()
lista = ReorderableList(
children=[
Container(key="a", child=Text(content="Primeiro")),
Container(key="b", child=Text(content="Segundo")),
Container(key="c", child=Text(content="Terceiro")),
],
on_reorder=reordenar, # (1)!
)
- O
ReorderEventcarrega o índice de origem e de destino.
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
children |
list[Widget] |
[] |
Os itens ordenados da lista. Prefira keys estáveis para o diff emitir um Reorder. |
on_reorder |
ReorderHandler \| None |
None |
Handler disparado quando um item é arrastado para um novo slot (recebe ReorderEvent com origem e destino). |
Use keys estáveis para diffar como Reorder
Com keys estáveis, o diff por chave reconhece que os itens só trocaram de
lugar e emite um patch Reorder (o mecanismo A2), em vez de atualizações
posicionais — nenhum tipo de patch novo é necessário.
InteractiveViewer¶
Um container de um filho só que o usuário pode arrastar e dar zoom (pinch +
drag), limitado entre min_scale e max_scale. O on_interaction dispara
conforme a view transforma, com a escala atual, o ponto focal e a rotação.
from tempest_core import InteractiveViewer, Image
visualizador = InteractiveViewer(
child=Image(src="planta-baixa.png"),
min_scale=0.5,
max_scale=4.0,
on_interaction=lambda e: app.set_state(scale=e.scale),
)
Props¶
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
child |
Widget \| None |
None |
O widget que é arrastado e ampliado. |
min_scale |
float |
0.5 |
O fator de zoom mínimo permitido. |
max_scale |
float |
4.0 |
O fator de zoom máximo permitido. |
on_interaction |
EventHandler \| None |
None |
Handler disparado enquanto a view transforma (recebe ScaleEvent com escala, ponto focal e rotação). |
InteractiveViewer vs. ScaleHandler
Os dois entregam um ScaleEvent, mas com papéis diferentes: o
ScaleHandler só reporta o pinch para você aplicar o efeito, enquanto o
InteractiveViewer já transforma o filho (pan + zoom) dentro dos limites
min_scale/max_scale e te avisa da transformação. Escolha o viewer quando
quiser pan+zoom pronto; o handler quando quiser controlar o efeito na mão.
Recapitulando¶
- Animação declara a intenção, o core interpola.
Animatedmonta um filho já no alvo do frame (dirigido por umAnimationController+Tween);AnimatedListanima entrada/saída com duração +Curvepróprias por direção. - Curvas vêm do enum
Curve:LINEAR/EASE_IN/EASE_OUT/EASE_IN_OUT/EASE/BOUNCE/ELASTIC, cada uma mapeada para a curva nativa do leaf. Herocasa uma geometria entre telas porhero_tagnuma transição de elemento compartilhado — a tag precisa ser única por tela.Shimmerpinta um sweep de gradiente sobre um layout;Skeletoné a folha sem filho, comcolor_schemeresolvido pelo renderizador contra o tema.GestureDetectoré o primitivo base (tap / double-tap / long-press / swipe); os avançados especializam:PanHandler(pan + fling),ScaleHandler(pinch + rotação + double-tap),DoubleTapHandler(só double-tap).Draggable+DragTargetformam o drag-and-drop viadrag_data→DragEvent.data;Dismissiblefaz swipe-to-dismiss;ReorderableListarrasta para reordenar (comkeys estáveis);InteractiveViewerdá pan + zoom prontos, limitado pormin_scale/max_scale.PanHandlereScaleHandlersão widgets re-exportados detempest_core.widgets— eles sombreiam de propósito os aliases de handler homônimos, que ficam privados aobase.