Upgrade to Pro — share decks privately, control downloads, hide ads and more …

海外のアプリで見かけたかっこいいTransitionを真似てみる

 海外のアプリで見かけたかっこいいTransitionを真似てみる

Shibuya.apk#52で発表した内容です!

Shirataki

April 04, 2025
Tweet

More Decks by Shirataki

Other Decks in Programming

Transcript

  1. ԣεϥΠυͰը໘ભҠ NavHost( navController = navController, startDestination = MountainRoute, ) {

    ɹ. . . composable<MountainDetailRoute>( enterTransition = { slideInHorizontally( initialOffsetX = { it }, animationSpec = tween(durationMillis = AnimationDurationMilliSeconds) ) }, exitTransition = { slideOutHorizontally( targetOffsetX = { it }, animationSpec = tween(durationMillis = AnimationDurationMilliSeconds) ) } ) { MountainDetailScreen( onBackPressed = { navController.popBackStack() }, ) } } Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  2. ৄࡉը໘ʹσʔλΛ౉͢ํ๏ΛબͿ w *%Λ౉ͯ͠ৄࡉը໘ͰGFUDI w ڞ༗7JFX.PEFMΛ࢖༻ w EBUBDMBTT͝ͱ౉͢ data class Mountain(

    val id: Int, val name: String, val area: String, val description: String, @DrawableRes val imageRes: Int, @DrawableRes val iconRes: Int, ) Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  3. ৄࡉը໘ʹσʔλΛ౉͢ํ๏ΛબͿ w *%Λ౉ͯ͠ৄࡉը໘ͰGFUDI w ڞ༗7JFX.PEFMΛ࢖༻ w EBUBDMBTT͝ͱ౉͢ w ΧελϜ/BW5ZQFΛ࡞੒ data

    class Mountain( val id: Int, val name: String, val area: String, val description: String, @DrawableRes val imageRes: Int, @DrawableRes val iconRes: Int, ) Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  4. ৄࡉը໘ʹEBUBDMBTT͝ͱ౉͢ w !4FSJBMJ[BCMFͱˏ1BSDFMJ[FΛ͚ͭͨEBUBDMBTTΛఆٛ @Serializable @Parcelize data class Mountain( val id:

    Int, val name: String, val area: String, val description: String, @DrawableRes val imageRes: Int, @DrawableRes val iconRes: Int, ) : Parcelable Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  5. ৄࡉը໘ʹEBUBDMBTT͝ͱ౉͢ w ΧελϜ/BW5ZQFΛ࡞੒ object MountainNavType : NavType<Mountain>( isNullableAllowed = false

    ) { override fun put(bundle: Bundle, key: String, value: Mountain) { bundle.putParcelable(key, value) } override fun get(bundle: Bundle, key: String): Mountain { return requireNotNull(BundleCompat.getParcelable(bundle, key, Mountain::class.java)) } override fun serializeAsValue(value: Mountain): String { return Uri.encode(Json.encodeToString(value)) } override fun parseValue(value: String): Mountain { return Json.decodeFromString(value) } } Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  6. composable<MountainDetailRoute>( typeMap = mapOf( typeOf<Mountain>() to MountainNavType, ), . .

    . ) { backStackEntry -> val mountain = backStackEntry.toRoute<MountainDetailRoute>().mountain MountainDetailScreen( mountain = mountain, onBackPressed = navController::popBackStack, ) } ৄࡉը໘ʹEBUBDMBTT͝ͱ౉͢ w /BW)PTUͰΧελϜ/BW5ZQFΛ࢖༻ Ұཡը໘͔Βৄࡉը໘΁ͷભҠ 1
  7. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ίϯςϯπ͕ڞ௨͢Δͭͷը໘ΛγʔϜϨεʹભҠ  ͭͷը໘ͦΕͧΕʹ4DPQFΛ౉͢ w "OJNBUFE7JTJCJMJUZ4DPQF w 4IBSFE5SBOTJUJPO4DPQF 

    ڞ௨ͷίϯςϯπʹ.PEJpFSTIBSFE&MFNFOU Λઃఆ IUUQTEFWFMPQFSBOESPJEDPNEFWFMPQVJDPNQPTFBOJNBUJPOTIBSFEFMFNFOUT
  8. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ίϯςϯπ͕ڞ௨͢Δͭͷը໘ΛγʔϜϨεʹભҠ  ͭͷը໘ͦΕͧΕʹ4DPQFΛ౉͢ w "OJNBUFE7JTJCJMJUZ4DPQF w 4IBSFE5SBOTJUJPO4DPQF 

    ڞ௨ͷίϯςϯπʹ.PEJpFSTIBSFE&MFNFOU Λઃఆ IUUQTEFWFMPQFSBOESPJEDPNEFWFMPQVJDPNQPTFBOJNBUJPOTIBSFEFMFNFOUT
  9. 4IBSFE&MFNFOU5SBOTJUJPOT 2 "OJNBUFE7JTJCJMJUZ4DPQF w "OJNBUFE$POUFOU w "OJNBUFE7JTJCJMJUZ w /BW)PTU /BWJHBUJPO$PNQPTF

     w /BW(SBQI#VJMEFSDPNQPTBCMF AnimatedContent( . . . ) { targetState -> if (!targetState) { MainContent( . . . animatedVisibilityScope = this@AnimatedContent, ) } else { DetailsContent( . . . animatedVisibilityScope = this@AnimatedContent, ) } }
  10. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ίϯςϯπ͕ڞ௨͢Δͭͷը໘ΛγʔϜϨεʹભҠ  ͭͷը໘ͦΕͧΕʹ4DPQFΛ౉͢ w "OJNBUFE7JTJCJMJUZ4DPQF w 4IBSFE5SBOTJUJPO4DPQF 

    ڞ௨ͷίϯςϯπʹ.PEJpFSTIBSFE&MFNFOU Λઃఆ IUUQTEFWFMPQFSBOESPJEDPNEFWFMPQVJDPNQPTFBOJNBUJPOTIBSFEFMFNFOUT
  11. 4IBSFE&MFNFOU5SBOTJUJPOT 2 4IBSFE5SBOTJUJPO4DPQF w 4IBSFE5SBOTJUJPO-BZPVUͰఏڙ SharedTransitionLayout { AnimatedContent( . .

    . ) { targetState -> if (!targetState) { MainContent( . . . sharedTransitionScope = this@SharedTransitionLayout ) } else { DetailsContent( . . . sharedTransitionScope = this@SharedTransitionLayout ) } } }
  12. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ͭͷը໘ͦΕͧΕʹ4DPQFΛ౉͢ w "OJNBUFE7JTJCJMJUZ4DPQF w 4IBSFE5SBOTJUJPO4DPQF SharedTransitionLayout { AnimatedContent(

    . . . ) { targetState -> if (!targetState) { MainContent( . . . animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } else { DetailsContent( . . . animatedVisibilityScope = this@AnimatedContent, sharedTransitionScope = this@SharedTransitionLayout ) } } }
  13. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ίϯςϯπ͕ڞ௨͢Δͭͷը໘ΛγʔϜϨεʹભҠ  ͭͷը໘ͦΕͧΕʹ4DPQFΛ౉͢ w "OJNBUFE7JTJCJMJUZ4DPQF w 4IBSFE5SBOTJUJPO4DPQF 

    ڞ௨ͷίϯςϯπʹ.PEJpFSTIBSFE&MFNFOU Λઃఆ IUUQTEFWFMPQFSBOESPJEDPNEFWFMPQVJDPNQPTFBOJNBUJPOTIBSFEFMFNFOUT
  14. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ڞ௨ͷίϯςϯπʹ.PEJpFSTIBSFE&MFNFOU Λઃఆ w SFNFNCFS4IBSFE$POUFOU4UBUF ʹಉ͡ΩʔΛઃఆ @Composable private fun

    MainContent( . . . sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { Row( . . . ) { with(sharedTransitionScope) { Image( . . . modifier = Modifier .sharedElement( rememberSharedContentState(key = "image"), animatedVisibilityScope = animatedVisibilityScope ) . . .
  15. 4IBSFE&MFNFOU5SBOTJUJPOT 2 લ४උ w "OJNBUFE7JTJCJMJUZ4DPQF1SPWJEFSͷઃఆ val LocalAnimatedVisibilityScope = staticCompositionLocalOf<AnimatedVisibilityScope> {

    error("No AnimatedVisibilityScope provided") } @Composable fun AnimatedVisibilityScopeProvider( animatedVisibilityScope: AnimatedVisibilityScope, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalAnimatedVisibilityScope provides animatedVisibilityScope, ) { content() } }
  16. 4IBSFE&MFNFOU5SBOTJUJPOT 2 લ४උ w 4IBSFE5SBOTJUJPO4DPQF1SPWJEFSͷઃఆ @OptIn(ExperimentalSharedTransitionApi::class) val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope>

    { error("No SharedTransitionScope provided") } @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun SharedTransitionScopeProvider( content: @Composable () -> Unit, ) { SharedTransitionLayout { CompositionLocalProvider( LocalSharedTransitionScope provides this, ) { content() } } }
  17. 4IBSFE&MFNFOU5SBOTJUJPOT 2 લ४උ w ࢁͷը૾ͷ.PEJpFSʹTIBSFE&MFNFOU Λઃఆ w ຖճಉ͡Α͏ͳίʔυΛॻ͘ͷ͸େม ˠΩʔΛड͚औΔΧελϜ.PEJpFSΛ࡞੒ with(LocalSharedTransitionScope.current)

    { Image( … modifier = Modifier .sharedElement( state = rememberSharedContentState(key = sharedTransitionImageKey), animatedVisibilityScope = LocalAnimatedVisibilityScope.current, ) ) }
  18. 4IBSFE&MFNFOU5SBOTJUJPOT 2 લ४උ w ΩʔΛड͚औΔ.PEJpFSͷ֦ுؔ਺Λ࡞੒ @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun Modifier.easySharedElement(key: Any):

    Modifier { with(LocalSharedTransitionScope.current) { return [email protected]( state = rememberSharedContentState(key = key), animatedVisibilityScope = LocalAnimatedVisibilityScope.current ) } }
  19. 4IBSFE&MFNFOU5SBOTJUJPOT 2 /BW)PTU @Composable fun SharedElementTransitionNavHost() { SharedTransitionScopeProvider { val

    navController = rememberNavController() NavHost( navController = navController, startDestination = MountainRoute, ) { composable<MountainRoute> { AnimatedVisibilityScopeProvider(animatedVisibilityScope = this) { MountainScreen( . . . ) } } composable<MountainDetailRoute>( . . . ) { backStackEntry -> AnimatedVisibilityScopeProvider(animatedVisibilityScope = this) { . . . MountainDetailScreen( . . . }
  20. 4IBSFE&MFNFOU5SBOTJUJPOT 2 ը໘ؒͰڞ௨ͷίϯςϯπ fun ImageWithCategoryIcon( . . . ) {

    Column(modifier = modifier) { Image( painter = painterResource(imageRes), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier .easySharedElement(key = sharedTransitionImageKey) .fillMaxWidth() .height(imageHeight) ) . . .