Slide 1

Slide 1 text

Jetpack Compose A to Z ✨૑ࢿ࠼

Slide 2

Slide 2 text

👋૑ࢿ࠼ 🏠TVOHCJO 🏰TVOHCJOMBOE 🐙HJUIVCDPNKJTVOHCJO

Slide 3

Slide 3 text

-PPLBIFBE-BZPVU "OJNBUJPO *OUFSOBM4ZTUFN $PNQJMFS%FCVHHJOH #FTU1SBDUJDF 8SBQVQ 📖ݾର

Slide 4

Slide 4 text

↟ BMQIBӝળ ↟ ௏٘ࣁࠗࢎ೦ࢤۚ 📝द੘ೞӝ੹ী

Slide 5

Slide 5 text

1. LookaheadLayout

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

start fi nish animation frame

Slide 9

Slide 9 text

$PMVNO3PX🙅 #PY#PY8JUI$POTUSBJOUT🙅 -B[Z-JTU🙅 $POTUSBJOU-BZPVU🙅 -BZPVU🙅 -PPLBIFBE-BZPVU🥳 BOJNBUJPOGSBNFਸ୶੸ೞӝਤೠۨ੉ইਓ

Slide 10

Slide 10 text

start fi nish 1st intermediate layout 2nd intermediate layout 3rd intermediate layout Lookahead 1. Modi fi er.onPlaced 2. Modi fi er.intermediateLayout

Slide 11

Slide 11 text

/** * ۨ੉ইਓਸ Ѿ੿ೞӝ ਤ೧ measure ܳ ޷ܻ ೞҊ ୶റ placement ױ҅ܳ प೯ೞח ۨ੉ইਓੑפ׮. => measure ܳ ޷ܻ ೞח ױ҅: lookahead ױ҅ * lookahead ױ҅о ՘աݶ ୶റ [LookaheadLayoutScope.intermediateLayout] ਸ ా೧ * lookahead Ѿҗܳ ӝ߈ਵ۽ ۨ੉ইਓ੄ measure ߂ placement ܳ ઑ੿ೡ ࣻ ੓ח ژ ׮ܲ measure ߂ placement ױ҅о द੘ؾפ׮. => intermediate layout * * ੉ܳ ੉ਊೞৈ ۨ੉ইਓ੉ ޷ܻ measure ػ intermediate layout ਸ ೱ೧ ௼ӝ৬ ਤ஖ܳ ੼ର੸ਵ۽ ߸҃ೡ ࣻ ੓णפ׮. */ @ExperimentalComposeUiApi @UiComposable @Composable fun LookaheadLayout( content: @Composable @UiComposable LookaheadLayoutScope.() -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy )

Slide 12

Slide 12 text

/** * [LookaheadLayout] ੄ ݽٚ(૒੽ ߂ р੽) ೞਤ ۨ੉ইਓী ؀ೠ receiver ߧਤܳ ઁҕ೤פ׮. * lookahead ױ҅ীࢲ ҅࢑ػ ۨ੉ইਓ੄ measurement ߂ placement ח * [LookaheadLayoutScope] ীࢲ пп [Modifier.intermediateLayout] ߂ [Modifier.onPlaced] ܳ ా೧ observe ೡ ࣻ ੓णפ׮. */ @ExperimentalComposeUiApi interface LookaheadLayoutScope { fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize ) -> MeasureResult ): Modifier }

Slide 13

Slide 13 text

@ExperimentalComposeUiApi interface LookaheadLayoutScope { /** * intermediate layout ੉ ߓ஖ؼ ਤ஖о ҅࢑ػ റ ഐ୹ؾפ׮. * * [LookaheadLayoutCoordinates] о ઱য૑ݶ ҅࢑ػ intermediate layout ੄ য়೐ࣇҗ അ੤ ߓ஖ظ ੓ח ஹನ੷࠶੄ য়೐ࣇਸ * [LookaheadLayoutCoordinates.localLookaheadPositionOf] ৬ [LookaheadLayoutCoordinates.localPositionOf] ܳ ࢎਊೞৈ ঳ਸ ࣻ ੓णפ׮. * ੉ܳ ా೧ ҅࢑ػ intermediate layout ੄ য়೐ࣇਸ ӝ߈ਵ۽ ஹನ੷࠶੄ ߓ஖ܳ ઑ੿ೡ ࣻ ੓णפ׮. * * [onPlaced ۈ׮ ੋ੗] * * @param lookaheadScopeCoordinates [LookaheadLayout] ੉ ࢎਊೞח [LookaheadLayoutCoordinates] * @param layoutCoordinates ੉ modifier ੄ ஹನ੷࠶੉ ࢎਊೞח [LookaheadLayoutCoordinates] */ fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize

Slide 14

Slide 14 text

start fi nish Lookahead 1st intermediate layout 2nd intermediate layout will be 3rd intermediate layout OEJOUFSNFEJBUFMBZPVUীࢲप೯غח࢚ട JOUFSNFEJBUFMBZPVU੉ߓ஖ؼਤ஖ܳ҅࢑ೣ ੉റ೧׼ਤ஖ܳModifier.onPlaced۽ࠁն

Slide 15

Slide 15 text

@ExperimentalComposeUiApi interface LookaheadLayoutScope { /** * intermediate layout ੉ ߓ஖ؼ ਤ஖о ҅࢑ػ റ ഐ୹ؾפ׮. * * [LookaheadLayoutCoordinates] о ઱য૑ݶ ҅࢑ػ intermediate layout ੄ য়೐ࣇҗ അ੤ ߓ஖ظ ੓ח ஹನ੷࠶੄ য়೐ࣇਸ * [LookaheadLayoutCoordinates.localLookaheadPositionOf] ৬ [LookaheadLayoutCoordinates.localPositionOf] ܳ ࢎਊೞৈ ঳ਸ ࣻ ੓णפ׮. * ੉ܳ ా೧ ҅࢑ػ intermediate layout ੄ য়೐ࣇਸ ӝ߈ਵ۽ ஹನ੷࠶੄ ߓ஖ܳ ઑ੿ೡ ࣻ ੓णפ׮. * * [onPlaced ۈ׮ ੋ੗] * * @param lookaheadScopeCoordinates [LookaheadLayout] ੉ ࢎਊೞח [LookaheadLayoutCoordinates] * @param layoutCoordinates ੉ modifier ੄ ஹನ੷࠶੉ ࢎਊೞח [LookaheadLayoutCoordinates] */ fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize

Slide 16

Slide 16 text

/** * ۨ੉ইਓী ؀೧ measure ػ bounds ੄ ഓ؊ੑפ׮. */ @JvmDefaultWithCompatibility interface LayoutCoordinates { // … ࢤۚ /** * [sourceCoordinates] ҕр੄ [relativeToSource] ܳ ۽ஸ coordinate ۽ ߸ജ೤פ׮. * [sourceCoordinates] ח زੌೠ ۨ੉ইਓ ҅கী ࣘೞח ݽٚ [LayoutCoordinates] ੌ ࣻ ੓णפ׮. * * @param sourceCoordinates ߸ജೡ [Offset] ੉ ੓ח [LayoutCoordinates] * @param relativeToSource ߸ഝೡ [Offset] * * @return ۽ஸ coordinate ۽ ߸ജػ [Offset] */ fun localPositionOf(sourceCoordinates: LayoutCoordinates, relativeToSource: Offset): Offset }

Slide 17

Slide 17 text

/** * lookhead ױ҅ ૓೯ ੹җ റ੄ ۨ੉ইਓ ݽف੄ [LayoutCoordinates] ܳ ࠁਬ೤פ׮. */ @ExperimentalComposeUiApi sealed interface LookaheadLayoutCoordinates : LayoutCoordinates { /** * [sourceCoordinates] ҕр੄ [relativeToSource] ܳ ۽ஸ coordinate ۽ ߸ജ೤פ׮. * [sourceCoordinates] ח زੌೠ ۨ੉ইਓ ҅கী ࣘೞח ݽٚ [LookaheadLayoutCoordinates] ੌ ࣻ ੓णפ׮. * * [localPositionOf] ৬ ׳ܻ [localLookaheadPositionOf] ח coordinate ҅࢑ਸ ਤ೧ lookahead ਤ஖ܳ ࢎਊ೤פ׮. * * @param sourceCoordinates ߸ജೡ [Offset] ੉ ੓ח [LookaheadLayoutCoordinates] * @param relativeToSource ߸ഝೡ [Offset] * * @return ۽ஸ coordinate ۽ ߸ജػ [Offset] */ fun localLookaheadPositionOf( sourceCoordinates: LookaheadLayoutCoordinates, relativeToSource: Offset = Offset.Zero ): Offset }

Slide 18

Slide 18 text

// interface LookaheadLayoutScope /** * lookahead ױ҅ীࢲ ҅࢑ػ ੿ࠁܳ ӝ߈ਵ۽ intermediate layout ܳ ߓ஖೤פ׮. * intermediate layout ੄ ௼ӝо ઁҕغח ۈ׮ੋ [measure] ੋ੗ܳ ా೧ intermediate layout ܳ morph ೡ ࣻ ੓णפ׮. * * morph: അ੤ ݽনਸ ׮ܲ ݽনਵ۽ ߄Բח Ѫ * * [measure ۈ׮ ੋ੗] * * @param measurable intermediate layout ੄ measurable * @param constraints intermediate layout ੄ constraints * @param lookaheadSize intermediate layout ੄ ௼ӝ */ fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize ) -> MeasureResult ): Modifier }

Slide 19

Slide 19 text

start fi nish Lookahead 1st intermediate layout 2nd intermediate layout will be 3rd intermediate layout OEJOUFSNFEJBUFMBZPVUীࢲप೯غח࢚ട Modifier.intermediateLayoutਵ۽ߓ஖ೡҔ LookaheadLayoutCoordinates.localLookaheadPositionOf੄য়೐ࣇ LookaheadLayoutCoordinates.localPositionOf੄য়೐ࣇ

Slide 20

Slide 20 text

start fi nish Lookahead ௼ӝয়೐ࣇ

Slide 21

Slide 21 text

௼ӝઑ੿.PEJGJFSJNUFSNFEJBUF-BZPVU /** * lookahead ױ҅ীࢲ ҅࢑ػ ੿ࠁܳ ӝ߈ਵ۽ intermediate layout ܳ ߓ஖೤פ׮. * intermediate layout ੄ ௼ӝо ઁҕغח ۈ׮ੋ [measure] ੋ੗ܳ ా೧ intermediate layout ܳ morph ೡ ࣻ ੓णפ׮. */ fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize ) -> MeasureResult ): Modifier

Slide 22

Slide 22 text

য়೐ࣇઑ੿.PEJGJFSPO1MBDFE /** * [LookaheadLayoutCoordinates] о ઱য૑ݶ ҅࢑ػ intermediate layout ੄ য়೐ࣇҗ അ੤ ߓ஖ظ ੓ח ஹನ੷࠶੄ য়೐ࣇਸ * [LookaheadLayoutCoordinates.localLookaheadPositionOf] ৬ [LookaheadLayoutCoordinates.localPositionOf] ܳ ࢎਊೞৈ ঳ਸ ࣻ ੓णפ׮. * ੉ܳ ా೧ ҅࢑ػ intermediate layout ੄ য়೐ࣇਸ ӝ߈ਵ۽ ஹನ੷࠶੄ ߓ஖ܳ ઑ੿ೡ ࣻ ੓णפ׮. */ fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier

Slide 23

Slide 23 text

fun Modifier.movement(lookaheadScope: LookaheadLayoutScope) = composed { var targetOffset: IntOffset? by remember { mutableStateOf(null) } // ߓ஖ೡ য়೐ࣇ var placementOffset by remember { mutableStateOf(IntOffset.Zero) } // അ੤ য়೐ࣇ with(lookaheadScope) { this@composed .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> // LookaheadLayout ੄ ۽ஸ coordinates ীࢲ ੉ modifier ੄ lookahead ਤ஖ܳ ߈ജ targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(sourceCoordinates = layoutCoordinates) .round() // о੢ оө਍ IntOffset чਵ۽ য়೐ࣇ ߈ৢܿ // LookaheadLayout ੄ ۽ஸ coordinates ীࢲ ੉ modifier ੄ അ੤ ਤ஖ܳ ߈ജ placementOffset = lookaheadScopeCoordinates .localPositionOf( sourceCoordinates = layoutCoordinates, relativeToSource = Offset.Zero ) .round() } .intermediateLayout { measurable, constraints, _ -> val placeable = measurable.measure(constraints) layout(width = placeable.width, height = placeable.height) { // ੉زೠ য়೐ࣇী ߓ஖

Slide 24

Slide 24 text

targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(sourceCoordinates = layoutCoordinates) .round() // о੢ оө਍ IntOffset чਵ۽ য়೐ࣇ ߈ৢܿ // LookaheadLayout ੄ ۽ஸ coordinate ীࢲ ੉ modifier ੄ അ੤ ਤ஖ܳ ߈ജ placementOffset = lookaheadScopeCoordinates .localPositionOf( sourceCoordinates = layoutCoordinates, relativeToSource = Offset.Zero ) .round() } .intermediateLayout { measurable, constraints, _ -> val placeable = measurable.measure(constraints) layout(width = placeable.width, height = placeable.height) { // ੉زೠ য়೐ࣇী ߓ஖ val (x, y) = targetOffset!! - placementOffset placeable.place(x = x, y = y) } } } }

Slide 25

Slide 25 text

fun Modifier.transformation(lookaheadScope: LookaheadLayoutScope) = with(lookaheadScope) { intermediateLayout { measurable, _, lookaheadSize -> val (width, height) = lookaheadSize // lookahead ௼ӝ۽ width, height Ѿ੿ val animatedConstraints = Constraints.fixed( width = width.coerceAtLeast(0), // ୭ࣗ 0 ਵ۽ ࢸ੿ height = height.coerceAtLeast(0) ) val placeable = measurable.measure(animatedConstraints) layout(width = placeable.width, height = placeable.height) { // lookahead ௼ӝী ݏѱ ߓ஖ placeable.place(x = 0, y = 0) } } }

Slide 26

Slide 26 text

LookaheadLayout( modifier = modifier .fillMaxSize() .navigationBarsPadding() .padding(16.dp), content = { Fab( modifier = Modifier .size( size = FabDefaults.size( isExpanded = isExpanded, maxWidthDp = screenMaxWidth.dp ) ) .movement(lookaheadScope = this) .transformation(lookaheadScope = this) .noRippleClickable { isExpanded = !isExpanded }, isExpanded = isExpanded ) }, measurePolicy = DefaultMeasurePolicy )

Slide 27

Slide 27 text

start fi nish Lookahead ௼ӝয়೐ࣇগפݫ੉࣌9

Slide 28

Slide 28 text

2. Animation

Slide 29

Slide 29 text

#FGPSF😣"GUFS🪄

Slide 30

Slide 30 text

ఫझ౟࢚࢝গפݫ੉࣌ ߓ҃ਤ஖੉زগפݫ੉࣌ GBEFJOPVU TMJEFJOPVUGBEFJOPVU "GUFS🪄

Slide 31

Slide 31 text

ఫझ౟࢚࢝গפݫ੉࣌➡️BOJNBUF$PMPS"T4UBUF ߣبزੌ ߓ҃ਤ஖੉زগפݫ੉࣌➡️ߓ࢚҃࢝গפݫ੉࣌ਵ۽੐द؀୓ GBEFJOPVU➡️"OJNBUFE$POUFOU TMJEFJOPVUGBEFJOPVU➡️"OJNBUFE$POUFOU "GUFS🪄

Slide 32

Slide 32 text

Column( modifier = Modifier.fillMaxSize().background(color = Color.BackgroundWhite), verticalArrangement = Arrangement.SpaceBetween ) { TabContainer { TabDefaults.Items.forEach { tab -> TabItem( title = tab.shortname, backgroundColor = tabBackgroundColor(selectedTab = selectedTabState, nowTab = tab), textColor = tabTextColor(selectedTab = selectedTabState, nowTab = tab), onTabClick = { selectedTabState = tab } ) } } MovieContainer { MovieName(fullname = selectedTabState.fullname) MoviePoster(posterDrawable = selectedTabState.poster) } }

Slide 33

Slide 33 text

@Stable private fun tabBackgroundColor( selectedTab: Int, nowTab: Int ): Color = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedBackground false -> TabDefaults.Color.defaultBackground } @Stable private fun tabTextColor( selectedTab: Int, nowTab: Int ): Color = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedText false -> TabDefaults.Color.defaultText } @Composable private fun tabBackgroundColorWithAnimation( selectedTab: Tab, nowTab: Tab ): Color = animateColorAsState( targetValue = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedBackground false -> TabDefaults.Color.defaultBackground } ).value @Composable private fun tabTextColorWithAnimation( selectedTab: Tab, nowTab: Tab ): Color = animateColorAsState( targetValue = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedText false -> TabDefaults.Color.defaultText } ).value

