Skip to content

Commit

Permalink
add predictive back androidx transition animation (#109)
Browse files Browse the repository at this point in the history
* add predictive back androidx transition animation

* apply only a ChangeBounds when back gesture is cancelled for the predictive back androidx transitions sample

* add lifecycle owner to OnBackPressedCallback

* Predictive Back : AndroidX Transitions. Ack feedback.
  • Loading branch information
ashnohe authored Dec 11, 2023
1 parent 3ed3a52 commit a30a043
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 6 deletions.
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ androidx-activity = "androidx.activity:activity:1.8.1"
androidx-core = "androidx.core:core-ktx:1.12.0"
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
# Fragment 1.7.0 alpha is for predictive back to work with Fragment transitions
# Fragment 1.7.0 alpha and Transition 1.5.0 alpha are required for predictive back to work with Fragments and transitions
androidx-fragment = "androidx.fragment:fragment-ktx:1.7.0-alpha07"
androidx-transition = "androidx.transition:transition-ktx:1.5.0-alpha05"
androidx-activity-compose = "androidx.activity:activity-compose:1.8.1"
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
Expand Down
87 changes: 85 additions & 2 deletions samples/user-interface/predictiveback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Shows different types of predictive back animations, including:
In general, rely on the default cross-activity animation; however, if required use
`overrideActivityTransition` instead of `overridePendingTransition`. Although animation resources are
expected for `overrideActivityTransition`, we strongly recommend to stop using animation and to
instead use animator and androidx transitions for most use cases.
instead use animator and androidx transitions for most use cases. For more details see the
[developer documentation](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -88,4 +89,86 @@ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
predictiveBackCallback
)
}
```
```

