Continue Animation Into Sub Activity

Viewed 136
4 Answers

This is not possible. Even if reflection is used to access and set current frame. It will not work in the case above. Even if the AnimationDrawable was created programmatically and it was shared between both of the activities. It will not work either. Some developers suggested using a background on main activity and suggested that it would be shared on child activities. That is not how activities work. Even if there was a way to achieve the sharing of animation, the app would have out of memory issues.

TL;DR It is not possible.

I think what you want is to have a single activity and add a fragment for the 1st screen. Then when you want to navigate away (what you do now with startActivity) you should instead replace the fragment.

Something like this:


class MainActivity: Activity()  {


  onCreate(...) {
           
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

      add1stFragment()
  }

  fun add1stFragment(){
     supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragmentContainer, LandingFragment())
            .commitAllowingStateLoss()

  }

}

class LandingFragment : Fragment() {

   override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
      // inflate layout with the contents of 1st screen
   }

   fun add2ndFragment(){
     supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragmentContainer, AlternativeFragment())
            .commitAllowingStateLoss()

  }
}


class AlternativeFragment : Fragment() {
  fun  onCreateView(...){...
}
}

res/layout/activity_main:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainActivity">

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

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:elevation="4dp">

            <com.google.android.material.appbar.MaterialToolbar
                android:layout_width="match_parent"
                android:layout_height="wrap_content"

                app:title="TITLE" />

        </com.google.android.material.appbar.AppBarLayout>

        <FrameLayout
            android:id="@+id/fragmentContainer"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1">


        </FrameLayout>
    </LinearLayout>
</layout>

Now the toolbar and background are the same for both screens, because they belong to the activity. While the screen contents belong to the different fragments.

The only downside is that if you rotate the screen the animation would start again. If you want that to continue from where it was you can use a couple things, but they will depend on how exactly you're doing the animation.

Unfortunately I tried giving a quick look at AnimationDrawable and I don't see how to make it jump to a specific point in its animation, nor how to get the current progress. Otherwise it would looks something like this (in case you used a different animation method):

class MainActivity: Activity {

 private var animDrawable : AnimatedDrawable? =null 
onCreate(savedInstanceState:Bundle?) {

       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       animDrawable = root_layout.background as? AnimationDrawable
      
      fromIndex = getIndex(savedInstanceState)
     

      add1stFragment()
  }


   override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("POSITION",animDrawable.getPosition)
    }

     

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        startAnimation(fromIndex) // extract the current position and start playing from there
    }
 

Make sure to check the background of your fragments is transparent. And optionally, set AppBarLayout opacity with android:alpha="0.5". It's questionable taste but it will allow the background to shine through.

Edit: so there is a very roundabout way to get the current position and resume from a given position, as shown in https://stackoverflow.com/a/13613613/2754856

Also, just figured out that you shouldn't call AnimationDrawable::start from onCreate: file:///home/fabiomint/bin/astudio/android-sdk/docs/reference/android/graphics/drawable/AnimationDrawable.html#start() and made some edits to match that.

Edit 2 for handling multiple activities:

It also works for screen rotation. It's not as good as using the same animation on the same activity, because the act of pausing/resuming animation on a different activity has a side effect of making a copy of the animation with the different frames with switched orders.

As I mentioned above, having a single activity is my 1st suggestion. The 2nd one is using another animation technique for this, AnimationDrawable is a pain to work with. ObjectAnimator is more friendly and it's possible to build gradient animation with it, but it can't animate shapes and other things that I believe only AnimationDrawable supports, like animated vectors.

make a singleton or global variable:


var activityBackground = PausableAnimationDrawable()
 

then on each of your activities do


    override fun onResume() {
        super.onResume()

        findViewById<View>(R.id.root)
            ?.let {

                it.background = activityBackground.getAnimation(this)
            }
        activityBackground.resume()
    }

    override fun onPause() {
        super.onPause()
        activityBackground.pause()
    }

class PausableAnimationDrawable {

    private var activeAnimation: AnimationDrawable? = null
    private var animation: AnimationDrawable? = null

    private var indexFrame: Int = 0

    fun pause() {

        indexFrame = getCurrentIndex()
    }

    fun resume() {
        activeAnimation?.start()
        activeAnimation?.setVisible(true, false)
    }

    fun getAnimation(context: Context): AnimationDrawable? {

        if (animation == null) {
            animation = getNewAnimation(context)
        }

        activeAnimation = getReindexedAnimation(animation, indexFrame)

        return activeAnimation
    }

    private fun getReindexedAnimation(
        animation: AnimationDrawable?,
        indexFrame: Int
    ): AnimationDrawable? {

        val animation = animation ?: return null

        return AnimationDrawable()
            .also { newDrawable ->
                for (i in indexFrame..animation.numberOfFrames) {
                    animation.getFrame(i)
                        ?.let {
                            newDrawable.addFrame(it, animation.getDuration(i))
                        }
                }
                for (i in 0 until indexFrame) {
                    animation.getFrame(i)
                        ?.let {
                            newDrawable.addFrame(it, animation.getDuration(i))
                        }
                }
            }

    }

    private fun getNewAnimation(context: Context): AnimationDrawable? {
        return context.applicationContext.getDrawable(R.drawable.gradient)
            ?.let { it as? AnimationDrawable }
            ?.also { it.setVisible(false, false) }
    }


    private fun getCurrentIndex(): Int {

        for (i in 0..(activeAnimation?.numberOfFrames ?: 0)) {
            if (activeAnimation?.getFrame(i) == activeAnimation?.current) {
                Timber.d("*** Frame %s match", i)
                return (i + indexFrame) % (activeAnimation?.numberOfFrames ?: 0)

            } else {
                Timber.d("*** Frame %s no match", i)
            }
        }

        return 0
    }


}

your best shot is to have your main activity with the background then make your next activity's background transparent such that on launching your next activity,you still see the background of your previous main activity

so you make the next activity transparent by setting the below theme on to your activity

android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"

And then set a transparent background to your next activity like so

 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
   android:background = "any tranparent image/color"  >
 </RelativeLayout>

I'd try passing to the child activity the important values of the animation (i.e angle, milliseconds elapsed, etc), and then start the new animation on the child activity with those values. That way your animation will seem like it's resuming.

Related