Slide 34

Slide 34 text

/** * [Color] ী ؀ೠ Fire-and-Forget গפݫ੉࣌ ӝמਸ ઁҕ೤פ׮. * animate*AsState ח [Float], [Color], [Dp], [Size], [Offset], [Rect], [Int], [IntOffset] ӒܻҊ [IntSize] ী ࢎ੹ ੿੄ظ ੓णפ׮. * ઁҕػ [targetValue] о ߸҃غݶ গפݫ੉࣌੉ ੗زਵ۽ प೯ؾפ׮. * [targetValue] о ߸҃ؼ ٸ ૓೯ ઺ੋ গפݫ੉࣌੉ ੉޷ ੓ח ҃਋ ૓೯ ઺ੋ গפݫ੉࣌਷ ࢜۽਍ [targetValue] ܳ ೱ೧ গפݫ੉࣌غب۾ ௏झܳ ઑ੿೤פ׮. * * animate*AsState ח গפݫ੉࣌੉ ੸ਊغҊ ੓ח [State] ܳ ߈ജ೤פ׮. * * @param targetValue গפݫ੉࣌੄ ఋѶ * @param animationSpec दрী ٮۄ чਸ ߸҃ೞח ؘ ࢎਊೡ গפݫ੉࣌ * @param finishedListener গפݫ੉࣌੉ ৮ܐؼ ٸ ঌܿਸ ߉ח ࢶఖ੸ ܻझց */ @Composable fun animateColorAsState( targetValue: Color, animationSpec: AnimationSpec = colorDefaultSpring, finishedListener: ((Color) -> Unit)? = null ): State

Slide 35

Slide 35 text

গפݫ੉࣌ࢎনਸݏ୺ࢸ੿ೞӝਤೠੋఠಕ੉झ TQSJOH UXFFO LFZGSBNFT SFQFBUBCMFJOGJOJUF3FQFBUBCMF TOBQ "OJNBUJPO4QFD

Slide 36

Slide 36 text

/** * द੘ чҗ ՘ ч ࢎ੉ী ޛܻ೟ ӝ߈ গפݫ੉࣌ਸ ੸ਊ೤פ׮. * * @param DampingRatio х࣭࠺ (఍ࢿ) * @param stiffness ъࢿ (ઙܐ чਵ۽ ੉زೞח ࣘب) * @param visibleThreshold оदࢿ ੐҅ч * গפݫ੉࣌੉ ؀࢚ী ߈ৢܿೞӝী ୽࠙൤ दп੸ਵ۽ оө਍ Ѫਵ۽ р઱غযঠ ೞח दӝܳ ੿੄೤פ׮. * * @return ઱য૓ ২࣌ਸ ࢎਊೞח [SpringSpec] */ @Stable fun spring( dampingRatio: Float = Spring.DampingRatioNoBouncy, stiffness: Float = Spring.StiffnessMedium, visibilityThreshold: T? = null ): SpringSpec = SpringSpec( dampingRatio = dampingRatio, stiffness = stiffness, visibilityThreshold = visibilityThreshold )

Slide 37

Slide 37 text

/** * ੉૚ ҋࢶਸ ࢎਊೞৈ ૑੿ػ [durationMillis] زউ द੘ чҗ ՘ ч рী গפݫ੉࣌ਸ ੸ਊ೤פ׮. * * @param durationMillis গפݫ੉࣌ ૑ࣘ दр (޻ܻୡ) * @param delayMillis গפݫ੉࣌੉ द੘غӝ ੹ী ؀ӝೞח दр (޻ܻୡ) * @param easing द੘җ ՘ ࢎ੉ܳ ࠁрೞח ؘ ࢎਊغח ੉૚ ҋࢶ * * @return ઱য૓ ২࣌ਸ ࢎਊೞח [TweenSpec] */ @Stable fun tween( durationMillis: Int = AnimationConstants.DefaultDurationMillis, delayMillis: Int = 0, easing: Easing = FastOutSlowInEasing ): TweenSpec = TweenSpec( durationMillis = durationMillis, delay = delayMillis, easing = easing )

Slide 38

Slide 38 text

/** * গפݫ੉࣌੄ ࠙ࣻ(fraction)ܳ ઑ੿ೞח ߑߨੑפ׮. * ੉૚ਸ ࢎਊೞݶ গפݫ੉࣌੉ ੌ੿ೠ ࣘب۽ ૓೯غח ؀न ࣘبܳ ֫੉Ѣա ծ୹ ࣻ ੓णפ׮. * * ࠙ࣻח গפݫ੉࣌੄ അ੤ ૑੼ਸ աఋղח 0 ীࢲ 1.0 ࢎ੉੄ чੑפ׮. ৈӝࢲ 0 ਷ द੘ਸ աఋղҊ 1.0 ਷ ՘ਸ աఋշפ׮. */ @Stable fun interface Easing { fun transform(fraction: Float): Float }

Slide 39

