[JAVA] Created a multifunctional routing library for Android that also supports Shared Element --MoriRouter

Overview

It's been a few years since Material Design was announced and Shared Element appeared. As far as I can see the apps in the store, I don't see many of them installed.

I wanted to put it in the development I'm participating in several times, but for some reason I came here without putting it in.

This time, as I was developing the app, I was getting more and more interested in creating a routing library that I was satisfied with and using Annotation Processor, so I decided to create what routing I think is good. did.

Even though I followed Github, I couldn't find a library that made Shared Element look good, so I decided to focus on that as well.

Deliverables

MoriRouter ezgif-3-5ae226e28e.gif

https://github.com/chuross/mori-router

A library that supports the development of screen transitions using ** Fragment ** using automatic generation using annotations.

Features

--Automatically generate code for screen transition via annotation --Automatically generate Builder for Fragment that configures the screen --Automatically generate code to transition from URL to a specific screen --Just put the value as a placeholder for the part you want to handle as a parameter --Automatically generate methods for SharedElement --Transition from list and difficult implementation such as using ViewPager can be done relatively easily --Since animation processing is absorbed by annotation and automatic generation, it is difficult to enter the description about animation in the View code. --Generated code with android support annotations

How to use

Download --Add JitPack to repositories of build.gradle

repositories {
    maven { url "https://jitpack.io" }
}

--Add to dependencies

dependencies {
    implementation 'com.github.chuross.mori-router:annotation:x.x.x'
    annotationProcessor 'com.github.chuross.mori-router:compiler:x.x.x' // or kpt
}

Basic

Just add the @RouterPath annotation to the Fragment you want to use as a screen transition.

The name of @ RouterPath and @Argument is the method name of the router.

@RouterPath(name = "main")
class MainScreenFragment : Fragment() {

    @Argument
    lateinit var param1: String

    @Argument(name = "hoge", required = false)
    var param3: ArrayList<String> = arrayListOf()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        MoriBinder.bind(this) // @You can put a value in each field of Argument
    }
    ....
}

When you build this, a class called MoriRouter is automatically generated, and a method to start this screen is added in it.

After that, generate and use a Router around the base Activity.

val transitionFactory = DefaultTransitionFactory { Fade() } // android.support.transition or android.transition animation

val options = MoriRouterOptions.Builder(R.id.container) //FrameLayout id for drawing screen transitions
                .setEnterTransitionFactory(transitionFactory) //Common animation when the screen starts
                .setExitTransitionFactory(transitionFactory) //Common animation at the end of the screen
                .build()

val router = MoriRouter(supportFragmentManager, options)

//A method for screen transition is automatically generated
router.main("required1", 1000) // main(String param1, Integer param2)
    .hoge(arrayListOf("fuga")) // optional value
    .launch() //Launch MainScreenFragment

router.pop() //Call this when returning to the previous screen

The content defined in the annotation earlier is generated as the method name as it is.

After that, if necessary, pass the parameters required for screen transition and call launch at the end to execute screen transition in Layout of R.id.container. Convenient: smiley :: v:

Fragment Builder By using @RouterPath, it became convenient to generate screen transitions. However, in reality, the screen may also be composed of Fragments, so I want to enjoy this too.

In such a case, you can automatically generate the Builder class by adding @WithArguments instead of @RouterPath.

@WithArguments
class HogeFragment : Fragment() {

    @Argument
    lateinit var hogeName: String
    ....
}

If you do this, the HogeFragmentBuilder class will be automatically generated, so you can handle it as follows.

val fragment: HogeFragment = HogeFragmentBuilder(hogeName).build()

Convenient: smiley :: v:

There is no problem even if you use FragmentArgs or ʻAutoBundle` because there is a merit that you can unify the description here.

Transition animation override

The Enter / Exit Transition passed at the time of initialization of MoriRouter is common to all screens and is used at the time of transition.

However, there are cases where you want to specify a dedicated animation on a specific screen.

In such a case, it can be defined by specifying ʻoverrideEnterTransitionFactory and ʻoverrideExitTransitionFactory in @RouterPath.

@RouterPath(
    name = "main",
    overrideEnterTransitionFactory = MainScreenTransitionFactory::class,
    overrideExitTransitionFactory = MainScreenTransitionFactory::class
)
class MainScreenFragment : Fragment() {

Transition from URL to a specific screen

By specifying the url format in @RouterPath, you can launch a specific screen via the URL.

Since multiple target formats can be specified, it can be used properly with a custom schema or http / https.

@RouterPath(
  name = "second",
  uris = [
    "example://hoge/{hoge_id}/{fuga}",
    "https://example.com/hoge/{hoge_id}/{fuga}"
  ]
)
class SecondScreenFragment : Fragment() {

    @UriArgument(name = "hoge_id")
    var hogeId: Int

    @UriArgument
    var fuga: String

    // @When using UriArgument@Argument(required=true)Cannot be used
    @Argument(required = false)
    var piyo: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        MoriBinder.bind(this)
    }
}

By using {} in the format, you can get the value corresponding to the field in the View class.

After that, there is a method called dispatch in MoriRouter, so if you pass Uri there, you will be able to transition the screen.

router.dispatch(Uri.parse("example://hoge/123/test")) // launch SecondScreenFragment (hogeId = 123, fuga=test)
router.dispatch(Uri.parse("https://example.com/hoge/123/test")) // launch SecondScreenFragment (hogeId = 123, fuga=test)

Convenient: smiley :: v:

Shared Element support

The implementation of Shared Element will animate nicely if you specify the same transitionName for the transition source and transition destination ... I had a time when I thought so.

If it is a simple pattern, it will still work, but in reality, there are many patterns that are not so easy.

In this library, I would like to introduce it because it is simplified as much as possible and devised to be easy to implement.

Basic

First, set the TransitionName to the transition source from XML or code. ** Make sure View has an ID **

<YourLayout
    android:id="@+id/your_id" <!-- must have view id -->
    android:transitionName="your_transition_name" />

--Code

ViewCompat.setTransitionName(yourView, "your_transition_name");

Next, define the transition destination class so that it receives SharedElement.

** Be sure to set the animation for Shared Element in sharedEnterTransitionFactory and sharedExitTransitionFactory **

After that, pass the ID of the View of the transition destination to the bindElement of MoriBinder and you're done. Make sure that the transition source and transition destination views have the same ID.

@RouterPath(
    name = "third",
    sharedEnterTransitionFactory = ThirdScreenSharedTransitionFactory::class,
    sharedExitTransitionFactory = ThirdScreenSharedTransitionFactory::class
)
class ThirdScreenFragment : Fragment() {
   ....
   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //Specify the same ID specified for the View you want to share element in the transition source
        //The transition source and transition destination views have the same ID.
        MoriBinder.bindElement(this, R.id.your_id)
   }
}

Once you have defined it so far, all you have to do is add the View you want to share element at the time of screen transition.

router.third()
       .addSharedElement(yourView) //Set the View you want to share element
       .launch()

After that, it will animate using the TransitionFactory that is specified nicely at the time of transition.

When using it with RecyclerView or ViewPager, it will be described in detail in the notes below. Note that ** TransitionName must be unique **

When the Shared Element changes dynamically

The transition destination may be a Shared Element to ViewPager, and another View may be returned as a Shared Element at the end of the screen.

In these cases, ʻaddSharedElement` cannot be used and must be manually mapped. However, this library also automatically generates a class that supports manual mapping, so it's an easy win.

First, set the same TransitionName for the transition source and transition destination views. Unlike before, in the case of manual mapping, it is necessary to know how to generate the TransitionName of the transition source as well.

I am like this I felt like getting a prefix from the transition source.

ViewCompat.setTransitionName(yourView, "your_transition_name");

After that, define manualSharedViewNames in @ RouterPath of the transition destination View. This is used as a name that connects the transition source and transition destination separately from the Transition Name. (A different name from TransitionName is good)

@RouterPath(
    name = "third",
    manualSharedViewNames = ["shared_view_image"],
    sharedEnterTransitionFactory = ThirdScreenSharedTransitionFactory::class,
    sharedExitTransitionFactory = ThirdScreenSharedTransitionFactory::class
)
class ThirdScreenFragment : Fragment() {
   ....

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       val callback = ThirdSharedElementCallBack()
                          .sharedViewImage({ /*Get the current Fragment from the ViewPager and return the View you want to shareElement in it*/ })

       setEnterSharedElementCallback(callback)
   }
}

After that, set setEnterSharedElementCallback to complete the transition destination.

ThirdSharedElementCallBack is an auto-generated code that simplifies manual mapping if you create a Callback via it.

Next, we will define the transition source.

@RouterPath(
    name = "second"
)
class SecondScreenFragment : Fragment() {
   ....

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       val callback = ThirdSharedElementCallBack()
                        .sharedViewImage({ /*Process to get View from RecyclerView*/ })

       setExitSharedElementCallback(callback)
   }

   ....

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

        // call manualSharedMapping
        router.third().manualSharedMapping(context).launch()
   }
}

Now set setExitSharedElementCallback. SharedElementCallback is the same as the previous one.

After that, when calling the transition process, you can call manualSharedMapping instead of ʻaddSharedElement`.

Convenient: smiley :: v:

Notes on Shared Element

Transition Name must be ** unique in principle ** (I'm addicted)

This of course also applies to RecyclerView and ViewPager, Even if you reuse the same View, if you reuse the Transition Name, it will not animate.

For example, in the case of RecyclerView, it is necessary to specify a different TransitionName for each position like transition_view_image_0 transition_view_image_1.

If you want to use the same Fragment with RecyclerView inside ViewPager, you need to divide it by ViewPager index + RecyclerView index like transition_view_image_1_1. (Mendo)

Afterword

I think there are still a lot of things to modify, but I think the SharedElement and routing code will be cleaner.

I plan to continue to maintain it, so I want to keep improving it. I think there is still a good way to do it, so maybe I'll try a different approach.

If you read the sample, you may understand something like "How do I implement this ?: thinking:".

https://github.com/chuross/mori-router/tree/master/app

Recommended Posts

Created a multifunctional routing library for Android that also supports Shared Element --MoriRouter
Created a library that makes it easy to handle Android Shared Prefences
LazyBLE Wrapper (created a library that makes Android BLE super simple) v0.14
I made a library for displaying tutorials on Android.
Find a Java library for Bayesian networks that might work