{"id":13659588,"url":"https://github.com/skydoves/orbital","last_synced_at":"2025-05-16T08:04:23.517Z","repository":{"id":41455423,"uuid":"509655833","full_name":"skydoves/Orbital","owner":"skydoves","description":"🪐 Jetpack Compose Multiplatform library that allows you to implement dynamic transition animations such as shared element transitions.","archived":false,"fork":false,"pushed_at":"2024-06-19T01:52:24.000Z","size":43252,"stargazers_count":1158,"open_issues_count":5,"forks_count":37,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-05-16T08:04:18.321Z","etag":null,"topics":["android","animation","jetpack-compose","sharedelementtransitions","skydoves"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skydoves.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"skydoves","custom":["https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"]}},"created_at":"2022-07-02T04:20:22.000Z","updated_at":"2025-05-16T02:19:46.000Z","dependencies_parsed_at":"2024-08-01T19:45:21.463Z","dependency_job_id":"9bb2e48c-f5eb-4aa9-bb31-590514167276","html_url":"https://github.com/skydoves/Orbital","commit_stats":null,"previous_names":["skydoves/orbitary"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FOrbital","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FOrbital/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FOrbital/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FOrbital/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skydoves","download_url":"https://codeload.github.com/skydoves/Orbital/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254493378,"owners_count":22080126,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","animation","jetpack-compose","sharedelementtransitions","skydoves"],"created_at":"2024-08-02T05:01:10.352Z","updated_at":"2025-05-16T08:04:18.509Z","avatar_url":"https://github.com/skydoves.png","language":"Kotlin","readme":"\u003ch1 align=\"center\"\u003eOrbital\u003c/h1\u003e\u003c/br\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"https://devlibrary.withgoogle.com/products/android/repos/skydoves-Orbitary\"\u003e\u003cimg alt=\"Google\" src=\"https://skydoves.github.io/badges/google-devlib.svg\"/\u003e\u003c/a\u003e\u003cbr\u003e\n  \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\u003cimg alt=\"License\" src=\"https://img.shields.io/badge/License-Apache%202.0-blue.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://android-arsenal.com/api?level=21\"\u003e\u003cimg alt=\"API\" src=\"https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/skydoves/Orbital/actions/workflows/android.yml\"\u003e\u003cimg alt=\"Build Status\" \n  src=\"https://github.com/skydoves/Orbital/actions/workflows/android.yml/badge.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://androidweekly.net/issues/issue-525\"\u003e\u003cimg alt=\"Android Weekly\" src=\"https://skydoves.github.io/badges/android-weekly.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://us12.campaign-archive.com/?u=f39692e245b94f7fb693b6d82\u0026id=68710ad80a\"\u003e\u003cimg alt=\"Kotlin Weekly\" src=\"https://skydoves.github.io/badges/kotlin-weekly.svg\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/skydoves\"\u003e\u003cimg alt=\"Profile\" src=\"https://skydoves.github.io/badges/skydoves.svg\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n🪐 Jetpack Compose animation library that allows you to implement animations such as shared element transition. This library support Kotlin Multiplatform (Android, iOS, Desktop, macOS, and js)\n\u003c/p\u003e\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"previews/preview0.gif\" width=\"270\"/\u003e\n\u003cimg src=\"previews/preview1.gif\" width=\"270\"/\u003e\n\u003cimg src=\"previews/preview3.gif\" width=\"270\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"previews/preview4.gif\" width=\"270\"/\u003e\n\u003cimg src=\"previews/preview5.gif\" width=\"270\"/\u003e\n\u003c/p\u003e\n\n## Download\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.skydoves/orbital.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.skydoves%22%20AND%20a:%22orbital%22)\n\n### Gradle\n\nAdd the dependency below to your **module**'s `build.gradle` file:\n\n```gradle\ndependencies {\n    implementation(\"com.github.skydoves:orbital:0.4.0\")\n}\n```\n\n\u003e **Note**: This is an experimental library that demonstrates various animations with Jetpack Compose. Please make sure that your project uses Jetpack Compose `1.5.4`, Compose Compiler `1.5.4`, and Kotlin `1.9.20`.\n\nFor Kotlin Multiplatform, add the dependency below to your **module**'s `build.gradle.kts` file:\n\n```gradle\nsourceSets {\n    val commonMain by getting {\n        dependencies {\n            implementation(\"com.github.skydoves:orbital:$version\")\n        }\n    }\n}\n```\n\n## Usage\n\nYou can implement three kinds of animations with Orbital: **Movement**, **Transformation**, and **Shared Element Transition**. \nBasically, you can run animation with `Orbital` Composable function, which provides `OrbitalScope` that allows you to create animations.\n\n### Transformation\n\n\u003cimg src=\"previews/preview1.gif\" width=\"300px\" align=\"center\"\u003e\n\n\nThe example below shows how to implement resizing animation with the `animateTransformation` extension of the `OrbitalScope`.\nThe `rememberContentWithOrbitalScope` allows you to create custom animations such as `animateTransformation` on the `OrbitalScope`.\nYou can apply the `animateTransformation` animation to specific Composables and customize its `AnimationSpec` as seen the below:\n\n```kotlin\n  val transformationSpec = SpringSpec\u003cIntSize\u003e(\n    dampingRatio = Spring.DampingRatioMediumBouncy,\n    stiffness = 200f\n  )\n\n  var isTransformed by rememberSaveable { mutableStateOf(false) }\n  val poster = rememberContentWithOrbitalScope {\n    GlideImage(\n      modifier = if (isTransformed) {\n        Modifier.size(300.dp, 620.dp)\n      } else {\n        Modifier.size(100.dp, 220.dp)\n      }.animateTransformation(this, transformationSpec),\n      imageModel = ItemUtils.urls[0],\n      contentScale = ContentScale.Fit\n    )\n  }\n\n  Orbital(\n    modifier = Modifier\n      .clickable { isTransformed = !isTransformed }\n  ) {\n    Column(\n      Modifier.fillMaxSize(),\n      horizontalAlignment = Alignment.CenterHorizontally,\n      verticalArrangement = Arrangement.Center\n    ) {\n      poster()\n    }\n  }\n```\n\n### Movement\n\n\u003cimg src=\"previews/preview2.gif\" width=\"300px\" align=\"center\"\u003e\n\nThe example below shows how to implement movement animation with the `animateMovement` extension of the `OrbitalScope`.\nThe `rememberContentWithOrbitalScope` allows you to create custom animations such as `animateMovement` on the `OrbitalScope`.\nYou can apply the `animateMovement` animation to specific Composables and customize its `AnimationSpec` as seen the below:\n\n```kotlin\n  val movementSpec = SpringSpec\u003cIntOffset\u003e(\n    dampingRatio = Spring.DampingRatioMediumBouncy,\n    stiffness = 200f\n  )\n  \n  var isTransformed by rememberSaveable { mutableStateOf(false) }\n  val poster = rememberContentWithOrbitalScope {\n    GlideImage(\n      modifier = if (isTransformed) {\n        Modifier.size(360.dp, 620.dp)\n      } else {\n        Modifier.size(130.dp, 220.dp)\n      }.animateMovement(this, movementSpec),\n      imageModel = ItemUtils.urls[3],\n      contentScale = ContentScale.Fit\n    )\n  }\n\n  Orbital(\n    modifier = Modifier\n      .clickable { isTransformed = !isTransformed }\n  ) {\n    if (isTransformed) {\n      Column(\n        Modifier.fillMaxSize(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center\n      ) {\n        poster()\n      }\n    } else {\n      Column(\n        Modifier\n          .fillMaxSize()\n          .padding(20.dp),\n        horizontalAlignment = Alignment.End,\n        verticalArrangement = Arrangement.Bottom\n      ) {\n        poster()\n      }\n    }\n  }\n```\n\n### Shared Element Transition\n\n\u003cimg src=\"previews/preview3.gif\" width=\"300px\" align=\"center\"\u003e\n\nThe example below shows how to implement shared element transition with the `animateSharedElementTransition` extension of the `OrbitalScope`.\nThe `rememberContentWithOrbitalScope` allows you to create custom animations such as `animateSharedElementTransition` on the `OrbitalScope`.\nYou can apply the `animateSharedElementTransition` animation to specific Composables and customize its `AnimationSpec`.\nAlso, you can set the different `AnimationSpec`s for the movement and transformation as seen the below:\n\n```kotlin\n@Composable\nprivate fun OrbitalSharedElementTransitionExample() {\n  var isTransformed by rememberSaveable { mutableStateOf(false) }\n  val item = MockUtils.getMockPosters()[3]\n  val poster = rememberContentWithOrbitalScope {\n    GlideImage(\n      modifier = if (isTransformed) {\n        Modifier.fillMaxSize()\n      } else {\n        Modifier.size(130.dp, 220.dp)\n      }.animateSharedElementTransition(\n        this,\n        SpringSpec(stiffness = 500f),\n        SpringSpec(stiffness = 500f)\n      ),\n      imageModel = item.poster,\n      contentScale = ContentScale.Fit\n    )\n  }\n\n  Orbital(\n    modifier = Modifier\n      .clickable { isTransformed = !isTransformed }\n  ) {\n    if (isTransformed) {\n      PosterDetails(\n        poster = item,\n        sharedElementContent = { poster() },\n        pressOnBack = {}\n      )\n    } else {\n      Column(\n        Modifier\n          .fillMaxSize()\n          .padding(20.dp),\n        horizontalAlignment = Alignment.End,\n        verticalArrangement = Arrangement.Bottom\n      ) {\n        poster()\n      }\n    }\n  }\n}\n```\n\n\u003e **Note**: LookaheadLayout is a very experimental API, so measuring complex Composables might throw exceptions.\n\n### Shared Element Transition with Multiple Items\n\nThe example below shows how to implement shared element transition with multipe items. The basic concept of the usage is the same as the **Shared Element Transition** example.\n\n\u003cimg src=\"previews/preview0.gif\" width=\"300px\" align=\"center\"\u003e\n\n```kotlin\n  var isTransformed by rememberSaveable { mutableStateOf(false) }\n  val items = rememberContentWithOrbitalScope {\n    ItemUtils.urls.forEach { url -\u003e\n      GlideImage(\n        modifier = if (isTransformed) {\n          Modifier.size(140.dp, 180.dp)\n        } else {\n          Modifier.size(100.dp, 220.dp)\n        }\n          .animateSharedElementTransition(movementSpec, transformationSpec)\n          .padding(8.dp),\n        imageModel = url,\n        contentScale = ContentScale.Fit\n      )\n    }\n  }\n\n  Orbital(\n    modifier = Modifier\n      .fillMaxSize()\n      .clickable { isTransformed = !isTransformed },\n    isTransformed = isTransformed,\n    onStartContent = {\n      Column(\n        Modifier.fillMaxSize(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center\n      ) {\n        items()\n      }\n    },\n    onTransformedContent = {\n      Row(\n        verticalAlignment = Alignment.CenterVertically\n      ) { items() }\n    }\n  )\n```\n\n### Shared Element Transition With LazyList\n\n\u003cimg src=\"previews/preview4.gif\" width=\"300px\" align=\"center\"\u003e\n\nThe provided code example illustrates the implementation of shared element transformation (container transform) with a lazy list, such as `LazyColumn` and `LazyRow`. The `OrbitalScope` function initiates a scope in which all layout scopes will be measured and pre-calculated for size and position across all child layouts.   \n\n```kotlin\n@Composable\nfun OrbitalLazyColumnSample() {\n  val mocks = MockUtils.getMockPosters()\n\n  Orbital {\n    LazyColumn {\n      items(mocks, key = { it.name }) { poster -\u003e\n        var expanded by rememberSaveable { mutableStateOf(false) }\n        AnimatedVisibility(\n          remember { MutableTransitionState(false) }\n            .apply { targetState = true },\n          enter = fadeIn(),\n        ) {\n          Orbital(modifier = Modifier\n            .fillMaxWidth()\n            .clickable {\n              expanded = !expanded\n            }\n            .background(color = poster.color, shape = RoundedCornerShape(10.dp))) {\n            val title = rememberMovableContentOf {\n              Column(\n                modifier = Modifier\n                  .padding(10.dp)\n                  .animateBounds(Modifier),\n              ) {\n                Text(\n                  text = poster.name,\n                  fontSize = 18.sp,\n                  color = Color.Black,\n                  fontWeight = FontWeight.Bold,\n                )\n\n                Text(\n                  text = poster.description,\n                  color = Color.Gray,\n                  fontSize = 12.sp,\n                  maxLines = 3,\n                  overflow = TextOverflow.Ellipsis,\n                  fontWeight = FontWeight.Bold,\n                )\n              }\n            }\n            val image = rememberMovableContentOf {\n              GlideImage(\n                imageModel = { poster.poster },\n                component = rememberImageComponent {\n                  +CrossfadePlugin()\n                },\n                modifier = Modifier\n                  .padding(10.dp)\n                  .animateBounds(\n                    if (expanded) {\n                      Modifier.fillMaxWidth()\n                    } else {\n                      Modifier.size(80.dp)\n                    },\n                    spring(stiffness = Spring.StiffnessLow),\n                  )\n                  .clip(RoundedCornerShape(5.dp)),\n                imageOptions = ImageOptions(requestSize = IntSize(600, 600)),\n              )\n            }\n\n            if (expanded) {\n              Column {\n                image()\n                title()\n              }\n            } else {\n              Row {\n                image()\n                title()\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nYou should bear in mind these three aspects:\n\n- **Orbital**: The `Orbital` function starts a scope, which measures and pre-calculates the layout size and position for all child layouts. Fundamentally, it initiates a reusable Compose node for the given content, which makes all magic things under the hood. You can utilize the `Orbital` in nested ways based on your specific scenarios, as illustrated in the code above.\n- **rememberMovableContentOf**: Utilize this function to remember a movable Composable function, allowing it to be relocated within the Compose tree. All items intended for transformation should be pre-defined using this function, enabling you to display different content based on various situations. All content defined using `rememberMovableContentOf` must be employed within the `Orbital`. \n- **animateBounds**: This serves as the delegate of the `Modifier` to compute distinct layout sizes based on various situations. It should be used in conjunction with the `rememberMovableContentOf` function.\n\n### Transition Between Composables\n\n\u003cimg src=\"previews/preview5.gif\" width=\"300px\" align=\"center\"\u003e\n\nYou can implement transitions between composable functions, as you've learned in the previous functions. The sample code below demonstrates how you can implement a shared element transition between screens A and B by defining shared content:\n\n```kotlin\nenum class Screen {\n  A, B;\n}\n\n@Composable\nfun ScreenTransitionSample() {\n  Orbital {\n    var screen by rememberSaveable { mutableStateOf(Screen.A) }\n    val sizeAnim = spring\u003cIntSize\u003e(stiffness = Spring.StiffnessLow)\n    val positionAnim = spring\u003cIntOffset\u003e(stiffness = Spring.StiffnessLow)\n    val image = rememberMovableContentOf {\n      GlideImage(\n        imageModel = { MockUtils.getMockPoster().poster },\n        component = rememberImageComponent {\n          +CrossfadePlugin()\n        },\n        modifier = Modifier\n          .padding(10.dp)\n          .animateBounds(\n            modifier = if (screen == Screen.A) {\n              Modifier.size(80.dp)\n            } else {\n              Modifier.fillMaxWidth()\n            },\n            sizeAnimationSpec = sizeAnim,\n            positionAnimationSpec = positionAnim,\n          )\n          .clip(RoundedCornerShape(12.dp)),\n        imageOptions = ImageOptions(requestSize = IntSize(600, 600)),\n      )\n    }\n\n    val title = rememberMovableContentOf {\n      Column(\n        modifier = Modifier\n          .padding(10.dp)\n          .animateBounds(\n            modifier = Modifier,\n            sizeAnimationSpec = sizeAnim,\n            positionAnimationSpec = positionAnim\n          ),\n      ) {\n        Text(\n          text = MockUtils.getMockPoster().name,\n          fontSize = 18.sp,\n          color = Color.Black,\n          fontWeight = FontWeight.Bold,\n        )\n\n        Text(\n          text = MockUtils.getMockPoster().description,\n          color = Color.Gray,\n          fontSize = 12.sp,\n          maxLines = 3,\n          overflow = TextOverflow.Ellipsis,\n          fontWeight = FontWeight.Bold,\n        )\n      }\n    }\n\n    if (screen == Screen.A) {\n      ScreenA(\n        sharedContent = {\n          image()\n          title()\n        }) {\n        screen = Screen.B\n      }\n    } else {\n      ScreenB(\n        sharedContent = {\n          image()\n          title()\n        }) {\n        screen = Screen.A\n      }\n    }\n  }\n}\n\n@Composable\nprivate fun ScreenA(\n  sharedContent: @Composable () -\u003e Unit,\n  navigateToScreenB: () -\u003e Unit\n) {\n  Orbital {\n    Row(modifier = Modifier\n      .background(color = Color(0xFFffd7d7))\n      .fillMaxSize()\n      .clickable {\n        navigateToScreenB.invoke()\n      }) {\n      sharedContent()\n    }\n  }\n}\n\n@Composable\nprivate fun ScreenB(\n  sharedContent: @Composable () -\u003e Unit,\n  navigateToScreenA: () -\u003e Unit\n) {\n  Orbital {\n    Column(modifier = Modifier\n      .background(color = Color(0xFFe3ffd9))\n      .fillMaxSize()\n      .clickable {\n        navigateToScreenA()\n      }) {\n      sharedContent()\n    }\n  }\n}\n```\n\nUnfortunately, you can't achieve this transition with [Jetpack Compose Navigation](https://developer.android.com/jetpack/compose/navigation) yet.\n\n## Find this repository useful? :heart:\nSupport it by joining __[stargazers](https://github.com/skydoves/Orbital/stargazers)__ for this repository. :star: \u003cbr\u003e\nAlso, __[follow me](https://github.com/skydoves)__ on GitHub for my next creations! 🤩\n\n# License\n```xml\nDesigned and developed by 2022 skydoves (Jaewoong Eum)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","funding_links":["https://github.com/sponsors/skydoves","https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"],"categories":["Libraries","Uncategorized"],"sub_categories":["🍎 Compose UI","Uncategorized"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Forbital","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskydoves%2Forbital","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Forbital/lists"}