Slide 39 text

// ࡅܰѱ ࣘبܳ ֫੉Ҋ ੼ର੸ਵ۽ ו۰૘פ׮. val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f) // ٜযয়ח ਃࣗח ઁੌ ࡅܲ ࣘبীࢲ ੼੼ ו۰૘פ׮. val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f) // աоח ਃࣗח ઁੌ וܽ ࣘبীࢲ ੼੼ ࡈۄ૘פ׮. val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f) // ࣻ੿غ૑ ঋ਷ ࠙ࣻܳ ߈ജ೤פ׮. ੉૚੉ ೙ਃೞ૑݅ ࣻ੿ػ ੉૚੉ ೙ਃೞ૑ ঋ਷ ҃਋ ӝࠄчਵ۽ ਬਊ೤פ׮. val LinearEasing: Easing = Easing { fraction -> fraction } // 3ର ߬૑য ҋࢶਸ ҳഅ೤פ׮. @Immutable class CubicBezierEasing( private val a: Float, private val b: Float, private val c: Float, private val d: Float ) : Easing

Slide 40

Slide 40 text

IUUQTFBTJOHTOFU

Slide 41

Slide 41 text

/** * গפݫ੉࣌ ӝрী ৈ۞ ఋ੐झఙ೐ীࢲ ૑੿ػ झշࢫ чਸ ӝ߈ਵ۽ গפݫ੉࣌ਸ ୊ܻ೤פ׮. * ঱ઁա গפݫ੉࣌ ч਷ ف ః೐ۨ੐ ч ࢎ੉ী ࠁрؾפ׮. * ః೐ۨ੐݃׮ ࢎਊೡ [Easing] ਸ ૑੿ೡ ࣻ ੓णפ׮. */ @Stable fun keyframes( init: KeyframesSpec.KeyframesSpecConfig.() -> Unit ): KeyframesSpec = KeyframesSpec( config = KeyframesSpec.KeyframesSpecConfig().apply(init) ) @Composable fun KeyframesExample(target: Int) { val value by animateIntAsState( targetValue = target, // 0 ~ 100 animationSpec = keyframes { durationMillis = 1000 // 1000 ms زউ গפݫ੉࣌ ૓೯ 100 at 50 with LinearEasing // LinearEasing ਸ ࢎਊೞݴ 100 ms উী 50 ө૑ গפݫ੉࣌ 500 at 80 with FastOutSlowInEasing // FastOutSlowInEasing ਸ ࢎਊೞݴ 500 ms উী 80 ө૑ গפݫ੉࣌ // ੉റ 501 ~ 1000 ms زউ ӝઓী ࢸ੿ೠ ੉૚ੋ FastOutSlowInEasing ܳ ҅ࣘ ࢎਊೞৈ 100 ө૑ গפݫ੉࣌ } ) }

Slide 42

Slide 42 text

/** * ૑੿ػ ߈ࠂ പࣻী ب׳ೡ ٸө૑ ӝр ӝ߈ গפݫ੉࣌(৘: [tween] ژח [keyframes])ਸ ߈ࠂ੸ਵ۽ प೯೤פ׮. * * @param iterations ߈ࠂೡ പࣻ * @param animation ߈ࠂೡ গפݫ੉࣌ * @param repeatMode ߈ࠂೡ ݽ٘ - [RepeatMode.Restart] ژח [RepeatMode.Reverse] * @param initialStartOffset গפݫ੉࣌ਸ द੘ೡ য়೐ࣇ */ @Stable fun repeatable( iterations: Int, animation: DurationBasedAnimationSpec, repeatMode: RepeatMode = RepeatMode.Restart, initialStartOffset: StartOffset = StartOffset(0) ): RepeatableSpec = RepeatableSpec( iterations = iterations, animation = animation, repeatMode = repeatMode, initialStartOffset = initialStartOffset )

Slide 43

Slide 43 text

/** * [repeatable] җ زੌೞҊ, ਬੌೠ ର੉੼਷ ޖೠ ߈ࠂ੉ۄ iterations ੋ੗о হणפ׮. */ @Stable fun infiniteRepeatable( animation: DurationBasedAnimationSpec, repeatMode: RepeatMode = RepeatMode.Restart, initialStartOffset: StartOffset = StartOffset(0) ): InfiniteRepeatableSpec = InfiniteRepeatableSpec( animation = animation, repeatMode = repeatMode, initialStartOffset = initialStartOffset )

Slide 44

Slide 44 text

/** * snap ਷ чਸ ૊द ઙܐ чਵ۽ ߸ജೞח ౠࣻ ݾ੸੄ [AnimationSpec] ੑפ׮. * * @param delayMillis গפݫ੉࣌ द੘ਸ ૑োೡ दр (޻ܻୡ) */ @Stable fun snap(delayMillis: Int = 0): SnapSpec = SnapSpec( delay = delayMillis )

Slide 45

Slide 45 text

@Composable fun animateColorAsState( targetValue: Color, animationSpec: AnimationSpec = colorDefaultSpring, finishedListener: ((Color) -> Unit)? = null ): State

Slide 46

Slide 46 text

MovieContainer { MovieName(fullname = selectedTabState.fullname) MoviePoster(posterDrawable = selectedTabState.poster) } MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState ) { tab -> MovieName(fullname = tab.fullname) } AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState ) { tab -> MoviePoster(posterDrawable = tab.poster) } }

Slide 47

Slide 47 text

/** * [targetState] о ߸҃ؼ ٸ [content] ী ੗زਵ۽ গפݫ੉࣌ਸ ੸ਊೞח ஶప੉ցੑפ׮. * * @param targetState ߸҃ؼ ࢚క * @param modifier ੸ਊೡ [Modifier] * @param transitionSpec ੸ਊೡ গפݫ੉࣌ * @param contentAlignment [content] о ߓ஖ؼ [Alignment] * @param content ߓ஖ೡ ஹನ੷࠶ */ @ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { /* … */ }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 48

Slide 48 text

@ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { fadeIn( animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) + scaleIn( initialScale = 0.92f, animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) with fadeOut( animationSpec = tween(durationMillis = 90) ) }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 49

Slide 49 text

/** * ஹನ੷࠶੉ [AnimatedContent] ী ٜযоҊ աоח ߑߨਸ ੿੄೤פ׮. * * @param targetContentEnter ࢜۽਍ ஹನ੷࠶੉ ٜযয়ח গפݫ੉࣌ * @param initialContentExit ӝઓ ஹನ੷࠶੉ աоח গפݫ੉࣌ * @param targetContentZIndex ஹನ੷࠶੉ ٜযয়Ҋ աт ٸ ࢎਊೡ zIndex * @param sizeTransform ஹನ੷࠶੉ ٜযয়Ҋ աт ٸ ࢎ੉ૉо ߸ೡ ҃਋ ੉ܳ ҙܻೞӝ ਤೠ ২࣌ */ @ExperimentalAnimationApi class ContentTransform( val targetContentEnter: EnterTransition, val initialContentExit: ExitTransition, targetContentZIndex: Float = 0f, sizeTransform: SizeTransform? = SizeTransform() ) { var targetContentZIndex by mutableStateOf(targetContentZIndex) var sizeTransform: SizeTransform? = sizeTransform internal set }

Slide 50

Slide 50 text

@ExperimentalAnimationApi class ContentTransform( val targetContentEnter: EnterTransition, val initialContentExit: ExitTransition, targetContentZIndex: Float = 0f, sizeTransform: SizeTransform? = SizeTransform() ) { /** * ஶప੉ցী ٜযт ٸ ࢜۽਍ ؀࢚ ஹನ੷࠶੄ zIndex ܳ ੄޷೤פ׮. * ӝࠄч਷ 0f ੑפ׮. zIndex о ֫਷ ஹನ੷࠶਷ zIndex о ծ਷ ஹನ੷࠶ ਤী Ӓ۰૘פ׮. * ੋؙझо э਷ ஹನ੷࠶਷ ఋѶ ஹನ੷࠶੉ ݔ ਤী ߓ஖ؾפ׮. */ var targetContentZIndex by mutableStateOf(targetContentZIndex) /** * ࢜ ஹನ੷࠶੉ AnimatedContent ী ٜযоҊ ੉੹ ஹನ੷࠶੉ աт ٸ ௼ӝо ߸҃غח ҃਋ ஶప੉ց੄ ഛ੢ ߂ ୷ࣗܳ ҙܻ೤פ׮. * ӝࠄ੸ਵ۽ [spring][SpringSpec] ਷ ݽٚ ௼ӝ ߸҃ਸ গפݫ੉࣌ೞח ؘ ࢎਊغݴ [AnimatedContent] ח গפݫ੉࣌ػ ௼ӝ۽ ੜ݀פ׮. * ࢎਊؼ [SizeTransform] ܳ ૒੽ ࢸ੿ೡ ࣻ ੓णפ׮. ௼ӝ গפݫ੉࣌੉ ೙ਃೞ૑ ঋਵݶ [sizeTransform] ਸ null ۽ ࢸ੿ೞࣁਃ. */ var sizeTransform: SizeTransform? = sizeTransform internal set }

Slide 51

Slide 51 text