## Custom AndroidX Transition
For more details see the
[developer documentation](https://developer.android.com/about/versions/14/features/predictive-back#androidx-transitions).

```kotlin
class MyFragment : Fragment() {

val transitionSet = TransitionSet().apply {
addTransition(Fade(Fade.MODE_OUT))
addTransition(ChangeBounds())
addTransition(Fade(Fade.MODE_IN))
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val callback = object : OnBackPressedCallback(enabled = false) {

var controller: TransitionSeekController? = null

override fun handleOnBackStarted(backEvent: BackEvent) {
// Create the transition
controller = TransitionManager.controlDelayedTransition(
// textContainer is a FrameLayout containing the shortText and longText TextViews
binding.textContainer,
transitionSet
)
changeTextVisibility(ShowText.SHORT)
}

override fun handleOnBackProgressed(backEvent: BackEvent) {
// Play the transition as the user swipes back
if (controller?.isReady == true) {
controller?.currentFraction = backEvent.progress
}
}

override fun handleOnBackPressed() {
// Finish playing the transition when the user commits back
controller?.animateToEnd()
this.isEnabled = false
}

override fun handleOnBackCancelled() {
// If the user cancels the back gesture, reset the state
transition(ShowText.LONG)
}
}

binding.shortText.setOnClickListener {
transition(ShowText.LONG)
callback.isEnabled = true
}

this.requireActivity().onBackPressedDispatcher.addCallback(callback)
}

private fun transition(showText: ShowText) {
TransitionManager.beginDelayedTransition(
binding.textContainer,
transitionSet
)
changeTextVisibility(showText)
}

enum class ShowText { SHORT, LONG }
private fun changeTextVisibility(showText: ShowText) {
when (showText) {
ShowText.SHORT -> {
binding.shortText.isVisible = true
binding.longText.isVisible = false
}
ShowText.LONG -> {
binding.shortText.isVisible = false
binding.longText.isVisible = true
}
}
}
}
```

2 changes: 2 additions & 0 deletions samples/user-interface/predictiveback/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ dependencies {
implementation(libs.mdc)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.androidx.fragment)
implementation(libs.androidx.transition)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
package com.example.platform.ui.predictiveback

enum class PBAnimation {
SYS_UI, BACK_TO_HOME, CROSS_ACTIVITY, CUSTOM_CROSS_ACTIVITY, CROSS_FRAGMENT, PROGRESS_API
SYS_UI,
BACK_TO_HOME,
CROSS_ACTIVITY,
CUSTOM_CROSS_ACTIVITY,
CROSS_FRAGMENT,
PROGRESS_API,
TRANSITION
}
data class PBAnimationText(val title: String, val description: String)

Expand Down Expand Up @@ -45,5 +51,9 @@ val animations = mapOf<PBAnimation, PBAnimationText>(
PBAnimation.PROGRESS_API to PBAnimationText(
"Progress API",
"Click to see an animation created with the Predictive Back Progress API."
),
PBAnimation.TRANSITION to PBAnimationText(
"Transition",
"Click to see an animation created with AndroidX Transitions and the Predictive Back Progress API."
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class PBListFragment : Fragment() {
binding.progressApiCard.setOnClickListener {
findNavController().navigate(R.id.show_PBProgressAPI)
}
binding.transitionsCard.setOnClickListener {
findNavController().navigate(R.id.show_PBTransition)
}
}

override fun onDestroyView() {
Expand All @@ -71,5 +74,7 @@ class PBListFragment : Fragment() {
binding.crossFragmentDescription.text = animations[PBAnimation.CROSS_FRAGMENT]?.description ?: ""
binding.progressApiTitle.text = animations[PBAnimation.PROGRESS_API]?.title ?: ""
binding.progressApiDescription.text = animations[PBAnimation.PROGRESS_API]?.description ?: ""
binding.transitionsTitle.text = animations[PBAnimation.TRANSITION]?.title ?: ""
binding.transitionsDescription.text = animations[PBAnimation.TRANSITION]?.description ?: ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.platform.ui.predictiveback

import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback
import androidx.core.view.isVisible
import androidx.transition.ChangeBounds
import androidx.transition.Fade
import androidx.transition.TransitionManager
import androidx.transition.TransitionSeekController
import androidx.transition.TransitionSet
import com.example.platform.ui.predictiveback.databinding.FragmentTransitionBinding

class PBTransition : Fragment() {
private var _binding: FragmentTransitionBinding? = null
private val binding get() = _binding!!

val transitionSet = TransitionSet().apply {
addTransition(Fade(Fade.MODE_OUT))
addTransition(ChangeBounds())
addTransition(Fade(Fade.MODE_IN))
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentTransitionBinding
.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

changeTextVisibility(ShowText.SHORT)

val callback = object : OnBackPressedCallback(enabled = false) {

var controller: TransitionSeekController? = null

override fun handleOnBackStarted(backEvent: BackEventCompat) {
controller = TransitionManager.controlDelayedTransition(
binding.textContainer,
transitionSet
)
changeTextVisibility(ShowText.SHORT)
}

override fun handleOnBackProgressed(backEvent: BackEventCompat) {
if (controller?.isReady == true) {
controller?.currentFraction = backEvent.progress
}
}

override fun handleOnBackPressed() {
controller?.animateToEnd()
this.isEnabled = false
}

override fun handleOnBackCancelled() {
// If the user cancels the back gesture, reset the state
TransitionManager.beginDelayedTransition(
binding.textContainer,
ChangeBounds()
)
changeTextVisibility(ShowText.LONG)
}
}

binding.shortText.setOnClickListener {
TransitionManager.beginDelayedTransition(binding.textContainer, transitionSet)
changeTextVisibility(ShowText.LONG)
callback.isEnabled = true
}

this.requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)

binding.longText.movementMethod = ScrollingMovementMethod()
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

enum class ShowText { SHORT, LONG }
private fun changeTextVisibility(showText: ShowText) {
when (showText) {
ShowText.SHORT -> {
binding.shortText.isVisible = true
binding.longText.isVisible = false
binding.body.text = "Click on the box."
}
ShowText.LONG -> {
binding.shortText.isVisible = false
binding.longText.isVisible = true
binding.body.text = "Swipe back slowly to see the Predictive Back AndroidX Transition."
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".PBListFragment">

<LinearLayout
Expand Down Expand Up @@ -239,6 +239,41 @@

</com.google.android.material.card.MaterialCardView>

<com.google.android.material.card.MaterialCardView
android:id="@+id/transitions_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
style="?attr/materialCardViewFilledStyle">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/transitions_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textSize="16sp"/>

<TextView
android:id="@+id/transitions_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />

</LinearLayout>

</com.google.android.material.card.MaterialCardView>

</LinearLayout>

</ScrollView>
Loading

0 comments on commit a30a043

Please sign in to comment.