Zoom e elementos compartilhados
Toque em um card e faça a página crescer a partir dele. Ao voltar, ela encolhe para o mesmo elemento. Esse é o zoom nativo do UIKit, disponível no iOS 18+ quando o runtime expõe UIViewControllerTransition. Android e runtimes sem essa API usam a transição normal ou o fallback explícito.
Card com RouterLink
Salve como Inbox.vue. O único filho de RouterLink é o elemento de origem; não é necessário obter um ref nem criar uma animação em JavaScript.
<script setup lang="ts">
import { RouterLink } from 'vue-router-ns'
</script>
<template>
<Page>
<ActionBar title="Inbox" />
<StackLayout padding="24">
<RouterLink :to="{ name: 'message', params: { id: 1 } }" zoom>
<StackLayout padding="24" borderRadius="16" backgroundColor="#dbeafe">
<Label text="Ana" color="#1e3a8a" fontSize="24" />
<Label text="Lunch on Friday?" color="#1e3a8a" textWrap="true" />
</StackLayout>
</RouterLink>
</StackLayout>
</Page>
</template>Destino e configuração
Salve o destino como Message.vue:
<script setup lang="ts">
const props = defineProps<{ id: string }>()
const subjects: Record<string, string> = {
'1': 'Lunch on Friday?',
'2': 'Release notes',
}
</script>
<template>
<Page>
<ActionBar title="Message" />
<StackLayout padding="24">
<Label
:text="subjects[props.id] ?? 'Message'"
:sharedTransitionTag="`message-${props.id}`"
fontSize="28"
textWrap="true"
/>
<Label
text="This is a native Page. Use Back to return to the inbox."
textWrap="true"
/>
</StackLayout>
</Page>
</template>Registre as duas páginas no router e instale-o no app, como no guia de instalação:
import { createRouter } from 'vue-router-ns'
export const router = createRouter({
routes: [
{ path: '/', component: () => import('./Inbox.vue') },
{
path: '/messages/:id',
name: 'message',
component: () => import('./Message.vue'),
props: true,
},
],
})No componente raiz, use <RouterView />. Teste primeiro em uma única pilha: o primeiro detalhe de um split expandido aparece em outro painel, sem zoom a partir da lista.
Linha de lista com zoomFrom
Substitua Inbox.vue por esta versão para usar a view entregue pelo itemTap. O destino acima também tem a tag necessária para a transição compartilhada usada como fallback.
<script setup lang="ts">
import { PageTransition, SharedTransition, type ItemEventData } from '@nativescript/core'
import { useRouter } from 'vue-router-ns'
const router = useRouter()
const messages = [
{ id: 1, subject: 'Lunch on Friday?' },
{ id: 2, subject: 'Release notes' },
]
function open({ index, view }: ItemEventData) {
const message = messages[index]
if (!message) return
router.push(
{ name: 'message', params: { id: message.id } },
{
zoomFrom: view,
transition: SharedTransition.custom(new PageTransition()),
},
)
}
</script>
<template>
<Page>
<ActionBar title="Inbox" />
<ListView :items="messages" @itemTap="open">
<template #default="{ item }">
<Label
:text="item.subject"
:sharedTransitionTag="`message-${item.id}`"
padding="24"
/>
</template>
</ListView>
</Page>
</template>O que observar
- Toque em uma linha: a mensagem deve abrir na mesma pilha.
- Com o zoom disponível, a página cresce a partir da linha. Voltar deve retornar à origem.
- Com movimento reduzido ativado, o router pede uma transição sem animação.
- No Android, as labels com a mesma
sharedTransitionTagparticipam da transição compartilhada. - Uma animação lateral comum não comprova o zoom. Verifique a API disponível no runtime e evite
Frame.defaultTransitionnesse frame.
O Inbox do demo usa zoomFrom com fallback. Para reproduzir o vídeo, abra Settings → Zoom from a card no demo. Essa página usa uma pilha simples; veja seu código.
Referência das transições
O transition aceita o SharedTransition do NativeScript, que transforma as views que têm o mesmo sharedTransitionTag de uma página na outra, nas duas plataformas:
<!-- na lista -->
<Image :src="photo.thumb" :sharedTransitionTag="`photo-${photo.id}`" />
<!-- na página da foto -->
<Image :src="photo.full" :sharedTransitionTag="`photo-${photo.id}`" />import { PageTransition, SharedTransition } from '@nativescript/core'
router.push(`/photos/${photo.id}`, {
transition: SharedTransition.custom(new PageTransition()),
})Voltar toca a animação ao contrário. O dismiss interativo dele (interactive: { dismiss }, só no iOS) é um pop nativo, como o swipe de voltar: o router acompanha, mas um leave guard não consegue impedir.
No iOS 18 ou mais novo, o zoomFrom entrega a animação ao zoom do UIKit: a página cresce a partir de uma view, e encolhe de volta para ela quando fecha, pelo botão de voltar, por um swipe da borda ou arrastando para baixo. Enquanto um leave guard está ativo, o arrastar fica segurado, como o swipe de voltar.
function open({ index, view }: ItemEventData) {
router.push(`/photos/${photos[index].id}`, { zoomFrom: view })
}O zoomFrom aceita uma view, o elemento de um template ref ou um componente, e o <RouterLink zoom> faz o zoom a partir do próprio filho. Se a view já não existir quando a página fechar, ela fecha sem zoom. Onde o zoom não se aplica (Android, iOS antes do 18, animated: false), a página usa a transição de sempre, então passar os dois dá a cada plataforma a sua:
router.push(to, {
zoomFrom: view, // iOS 18
transition: SharedTransition.custom(new PageTransition()), // em todo o resto
})Uma transição padrão definida no frame (Frame.defaultTransition) toma a animação do UIKit de volta, então deixe-a sem definir onde você usar o zoom.