/** * ஹನ੷࠶੉ ಴दؼ ٸ ૓೯غח গפݫ੉࣌ਸ ੿੄೤פ׮. * ࢎਊೡ ࣻ ੓ח [EnterTransition] ੄ 4о૑ ஠పҊܻח ׮਺җ эणפ׮. * * 1. fade: [fadeIn] * 2. scale: [scaleIn] * 3. slide: [slideIn], [slideInHorizontally], [slideInVertically] * 4. expand: [expandIn], [expandHorizontally], [expandVertically] */ @Immutable sealed class EnterTransition { internal abstract val data: TransitionData @Stable operator fun plus(enter: EnterTransition): EnterTransition { // … } }

Slide 52

Slide 52 text

@Immutable sealed class EnterTransition { internal abstract val data: TransitionData // ղࠗীࢲ ࢎਊೞח ੿ࠁܳ ࠁҙೣ /** * ׮ܲ [EnterTransition] ਸ Ѿ೤೤פ׮. [EnterTransition] ח زदী द੘غ޲۽ Ѿ೤غח ࣽࢲח ઺ਃೞ૑ ঋणפ׮. * গפݫ੉࣌ਸ ੸ਊೞח ࣽࢲח ঌ౵ ߂ ߓਯਸ ݢ੷ ઑ੿ೞҊ ୷ࣗ ژח ഛ؀ೠ ׮਺ ठۄ੉٘೤פ׮. * * @param enter Ѿ೤ೡ ׮ܲ [EnterTransition] * * @return 2ѐ੄ [EnterTransition] ੉ Ѿ೤ػ ࢜۽਍ [EnterTransition] */ @Stable operator fun plus(enter: EnterTransition): EnterTransition { return EnterTransitionImpl( TransitionData( fade = data.fade ?: enter.data.fade, slide = data.slide ?: enter.data.slide, changeSize = data.changeSize ?: enter.data.changeSize, scale = data.scale ?: enter.data.scale ) ) } }

Slide 53

Slide 53 text

/** * ஹನ੷࠶੉ ࢎۄ૕ ٸ ૓೯غח গפݫ੉࣌ਸ ੿੄೤פ׮. * ࢎਊೡ ࣻ ੓ח [ExitTransition] ੄ 4о૑ ஠పҊܻח ׮਺җ эणפ׮. * * 1. fade: [fadeOut] * 2. scale: [scaleOut] * 3. slide: [slideOut], [slideOutHorizontally], [slideOutVertically] * 4. shrink: [shrinkOut], [shrinkHorizontally], [shrinkVertically] */ @Immutable sealed class ExitTransition { internal abstract val data: TransitionData @Stable operator fun plus(exit: ExitTransition): ExitTransition { // … } }

Slide 54

Slide 54 text

@Immutable sealed class ExitTransition { internal abstract val data: TransitionData // ղࠗীࢲ ࢎਊೞח ੿ࠁܳ ࠁҙೣ /** * ׮ܲ [ExitTransition] ਸ Ѿ೤೤פ׮. [ExitTransition] ח زदী द੘غ޲۽ Ѿ೤غח ࣽࢲח ઺ਃೞ૑ ঋणפ׮. * গפݫ੉࣌ਸ ੸ਊೞח ࣽࢲח ঌ౵ ߂ ߓਯਸ ݢ੷ ઑ੿ೞҊ ୷ࣗ ژח ഛ؀ೠ ׮਺ ठۄ੉٘೤פ׮. * * @param exit Ѿ೤ೡ ׮ܲ [ExitTransition] * * @return 2ѐ੄ [ExitTransition] ੉ Ѿ೤ػ ࢜۽਍ [ExitTransition] */ @Stable operator fun plus(exit: ExitTransition): ExitTransition { return ExitTransitionImpl( TransitionData( fade = data.fade ?: exit.data.fade, slide = data.slide ?: exit.data.slide, changeSize = data.changeSize ?: exit.data.changeSize, scale = data.scale ?: exit.data.scale ) ) } }

Slide 55

Slide 55 text

@ExperimentalAnimationApi class ContentTransform( val targetContentEnter: EnterTransition, val initialContentExit: ExitTransition, targetContentZIndex: Float = 0f, sizeTransform: SizeTransform? = SizeTransform() ) { var targetContentZIndex by mutableStateOf(targetContentZIndex) var sizeTransform: SizeTransform? = sizeTransform internal set }

Slide 56

Slide 56 text

/** * ஹನ੷࠶੄ ௼ӝо ߸҃ؼ ٸ ೠ ௼ӝীࢲ ׮ܲ ௼ӝ۽ ߸ജೞח ߑߨਸ ੿੄೤פ׮. */ @ExperimentalAnimationApi interface SizeTransform { /** * ࢎ੉ૉ ઑ੺ গפݫ੉࣌ীࢲ ஹನ੷࠶੄ ҃҅ী ݏѱ clip ೧ঠ ೞח૑ ৈࠗੑפ׮. */ val clip: Boolean /** * গפݫ੉࣌ ੸ਊ ੹ ࢎ੉ૉੋ [initialSize] ৬ গפݫ੉࣌ ੸ਊ റ ࢎ੉ૉੋ [targetSize] ܳ ӝ߈ਵ۽ [AnimationSpec] ਸ ٜ݅ ࣻ ੓णפ׮. */ fun createAnimationSpec(initialSize: IntSize, targetSize: IntSize): FiniteAnimationSpec }

Slide 57

Slide 57 text

@ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { fadeIn( animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) + scaleIn( initialScale = 0.92f, animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) with fadeOut( animationSpec = tween(durationMillis = 90) ) }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 58

Slide 58 text

/** * ઁҕػ [this][EnterTransition] ߂ [exit] ܳ ࢎਊೞৈ [ContentTransform] ਸ ࢤࢿೞݴ, * [EnterTransition] ߂ [ExitTransition] গפݫ੉࣌੉ زदী प೯ؾפ׮. * * @param exit [this][EnterTransition] ৬ ೤ச [ExitTransition] * * @return [this][EnterTransition] ৬ [ExitTransition] ੉ ೤୛૓ [ContentTransform] */ @ExperimentalAnimationApi infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform( targetContentEnter = this, initialContentExit = exit )

Slide 59

Slide 59 text

@ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { /* … */ }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 60

Slide 60 text

/** * [AnimatedContent] ীࢲ݅ ಞܻೞѱ ੸ਊೡ ࣻ ੓ח ӝמਸ ઁҕ೤פ׮. */ @ExperimentalAnimationApi class AnimatedContentScope internal constructor( internal val transition: Transition, internal var contentAlignment: Alignment, internal var layoutDirection: LayoutDirection ) : Transition.Segment { override val initialState: S get() = transition.segment.initialState // গפݫ੉࣌੉ द੘غӝ ੹ ୡӝ ч override val targetState: S get() = transition.segment.targetState // গפݫ੉࣌੉ ੸ਊؼ ч, ૊ গפݫ੉࣌੄ ઙܐ ч /** * അ੤ [this][ContentTransform] ੄ [sizeTransform] ܳ ੋ੗۽ ߉਷ [sizeTransform] ۽ ࢸ੿೤פ׮. * * @param sizeTransform ࢜۽ ੸ਊೡ [SizeTransform] * * @return [sizeTransform] ਸ ࢜۽ ੸ਊೠ [ContentTransform] */ @ExperimentalAnimationApi infix fun ContentTransform.using(sizeTransform: SizeTransform?): ContentTransform = apply { this.sizeTransform = sizeTransform } // …

Slide 61

Slide 61 text

/** * ஶప੉ց੄ о੢੗ܻীࢲ [AnimatedContent] ী ౠ੿ೠ ࣻಣ/ࣻ૒ slide-in ਸ ੿੄೤פ׮. * [slideInHorizontally] ߂ [slideInVertically] ৬ ׳ܻ द੘ য়೐ࣇ੉ [AnimatedContent] ੄ * അ੤ ௼ӝ৬ ੿۳ ২࣌ਸ ӝ߈ਵ۽ ੗ز ҅࢑ؾפ׮. * * @param towards ठۄ੉٘ ߑೱ * ஹನ੷࠶਷ח [SlideDirection.Left], [SlideDirection.Right], [SlideDirection.Up] ߂ [SlideDirection.Down] * ߑೱਵ۽ ஶప੉ցܳ ೱ೧ slide ೡ ࣻ ੓णפ׮. * @param animationSpec ࢎਊೡ গפݫ੉࣌ * @param initialOffset द੘ য়೐ࣇ. ੗زਵ۽ ҅࢑غ૑݅ ৉द ࣻزਵ۽ ૑੿ೡ ࣻب ੓णפ׮. */ fun slideIntoContainer( towards: AnimatedContentScope.SlideDirection, animationSpec: FiniteAnimationSpec = spring(visibilityThreshold = IntOffset.VisibilityThreshold), initialOffset: (offsetForFullSlide: Int) -> Int = { it } ): EnterTransition // slideIntoContainer ৬ زੌ, ױ slide-in ؀न slide-out fun slideOutOfContainer( towards: AnimatedContentScope.SlideDirection, animationSpec: FiniteAnimationSpec = spring(visibilityThreshold = IntOffset.VisibilityThreshold), targetOffset: (offsetForFullSlide: Int) -> Int = { it } ): ExitTransition }

Slide 62

Slide 62 text

@ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { /* … */ }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 63

Slide 63 text

@Composable private fun tabBackgroundColorWithAnimation( selectedTab: Tab, nowTab: Tab ): Color = animateColorAsState( targetValue = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedBackground false -> TabDefaults.Color.defaultBackground } ).value @Composable private fun tabTextColorWithAnimation( selectedTab: Tab, nowTab: Tab ): Color = animateColorAsState( targetValue = when (selectedTab == nowTab) { true -> TabDefaults.Color.selectedText false -> TabDefaults.Color.defaultText } ).value MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState ) { tab -> MovieName(fullname = tab.fullname) } AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState, contentAlignment = Alignment.Center ) { tab -> MoviePoster(posterDrawable = tab.poster) } }

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

@ExperimentalAnimationApi @Composable fun AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { fadeIn( animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) + scaleIn( initialScale = 0.92f, animationSpec = tween( durationMillis = 220, delayMillis = 90 ) ) with fadeOut( animationSpec = tween(durationMillis = 90) ) }, contentAlignment: Alignment = Alignment.TopStart, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 66

Slide 66 text

TMJEFJOPVUGBEFJOPVU DMJQ5P1BEEJOHGBMTF [*OEFY ೙ਃೠࣁࠗগפݫ੉࣌🖌️

Slide 67

Slide 67 text

MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState ) { tab -> MovieName(fullname = tab.fullname) } AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState, contentAlignment = Alignment.Center ) { tab -> MoviePoster(posterDrawable = tab.poster) } } MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState, transitionSpec = { fadeIn() with fadeOut() using SizeTransform(clip = false) } ) { tab -> MovieName(fullname = tab.fullname) } // … }

Slide 68

Slide 68 text

MovieContainer { // … AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState, contentAlignment = Alignment.Center, transitionSpec = { val targetIndex = targetState.index // targetState == Tab val initialIndex = initialState.index // initialState == Tab if (targetIndex > initialIndex) { // ׮਺ చ slideIntoContainer( towards = AnimatedContentScope.SlideDirection.Start ) with fadeOut() using SizeTransform(clip = false) } else { // ੉੹ చ fadeIn() with slideOutOfContainer( towards = AnimatedContentScope.SlideDirection.End ) using SizeTransform(clip = false) }.apply { targetContentZIndex = targetIndex.toFloat() } } ) { tab -> MoviePoster(posterDrawable = tab.poster) } } MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState ) { tab -> MovieName(fullname = tab.fullname) } AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState, contentAlignment = Alignment.Center ) { tab -> MoviePoster(posterDrawable = tab.poster) } }

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

🤔

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

/** * ࢚క ࣻળীࢲ ݽٚ গפݫ੉࣌ਸ ҙܻ೤פ׮. * গפݫ੉࣌਷ [Transition.animateFloat], [Transition.animateColor], [Transition.animateValue] ١ਸ * ࢎਊೞৈ ࢶ঱੸ੋ ߑधਵ۽ ٜ݅ ࣻ ੓णפ׮. * * @param initialState গפݫ੉࣌ ୡӝ ч * @param label Android Studio Animation Inspector ীࢲ [Transition] ਸ ҳ߹ೞח ؘ ࢎਊೡ కӒ */ @Stable class Transition @PublishedApi internal constructor( initialState: S, label: String? )

Slide 75

Slide 75 text

/** * [Transition] ਸ ࢤࢿೞҊ [targetState] чਵ۽ ୡӝ ࢚కܳ ૑੿೤פ׮. * [targetState] о ߸҃غݶ [Transition] ਷ ࢜ [targetState] чਵ۽ ٜ݅য૓ ݽٚ গפݫ੉࣌ਸ ઑ੿೤פ׮. * * @param targetState ߸҃ਸ х૑ೡ ؀࢚ * @param label Android Studio Animation Inspector ীࢲ [Transition] ਸ ҳ߹ೞח ؘ ࢎਊೡ కӒ * * @return [targetState] чਵ۽ ୡӝ ࢚కо ૑੿ػ [Transition] */ @Composable fun updateTransition( targetState: T, label: String? = null ): Transition

Slide 76

Slide 76 text

/** * ૑੿ػ [Transition] ী ࢝ӭ গפݫ੉࣌ਸ ୶о೤פ׮. * ੉ח ੉ গפݫ੉࣌੄ ࢤݺ ઱ӝо [Transition] ী ੄೧ ҙܻؽਸ ੄޷೤פ׮. * * @param transitionSpec ੸ਊೡ গפݫ੉࣌ * @param label Android Studio Animation Inspector ীࢲ [Transition] ਸ ҳ߹ೞח ؘ ࢎਊೡ కӒ * @param targetValueByState ૑੿ػ [Transition] ੄ গפݫ੉࣌੉ द੘عਸ ٸ ઁҕೡ ч * * @return গפݫ੉࣌੉ ੸ਊػ [Color] ੄ [State] */ @Composable inline fun Transition.animateColor( noinline transitionSpec: @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() }, label: String = "ColorAnimation", targetValueByState: @Composable (state: S) -> Color ): State

Slide 77

Slide 77 text

/** * ࢚ਤ [Transition] ੄ ࢚క৬ ೞਤ ࢚క р੄ ݒೝਸ ӝ߈ਵ۽ ೞਤ [Transition] ਸ ٟ݅פ׮. * ੉ח ׮਺җ э਷ ਊب۽ ࢎਊؾפ׮: * * 1. ೞਤ [Transition] ࢚కܳ ࢚ਤ [Transition] ਵ۽ ഐ੉झ౴ * ࢚ਤ [Transition] ਷ زੌೠ ؀࢚੄ ࢚క ߸҃ਵ۽ ੋ೧ ૓೯ ઺ੋ গפݫ੉࣌੉ ੓ח૑ ৈࠗܳ ੋध೤פ׮. * ੉ۧѱ ೞݶ ࢎ੹ী ૓೯઺੉؍ গפݫ੉࣌੉ ৮ܐغ঻ਸ ٸ ࣽର੸ਵ۽ ׮ܲ গפݫ੉࣌ਸ ࢸ੿ೡ ࣻ ੓णפ׮. * * 2. ҙबࢎ ܻ࠙ * ࢚ਤ [Transition] ীࢲ ߉਷ ࢚కীࢲ ࢎਊೞח ੿ࠁ݅ ೙ఠ݂ೞৈ ೞਤ۽ ੹׳ೞҊ रਸ ٸ ਬਊೞѱ ॳੌ ࣻ ੓णפ׮. * * @param label Android Studio Animation Inspector ীࢲ [Transition] ਸ ҳ߹ೞח ؘ ࢎਊೡ కӒ * @param transformToChildState ࢚ਤ [Transition] ীࢲ чਸ ߉Ҋ ࢜۽ ݅ٚ ࢚క ч * * @return ࢜۽਍ ࢚కܳ о૑ח ೞਤ [Transition] */ @ExperimentalTransitionApi @Composable inline fun Transition.createChildTransition( label: String = "ChildTransition", transformToChildState: @Composable (parentState: S) -> T ): Transition

Slide 78

Slide 78 text

var selectedTabState by remember { mutableStateOf(TabDefaults.Items.first()) } val selectedTabTypeTransition = updateTransition( targetState = selectedTabState.type, label = "selected tab" ) // … TabContainer { TabDefaults.Items.forEach { tab -> val backgroundColor by selectedTabTypeTransition.animateColor( transitionSpec = { defaultTween() }, label = "background color" ) { movie -> when (movie == tab.type) { true -> TabDefaults.Color.selectedBackground false -> TabDefaults.Color.defaultBackground } } val textColor by selectedTabTypeTransition.animateColor( transitionSpec = { defaultTween() }, label = "text color" ) { movie -> when (movie == tab.type) { true -> TabDefaults.Color.selectedText

Slide 79

Slide 79 text

) { movie -> when (movie == tab.type) { true -> TabDefaults.Color.selectedBackground false -> TabDefaults.Color.defaultBackground } } val textColor by selectedTabTypeTransition.animateColor( transitionSpec = { defaultTween() }, label = "text color" ) { movie -> when (movie == tab.type) { true -> TabDefaults.Color.selectedText false -> TabDefaults.Color.defaultText } } TabItem( title = tab.shortname, backgroundColor = backgroundColor, textColor = textColor, onTabClick = { selectedTabState = tab } ) } }

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

MovieContainer { AnimatedContent( modifier = Modifier .align(Alignment.CenterHorizontally) .padding(horizontal = 30.dp), targetState = selectedTabState, // … ӝઓҗ زੌ ) { tab -> MovieName(fullname = tab.fullname) } AnimatedContent( modifier = Modifier.wrapContentSize(), targetState = selectedTabState, // … ӝઓҗ زੌ ) { tab -> MoviePoster(posterDrawable = tab.poster) } }

Slide 82

Slide 82 text

/** * [this][Transition] ੄ targetState о ߸҃ؼ ٸ ੗زਵ۽ [content] ী গפݫ੉࣌ਸ ੸ਊೞח ஶప੉ցੑפ׮. * * @param modifier ੸ਊೡ [Modifier] * @param transitionSpec ੸ਊೡ গפݫ੉࣌ * @param contentAlignment [content] о ߓ஖ؼ [Alignment] * @param contentKey [Transition] ੄ targetState ী ؀ೠ ః. زੌೠ ఃܳ ҕਬೞח ߸҃ੌ ҃਋ গפݫ੉࣌੉ ૓೯غ૑ ঋणפ׮. * @param content ߓ஖ೡ ஹನ੷࠶ */ @ExperimentalAnimationApi @Composable fun Transition.AnimatedContent( modifier: Modifier = Modifier, transitionSpec: AnimatedContentScope.() -> ContentTransform = { // ӝઓ AnimatedContent ৬ زੌ }, contentAlignment: Alignment = Alignment.TopStart, contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedVisibilityScope.(targetState: S) -> Unit )

Slide 83

Slide 83 text

MovieContainer { selectedTabTypeTransition .createChildTransition(label = "selected tab fullname") { movie -> movie.fullname } .AnimatedContent(/* ӝઓҗ زੌ */) { tabFullname -> MovieName(fullname = tabFullname) } selectedTabTypeTransition .AnimatedContent(/* ӝઓҗ زੌ */) { movie -> MoviePoster(posterDrawable = movie.poster) } }

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

ࢶఖߓ҃য়೐ࣇ੉ز ࢶఖߓ҃ݽন ۄ਍٬ ೙ਃೠࣁࠗগפݫ੉࣌🖌️

Slide 87

Slide 87 text

@Composable private fun MovieTab( selectedTabTypeTransition: Transition, updateSelectedTab: (tab: Tab) -> Unit ) { BoxWithConstraints( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .clip( RoundedCornerShape( bottomStartPercent = DefaultCornerUnit, bottomEndPercent = DefaultCornerUnit ) ) ) { val backgroundBoxWidth = remember { maxWidth / TabDefaults.Items.count() } val backgroundOffsetTransition by selectedTabTypeTransition.animateIntOffset( transitionSpec = { defaultTween() }, label = "background offset" ) { movie -> IntOffset( x = with(LocalDensity.current) { (eachItemWidth * movie.index).toPx() }.toInt(),

Slide 88

Slide 88 text

) ) { val eachItemWidth = remember { maxWidth / TabDefaults.Items.count() } val backgroundOffsetTransition by selectedTabTypeTransition.animateIntOffset( transitionSpec = { defaultTween() }, label = "background offset" ) { movie -> IntOffset( x = with(LocalDensity.current) { (backgroundBoxWidth * movie.index).toPx() // dp -> float }.toInt(), // float -> int y = 0 ) } val backgroundShapeTransition by selectedTabTypeTransition.animateValue( transitionSpec = { defaultTween() }, label = "background shape", typeConverter = TwoWayConverter( convertToVector = { corner -> AnimationVector4D( v1 = corner.topStart.toPercent(), v2 = corner.topEnd.toPercent(), v3 = corner.bottomStart.toPercent(), v4 = corner.bottomEnd.toPercent() ) }, convertFromVector = { vector ->

Slide 89

Slide 89 text

/** * গפݫ੉࣌ী ࢎਊغח ݽٚ ؘ੉ఠ ਬഋ਷ ରਗী ٮۄ * [AnimationVector1D], [AnimationVector2D], [AnimationVector3D] ژח [AnimationVector4D] ۽ ߸ജؾפ׮. * ٮۄࢲ ё୓੄ ৈ۞ ҳࢿਃࣗܳ пп ੗୓ ࣘب ୶੸ ӝמਸ ࢎਊೞৈ ة݀੸ਵ۽ গפݫ੉࣌ ୊ܻೡ ࣻ ੓णפ׮. * * ৘ܳ ٜয [Color] ח A, R, G, B 4ѐ੄ ҳࢿ ਃࣗ۽ ੉ܖযઉ ੓णפ׮. * ٮۄࢲ [AnimationVector4D] ܳ ࢎਊ೤פ׮. */ sealed class AnimationVector { internal abstract fun reset() internal abstract fun newVector(): AnimationVector internal abstract operator fun get(index: Int): Float internal abstract operator fun set(index: Int, value: Float) internal abstract val size: Int }

Slide 90

Slide 90 text

/** * ੐੄੄ ఋੑ [T] ীࢲ [AnimationVector] ۽ ߸ജೞҊ [AnimationVector] ܳ ׮द ఋੑ [T] ۽ ߸ജೞח ߑߨী ؀ೠ ੿੄о ನೣغয ੓णפ׮. * ੉ܳ ా೧ গפݫ੉࣌ਸ ݽٚ ఋੑ੄ ѐ୓ীࢲ ҳഅೡ ࣻ ੓णפ׮. */ interface TwoWayConverter { /** * ఋੑ [T] ਸ [AnimationVector] ఋੑਵ۽ ߸ജೞח ߑߨਸ ੿੄೤פ׮. */ val convertToVector: (T) -> V /** * [AnimationVector] ఋੑਸ ׮द ఋੑ [T] ۽ ߸ജೞח ߑߨਸ ੿੄೤פ׮. */ val convertFromVector: (V) -> T }

Slide 91

Slide 91 text

) } val backgroundShapeTransition by selectedTabTypeTransition.animateValue( transitionSpec = { defaultTween() }, label = "background shape", typeConverter = TwoWayConverter( convertToVector = { corner -> AnimationVector4D( v1 = corner.topStart.getPercent(), v2 = corner.topEnd.getPercent(), v3 = corner.bottomStart.getPercent(), v4 = corner.bottomEnd.getPercent() ) }, convertFromVector = { vector -> RoundedCornerShape( topStartPercent = vector.v1.toInt(), topEndPercent = vector.v2.toInt(), bottomStartPercent = vector.v3.toInt(), bottomEndPercent = vector.v4.toInt() ) } ) ) { movie -> when (movie) { Movie.Thor -> RoundedCornerShape(bottomStartPercent = 30) Movie.Spider -> RoundedCornerShape(percent = 0)

Slide 92

Slide 92 text

) ) { movie -> when (movie) { Movie.Thor -> RoundedCornerShape(bottomStartPercent = 30) // ৽ଃ -> ৽ଃ ೞױ݅ ۄ਍٬ Movie.Spider -> RoundedCornerShape(percent = 0) // о਍ؘ -> ۄ਍٬ X Movie.Doctor -> RoundedCornerShape(bottomEndPercent = 30) // য়ܲଃ -> য়ܲଃ ೞױ݅ ۄ਍٬ } } TabContainer { TabDefaults.Items.forEach { tab -> val textColor by selectedTabTypeTransition.animateColor( transitionSpec = { defaultTween() }, label = "text color" ) { movie -> when (movie == tab.type) { true -> TabDefaults.Color.selectedText false -> TabDefaults.Color.defaultText } } TabItem( title = tab.shortname, backgroundColor = TabDefaults.Color.defaultBackground, textColor = textColor, onTabClick = {

Slide 93

Slide 93 text

false -> TabDefaults.Color.defaultText } } TabItem( title = tab.shortname, backgroundColor = TabDefaults.Color.defaultBackground, textColor = textColor, onTabClick = { updateSelectedTab(tab) } ) } } Box( modifier = Modifier .width(backgroundBoxWidth) .height(TabDefaults.Height) .offset { backgroundOffsetTransition } .clip(backgroundShapeTransition) .background(color = TabDefaults.Color.selectedBackground) ) } }

Slide 94

Slide 94 text

🤩🥳

Slide 95

Slide 95 text

\ start fi nish Lookahead ௼ӝয়೐ࣇগפݫ੉࣌9

Slide 96

Slide 96 text

fun Modifier.movement(lookaheadScope: LookaheadLayoutScope) = composed { var targetOffset: IntOffset? by remember { mutableStateOf(null) } var placementOffset by remember { mutableStateOf(IntOffset.Zero) } // 1. ӝࠄч੉ হ׮: animateAsState X // 2. ׮਺ গפݫ੉࣌ чਵ۽ ੿೧૓ ч੉ হ׮: Transition X with(lookaheadScope) { this@composed .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(sourceCoordinates = layoutCoordinates) .round() placementOffset = lookaheadScopeCoordinates .localPositionOf( sourceCoordinates = layoutCoordinates, relativeToSource = Offset.Zero ) .round() } .intermediateLayout { measurable, constraints, _ -> val placeable = measurable.measure(constraints) layout(width = placeable.width, height = placeable.height) { val (x, y) = targetOffset!! - placementOffset

Slide 97

Slide 97 text

fun Modifier.transformation(lookaheadScope: LookaheadLayoutScope) = with(lookaheadScope) { intermediateLayout { measurable, _, lookaheadSize -> val (width, height) = lookaheadSize // 1. ӝࠄч੉ হ׮: animateAsState X // 2. ׮਺ গפݫ੉࣌ чਵ۽ ੿೧૓ ч੉ হ׮: Transition X val animatedConstraints = Constraints.fixed( width = width.coerceAtLeast(0), height = height.coerceAtLeast(0) ) val placeable = measurable.measure(animatedConstraints) layout(width = placeable.width, height = placeable.height) { placeable.place(x = 0, y = 0) } } }

Slide 98

Slide 98 text

/** * [animateTo] ܳ ా೧ ч੉ ߸҃ؼ ٸ ੗زਵ۽ чী গפݫ੉࣌ਸ ੸ਊೞח ч ഓ؊ੑפ׮. */ class Animatable( initialValue: T, // ୡӝ ч val typeConverter: TwoWayConverter, // গפݫ੉࣌ ё୓ী ࢎਊೡ AnimationVector private val visibilityThreshold: T? = null // оदࢿ ੐҅ч ) { // গפݫ੉࣌੄ അ੤ ч val value: T get() = internalState.value // അ੤ গפݫ੉࣌੄ ؀࢚. গפݫ੉࣌੉ ઺ױ হ੉ ՘աݶ ੉ ݾ಴ чী ب׳೤פ׮. var taretValue: T by mutableStateOf(initialValue) private set // targetValue чਸ ೱ೧ গפݫ੉࣌ਸ द੘೤פ׮. block ੋ੗ח ݒ গפݫ੉࣌ ೐ۨ੐ ݃׮ ഐ୹ؾפ׮. suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable.() -> Unit)? = null ): AnimationResult }

Slide 99

Slide 99 text

fun Modifier.animateMovement( lookaheadScope: LookaheadLayoutScope, animationSpec: AnimationSpec = defaultSpring() ) = composed { var placementOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } var targetOffsetAnimation: Animatable? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { snapshotFlow { targetOffset }.collect { target -> if (target != null && target != targetOffsetAnimation?.targetValue) { targetOffsetAnimation?.run { launch { animateTo( targetValue = target, animationSpec = animationSpec ) } } ?: Animatable( initialValue = target, typeConverter = IntOffset.VectorConverter ).let { offsetAnimatable ->

Slide 100

Slide 100 text

animationSpec = animationSpec ) } } ?: Animatable( initialValue = target, typeConverter = IntOffset.VectorConverter ).let { offsetAnimatable -> targetOffsetAnimation = offsetAnimatable } } } } with(lookaheadScope) { this@composed .onPlaced { /* ӝઓҗ زੌ */ } .intermediateLayout { measurable, constraints, _ -> val placeable = measurable.measure(constraints) layout(width = placeable.width, height = placeable.height) { val (x, y) = (targetOffsetAnimation?.value ?: targetOffset!!) - placementOffset placeable.place(x = x, y = y) } } } }

Slide 101

Slide 101 text

fun Modifier.animateTransformation( lookaheadScope: LookaheadLayoutScope, animationSpec: AnimationSpec = defaultSpring() ) = composed { var targetSize: IntSize? by remember { mutableStateOf(null) } var targetSizeAnimation: Animatable? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { snapshotFlow { targetSize }.collect { target -> if (target != null && target != targetSizeAnimation?.targetValue) { targetSizeAnimation?.run { launch { animateTo( targetValue = target, animationSpec = animationSpec ) } } ?: Animatable( initialValue = target, typeConverter = IntSize.VectorConverter ).let { sizeAnimatable -> targetSizeAnimation = sizeAnimatable }

Slide 102

Slide 102 text

} ?: Animatable( initialValue = target, typeConverter = IntSize.VectorConverter ).let { sizeAnimatable -> targetSizeAnimation = sizeAnimatable } } } } with(lookaheadScope) { [email protected] { measurable, _, lookaheadSize -> targetSize = lookaheadSize val (width, height) = targetSizeAnimation?.value ?: lookaheadSize val animatedConstraints = Constraints.fixed( width = width.coerceAtLeast(0), height = height.coerceAtLeast(0) ) val placeable = measurable.measure(animatedConstraints) layout(width = placeable.width, height = placeable.height) { placeable.place(x = 0, y = 0) } } }

Slide 103

Slide 103 text

No content

Slide 104

Slide 104 text

ೞա੄"OJNBUJPO4QFD݅оמ ೞա੄গפݫ੉࣌݅оמ ೞա੄ఋੑ݅оמ "OJNBUF <5> 4UBUF 4UBUF <5> "OJNBUF"T4UBUF "OJNBUJPO 4QFD PCKFDU PCKFDU

Slide 105

Slide 105 text

ৈ۞ѐ੄"OJNBUJPO4QFDоמ ৈ۞ѐ੄গפݫ੉࣌оמ ೞա੄ఋੑ݅оמ "OJNBUBCMF <5> 4UBUF <5> PCKFDU PCKFDU BOJNBUF5P BOJNBUF5P "OJNBUJPO 4QFD "OJNBUJPO 4QFD "OJNBUBCMF

Slide 106

Slide 106 text

ৈ۞ѐ੄"OJNBUJPO4QFDоמ ৈ۞ѐ੄গפݫ੉࣌оמ ৈ۞ѐ੄ఋੑоמ উ٘۽੉٘झౚ٣য়JOTQFDUоמ 5SBOTJUJPO 
 <5> 4UBUF <5> 5SBOTJUJPO PCKFDU PCKFDU BOJNBUF <5> BOJNBUF <5> "OJNBUJPO 4QFD "OJNBUJPO 4QFD

Slide 107

Slide 107 text

3. Internal System ↟-JWF-JUFSBM ↟4UBCJMJUZ

Slide 108

Slide 108 text

No content

Slide 109

Slide 109 text

fun earth() { print("Bye World") }

Slide 110

Slide 110 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 111

Slide 111 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 112

Slide 112 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 113

Slide 113 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 114

Slide 114 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 115

Slide 115 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 116

Slide 116 text

fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 117

Slide 117 text

private val liveLiteralCache = HashMap>() @InternalComposeApi @ComposeCompilerApi fun liveLiteral(key: String, value: T): State { return liveLiteralCache.getOrPut(key) { mutableStateOf(value) } as State }

Slide 118

Slide 118 text

@InternalComposeApi fun updateLiveLiteralValue(key: String, value: Any?) { var needToUpdate = true val stateObj = liveLiteralCache.getOrPut(key) { needToUpdate = false mutableStateOf(value) } if (needToUpdate) { stateObj.value = value } }

Slide 119

Slide 119 text

private fun findEffectiveRecomposeScope(group: Int): RecomposeScopeImpl? { var current = group while (current > 0) { for (data in DataIterator(this, current)) { if (data is RecomposeScopeImpl) { return data } } current = groups.parentAnchor(current) } return null }

Slide 120

Slide 120 text

// @Composable XXX fun earth() { print(LiveLiterals$EarthKt.`getString$arg-0$call-print$fun-earth`()) } object LiveLiterals$EarthKt { var `String$arg-0$call-print$fun-earth` = "Bye World" var `State$String$arg-0$call-print$fun-earth`: MutableState? = null fun `getString$arg-0$call-print$fun-earth`(): String { val field = this.`String$arg-0$call-print$fun-earth` val state = if (field == null) { val tmp = liveLiteral( "String$arg-0$call-print$fun-earth", this.`String$arg-0$call-print$fun-earth` ) this.`String$arg-0$call-print$fun-earth` = tmp tmp } else field return field.value } }

Slide 121

Slide 121 text

/** * LiveLiteral ੉ ெઉ ੓ח ҃਋ীب ੸ਊغח ߧਤ ղীࢲ LiveLiteral ਸ ࢤࢿೞ૑ ঋب۾ * ஹನૉ ஹ౵ੌ۞ী ಴दೞח ؘ ࢎਊؾפ׮. */ @Target( AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.FILE ) @Retention(AnnotationRetention.SOURCE) annotation class NoLiveLiterals @NoLiveLiterals fun earth() { print("Bye World") }

Slide 122

Slide 122 text

3. Internal System ↟-JWF-JUFSBM ↟4UBCJMJUZ

Slide 123

Slide 123 text

/** * উ੿ ࢚కח ௼ѱ 3о૑੄ ઑѤਸ ٮܵפ׮. * * - ч੉ ߸҃عਸ ҃਋ ஹನ੷࠶ীѱ ঌ۰ઉঠ ೤פ׮. (૊, [State] ۽ ੘زظঠ ೣ) * - ੉੹ ੋझఢझ৬ അ੤ ੋझఢझо زੌ೧ঠ ೤פ׮. * - ݽٚ ҕѐ ೙ٜ٘ب ׮ উ੿ ࢚కৈঠ ೤פ׮. * * ੉ 3о૑ ઑѤਸ ٮܲ׮ݶ ஹನૉח ೧׼ ೙٘о উ੿੸ੋ ࢚కۄҊ ౵ঈೞҊ, * ೧׼ ೙٘੄ ч੉ ߄Շ૑ ঋও׮ݶ ࢎਊػ ஹನ੷࠶੄ ܻஹನ૑࣌ਸ ࢤۚ೤פ׮. * * @see Immutable * @see Stable */ @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) annotation class StableMarker

Slide 124

Slide 124 text

/** * ࢤࢿػ ੉റ۽ ݽٚ ҕѐ੸ੋ ೙٘о ੺؀ ߸ೞ૑ ঋח׮ח Ѫਸ աఋղݴ ௿ېझী ੸ਊؼ ࣻ ੓णפ׮. * * ࢤࢿ ੉റ ч੉ ߸҃غ૑ ঋਵ޲۽ উ੿੄ ୐ ߣ૩ ӏ஗ੋ * "ч੉ ߸҃عਸ ҃਋ ஹನ੷࠶ীѱ ঌ۰ઉঠ ೤פ׮." о ޖदؾפ׮. */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) @StableMarker annotation class Immutable fun main() { val list = mutableListOf(1) list.add(2) // ੋझఢझח زੌೞ૑݅ ч੉ ߄Չ ࣻ ੓णפ׮. // @Immutable ਷ ੉۞ೠ ߸҃ب ೲਊೞ૑ ঋणפ׮. }

Slide 125

Slide 125 text

/** * ч੉ ߸҃ؼ ࣻ ੓ח ࢚క੉ݴ, ੸ਊ ؀࢚ী ٮۄ ডрঀ ৉ೡ੉ ׳ۄ૘פ׮. * * ఋੑী ੸ਊػ׮ݶ ୶о ৉ೡ হ੉ [StableMarker] ੄ ৉ೡਸ Ӓ؀۽ оઉцפ׮. * * ೣࣻա ೐۽ಌ౭ী ੸ਊػ׮ݶ [StableMarker] ੄ ৉ೡী ୶оغח ৉ೡ੉ ࢤӤפ׮. * э਷ input ী ੓যࢲח ೦࢚ زੌೠ output ਸ ٜ݅যղݴ(ࣽࣻ ೣࣻ), ೣࣻ੄ ҃਋ ੋ੗ٜ ৉द ݽف উ੿੸ੋ ࢚కۄח Ѫਸ ডࣘ೤פ׮. * ݅ড input ੉ زੌೞ׮ݶ output ژೠ زੌೡ Ѫ੉ӝ ٸޙী ܻஹನ૑࣌ਸ झఈೞѱ ؾפ׮. */ @Target( AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY ) @Retention(AnnotationRetention.BINARY) @StableMarker annotation class Stable

Slide 126

Slide 126 text

/** * ஹ౵ੌ द੼ী ݽٚ ௿ېझٜ੄ উ੿ࢿਸ ੗زਵ۽ ୶ۿ೤פ׮. * * @param parameters উ੿ࢿਸ ୶ۿೞחؘ ب਑੉ ؼ ࣻ ੓ѱ ੋ੗ٜ੄ ࠺౟݃झ௼ܳ աఋշפ׮. */ @ComposeCompilerApi @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) annotation class StabilityInferred(val parameters: Int) // ਬੌೠ ೙٘ੋ value о উ੿ ࢚క੉Ҋ ࠛ߸ೞӝ ٸޙী Name ௿ېझח উ੿ ࢚క۽ ౸ױؾפ׮. class Name(val value: String) // ਬੌೠ ೙٘ੋ value ੄ ఋੑ੉ ઁ֎ܼ੉ۄ ఋੑਸ ౸ױೡ ࣻ হӝী ࢎ੹ী ࠺౟݃झఊػ чਸ ӝળਵ۽ উ੿ ࢚కܳ ୶ۿ೤פ׮. // ೞ૑݅ ೙٘о о߸ ࢚క੉ӝ ٸޙী T ఋੑ੉ উ੿੉ৈب NameWithGeneric ௿ېझח ࠛউ੿ ࢚క۽ ౸ױؾפ׮. class NameWithGeneric(var value: T)

Slide 127

Slide 127 text

@Composable fun TextWithImmutableList(texts: List) { Text(text = texts.joinToString()) }

Slide 128

Slide 128 text

public interface MutableList : List, MutableCollection @Composable fun TextWithImmutableList(texts: List = mutableListOf()) { Text(text = texts.joinToString()) }

Slide 129

Slide 129 text

@Immutable class ImmutableListWrapper(val values: List) @Composable fun TextWithImmutableList(texts: ImmutableListWrapper) { Text(text = texts.values.joinToString()) }

Slide 130

Slide 130 text

// https://github.com/Kotlin/kotlinx.collections.immutable @Composable fun TextWithImmutableList(texts: ImmutableList) { Text(text = texts.joinToString()) }

Slide 131

Slide 131 text

4. Compiler Debugging ↟CVJMENFUSJDT ↟CVJMESFQPSUT

Slide 132

Slide 132 text

CVJMENFUSJDT // ஹ౵ੌ۞о х૑ೠ ஹನ੷࠶ ೣٜࣻ੄ ੿ࠁܳ աఋշפ׮. kotlinOptions { freeCompilerArgs += [ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + "${rootProject.file(".").absolutePath}/report/compose-metrics" ] }

Slide 133

Slide 133 text

<ݽٕݺ>@<࠽٘ఋੑ>NPEVMFKTPO { { "skippableComposables": 48, // skippable ࢚కੋ ஹನ੷࠶ ѐࣻ "restartableComposables": 67, "readonlyComposables": 0, "totalComposables": 67, // ੹୓ ஹನ੷࠶ ѐࣻ "restartGroups": 67, "totalGroups": 72, "staticArguments": 130, "certainArguments": 28, "knownStableArguments": 793, // উ੿ ࢚క۽ ঌ۰૓ ੋ੗ ѐࣻ "knownUnstableArguments": 30, // ࠛউ੿ ࢚క۽ ঌ۰૓ ੋ੗ ѐࣻ "unknownStableArguments": 8, "totalArguments": 831, "markedStableClasses": 0, "inferredStableClasses": 16, // উ੿ ࢚క۽ ୶ۿػ ௿ېझ ѐࣻ "inferredUnstableClasses": 2, // ࠛউ੿ ࢚క۽ ୶ۿػ ௿ېझ ѐࣻ "inferredUncertainClasses": 0, "effectivelyStableClasses": 16, "totalClasses": 18, "memoizedLambdas": 54, "singletonLambdas": 5, "singletonComposableLambdas": 8, "composableLambdas": 31, "totalLambdas": 89 }

Slide 134

Slide 134 text

CVJMESFQPSUT // ஹ౵ੌ۞о х૑ೠ ஹನ੷࠶ ೣٜࣻ੉ ࢎਊ઺ੋ ௿ېझ৬ ੋ੗ী ؀ೠ উ੿ࢿ ੿ࠁܳ աఋշפ׮. kotlinOptions { freeCompilerArgs += [ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + "${rootProject.file(".").absolutePath}/report/compose-reports" ] }

Slide 135

Slide 135 text

<ݽٕݺ>@<࠽٘ఋੑ>DMBTTFTUYU /* ——— classes ——— */ sealed class UiState { object Loading : UiState() object Done : UiState() data class Exception(val throwable: Throwable) : UiState() } /* ——— reports ——— */ stable class Loading { = Stable } stable class Done { = Stable } unstable class Exception { unstable val throwable: Throwable = Unstable }

Slide 136

Slide 136 text

<ݽٕݺ>@<࠽٘ఋੑ>DPNQPTBCMFTDTW /* ——— composables ——— */ @Composable fun DisplayText(text: String = "Hi") { Text(text = text) } @Composable fun DisplayTexts(texts: List) { Text(text = texts.joinToString()) } /* ——— reports ——— */

Slide 137

Slide 137 text

<ݽٕݺ>@<࠽٘ఋੑ>DPNQPTBCMFTUYU /* ——— composables ——— */ @Composable fun DisplayText(text: String = "Hi") { Text(text = text) } @Composable fun DisplayTexts(texts: List) { Text(text = texts.joinToString()) } /* ——— reports ——— */ restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun DisplayText( stable text: String? = @dynamic LiveLiterals$MainActivityKt.String$param-text$fun-DisplayText() ) restartable /* skippable ੉ হ਺ */ scheme("[androidx.compose.ui.UiComposable]") fun DisplayTexts( unstable texts: List )

Slide 138

Slide 138 text

<ݽٕݺ>@<࠽٘ఋੑ>DPNQPTBCMFTUYU /* ——— composables ——— */ @Composable fun DisplayText(text: String = "Hi") { Text(text = text) } @Composable fun DisplayTexts(texts: List) { Text(text = texts.joinToString()) } /* ——— reports ——— */ restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun DisplayText( stable text: String? = @dynamic LiveLiterals$MainActivityKt.String$param-text$fun-DisplayText() ) restartable scheme("[androidx.compose.ui.UiComposable]") fun DisplayTexts( unstable texts: List )

Slide 139

Slide 139 text

<ݽٕݺ>@<࠽٘ఋੑ>DPNQPTBCMFTUYU /* ——— composables ——— */ @NoLiveLiterals // new @Composable fun DisplayText(text: String = "Hi") { Text(text = text) } @Composable fun DisplayTexts(texts: List) { Text(text = texts.joinToString()) } /* ——— reports ——— */ restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun DisplayText( stable text: String? = @static "Hi" ) restartable scheme("[androidx.compose.ui.UiComposable]") fun DisplayTexts( unstable texts: List )

Slide 140

Slide 140 text

5. Best Practice ↟ܻஹನ૑࣌ߧਤ઴੉ӝ ↟ܻஹನ૑࣌হগӝ ↟ࠛ೙ਃೠ࢚క୶੸ઁѢ ↟"05ஹ౵ੌഝࢿച

Slide 141

Slide 141 text

EPOVUIPMFTLJQQJOH setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Text( modifier = Modifier.clickable { number++ }, text = number.toString() ).also { println("Text recomposition") } } /* ܻஹನ૑࣌੉ য٣ী ૓೯ؼөਃ? 1, setContent content ৔৉ 2. Text 3. setContent content ৔৉ + Text */

Slide 142

Slide 142 text

EPOVUIPMFTLJQQJOH setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Text( modifier = Modifier.clickable { number++ }, text = number.toString() ).also { println("Text recomposition") } } /* [first-composition] setContent recomposition Text recomposition [re-composition] setContent recomposition Text recomposition ੉റ زੌ ۽Ӓ ߈ࠂ */

Slide 143

Slide 143 text

EPOVUIPMFTLJQQJOH setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Button(onClick = { number++ }) { Text( text = number.toString() ).also { println("Text recomposition") } }.also { println("Button recomposition") } } /* [first-composition] setContent recomposition Text recomposition Button recomposition [re-composition] Text recomposition ੉റ زੌ ۽Ӓ ߈ࠂ */

Slide 144

Slide 144 text

EPOVUIPMFTLJQQJOH // ஹ౵ੌ ੹ @Composable fun TextWrapper(text: String) { Text(text = text) } // ஹ౵ੌ റ (೨ब ࠗ࠙݅ ಴द) @Composable fun TextWrapper(composer: Composer, text: String) { composer.startRestartGroup(-199242123) // ղࠗীࢲ addRecomposeScope() ۽ ܻஹನ૑࣌ झ௏೐ܳ ୶оೞҊ ੓਺ Text(text = text) composer.endRestartGroup() }

Slide 145

Slide 145 text

EPOVUIPMFTLJQQJOH setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Text( modifier = Modifier.clickable { number++ }, text = number.toString() ).also { println("Text recomposition") } } setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Button(onClick = { number++ }) { Text( text = number.toString() ).also { println("Text recomposition") } }.also { println("Button recomposition") } } Text RecomposeScope 
 Text RecomposeScope 


Slide 146

Slide 146 text

EPOVUIPMFTLJQQJOH setContent { var number by remember { mutableStateOf(0) } println("setContent recomposition") Button(onClick = { number++ }) { Text( text = number.toString() ).also { println("Text recomposition") } }.also { println("Button recomposition") } } Text RecomposeScope 
 Button RecomposeScope

Slide 147

Slide 147 text

4UBCJMJUZ4ZTUFN setContent { var number by remember { mutableStateOf(1) } TextWrapper( modifier = Modifier.clickable { number++ }, text = number.toString() ) } @Composable fun TextWrapper( modifier: Modifier = Modifier, text: String ) { Text(modifier = modifier, text = text) }

Slide 148

Slide 148 text

4UBCJMJUZ4ZTUFN setContent { var number by remember { mutableStateOf(1) } TextWithLambda( modifier = Modifier.clickable { number++ }, text = { number.toString() } ) } @Composable fun TextWithLambda( modifier: Modifier = Modifier, text: () -> String // Function0 ) { Text(modifier = modifier, text = text()) }

Slide 149

Slide 149 text

5. Best Practice ↟ܻஹನ૑࣌ߧਤ઴੉ӝ ↟ܻஹನ૑࣌হগӝ ↟ࠛ೙ਃೠ࢚క୶੸ઁѢ ↟"05ஹ౵ੌഝࢿച

Slide 150

Slide 150 text

NPWBCMF$POUFOU0G val content = remember<@Composable () -> Unit> { { repeat(2) { Box(modifier = Modifier.size(100.dp).background(color = Color.Green)) } } } Column { Button(onClick = { isRow = !isRow }) { Text(text = "Switch") } if (isRow) { Row { content() } } else { Column { content() } } }

Slide 151

Slide 151 text

NPWBCMF$POUFOU0G val content = remember { movableContentOf { // movableContentWithReceiverOf repeat(2) { Box(modifier = Modifier.size(100.dp).background(color = Color.Green)) } } } Column { Button(onClick = { isRow = !isRow }) { Text(text = "Switch") } if (isRow) { Row { content() } } else { Column { content() } } }

Slide 152

Slide 152 text

!3FBE0OMZ$PNQPTBCMF object MaterialTheme { val colors: Colors @Composable @ReadOnlyComposable get() = LocalColors.current val typography: Typography @Composable @ReadOnlyComposable get() = LocalTypography.current // … } @Composable @ReadOnlyComposable fun stringResource(@StringRes id: Int): String { val resources = resources() return resources.getString(id) }

Slide 153

Slide 153 text

!/PO3FTUBSUBCMF$PNQPTBCMF @Composable @NonRestartableComposable fun DisposableEffect(key1: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult) { remember(key1) { DisposableEffectImpl(effect) } } @Composable @NonRestartableComposable fun Image( modifier: Modifier = Modifier, colorFilter: ColorFilter? = null, imageVector: ImageVector, contentDescription: String?, alpha: Float = DefaultAlpha, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit ) { Image( modifier = modifier, alignment = alignment, contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, painter = rememberVectorPainter(imageVector), contentDescription = contentDescription ) }

Slide 154

Slide 154 text

5. Best Practice ↟ܻஹನ૑࣌ߧਤ઴੉ӝ ↟ܻஹನ૑࣌হগӝ ↟ࠛ೙ਃೠ࢚క୶੸ઁѢ ↟"05ஹ౵ੌഝࢿച

Slide 155

Slide 155 text

TUBUJD$PNQPTJUJPO-PDBM0G val LocalContext = staticCompositionLocalOf { noLocalProvidedFor("LocalContext") } val LocalClipboardManager = staticCompositionLocalOf { noLocalProvidedFor("LocalClipboardManager") } val LocalDensity = staticCompositionLocalOf { noLocalProvidedFor("LocalDensity") } val LocalFocusManager = staticCompositionLocalOf { noLocalProvidedFor("LocalFocusManager") }

Slide 156

Slide 156 text

!/P-JWF-JUFSBMT @file:NoLiveLiterals @NoLiveLiterals

Slide 157

Slide 157 text

5. Best Practice ↟ܻஹನ૑࣌ߧਤ઴੉ӝ ↟ܻஹನ૑࣌হগӝ ↟ࠛ೙ਃೠ࢚క୶੸ઁѢ ↟"05ஹ౵ੌഝࢿച

Slide 158

Slide 158 text

#BTFMJOFQSPGJMFT #BTFMJOFQSPGJMFT਷ݠन௏٘ী؀ೠ઺ਃೠ҃۽ܳࢎ੹ஹ౵ੌೞӝਤ೧ 
 ࢸ஖઺ী"OESPJE۠ఋ੐ "35 ীࢲࢎਊೞח"1,ীನೣػ௿ېझ߂ݫࢲ٘ݾ۾ੑפ׮ IUUQTEFWFMPQFSBOESPJEDPNUPQJDQFSGPSNBODFCBTFMJOFQSPGJMFT

Slide 159

Slide 159 text

#BTFMJOFQSPGJMFT .BDSPCFODINBSLݽٕࢸ੿

Slide 160

Slide 160 text

#BTFMJOFQSPGJMFT .BDSPCFODINBSLݽٕࢸ੿ ௏٘੘ࢿ CBTFMJOFQSPGJMFTࢤࢿ @ExperimentalBaselineProfilesApi @RunWith(AndroidJUnit4::class) class BaselineProfileGenerator { @get:Rule val baselineProfileRule = BaselineProfileRule() @Test fun startup() = baselineProfileRule.collectBaselineProfile( packageName = AppPackageName ) { pressHome() startActivityAndWait() } }

Slide 161

Slide 161 text

#BTFMJOFQSPGJMFT .BDSPCFODINBSLݽٕࢸ੿ ௏٘੘ࢿ CBTFMJOFQSPGJMFT੸ਊ੹റஏ੿ @RunWith(AndroidJUnit4::class) class BaselineProfileBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() // … private fun startup(compilationMode: CompilationMode) { benchmarkRule.measureRepeated( packageName = AppPackageName, metrics = listOf(StartupTimingMetric()), iterations = 10, startupMode = StartupMode.COLD, compilationMode = compilationMode ) { pressHome() startActivityAndWait() } } }

Slide 162

Slide 162 text

#BTFMJOFQSPGJMFT .BDSPCFODINBSLݽٕࢸ੿ ௏٘੘ࢿ CBTFMJOFQSPGJMFT੸ਊ੹റஏ੿ @RunWith(AndroidJUnit4::class) class BaselineProfileBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun startupNoCompilation() { startup(compilationMode = CompilationMode.None()) // JIT ஹ౵ੌ } @Test fun startupBaselineProfile() { startup(compilationMode = CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require)) // AOT ஹ౵ੌ } // … }

Slide 163

Slide 163 text

#BTFMJOFQSPGJMFT .BDSPCFODINBSLݽٕࢸ੿ ௏٘੘ࢿ #VJME7BSJBOUTࢸ੿

Slide 164

Slide 164 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച

Slide 165

Slide 165 text

#BTFMJOFQSPGJMFT adb root

Slide 166

Slide 166 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO

Slide 167

Slide 167 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO adb pull

Slide 168

Slide 168 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO adb pull CBTFMJOFQSPGUYUਵ۽౵ੌݺ߸҃റTSDNBJOਵ۽੉ز

Slide 169

Slide 169 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO adb pull CBTFMJOFQSPGUYUਵ۽౵ੌݺ߸҃റTSDNBJOਵ۽੉ز ੸ਊ੹റ࠺Ү

Slide 170

Slide 170 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO adb pull CBTFMJOFQSPGUYUਵ۽౵ੌݺ߸҃റTSDNBJOਵ۽੉ز ੸ਊ੹റ࠺Ү

Slide 171

Slide 171 text

#BTFMJOFQSPGJMFT ܖ౟ӂೠഝࢿച 3VO adb pull CBTFMJOFQSPGUYUਵ۽౵ੌݺ߸҃റTSDNBJOਵ۽੉ز ੸ਊ੹റ࠺Ү ੸ਊೡݽٕী੄ઓࢿ୶о dependencies { implementation("androidx.profileinstaller:profileinstaller:1.2.0-beta01") }

Slide 172

Slide 172 text

6. Wrap-up ↟ة݀ߡ੹ҙܻ୓҅द੘

Slide 173

Slide 173 text

6. Wrap-up ↟नӏ"1*DPMMFDU"T4UBUF8JUI-JGFDZDMF

Slide 174

Slide 174 text

хࢎ೤פ׮ ஹನૉ ղࠗ ௏ٜ٘਷ cs.android.com ীࢲ оઉ৳णפ׮. ੉ ೐ۨઃప੉࣌ীח షझ౱ীࢲ ઁҕೠ షझಕ੉झо ੸ਊغয ੓णפ׮. QnA🙋