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.
MoriRouter
https://github.com/chuross/mori-router
A library that supports the development of screen transitions using ** Fragment ** using automatic generation using annotations.
--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
Download
--Add JitPack to repositories of build.gradle
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
implementation 'com.github.chuross.mori-router:annotation:x.x.x'
annotationProcessor 'com.github.chuross.mori-router:compiler:x.x.x' // or kpt
}
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.
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() {
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:
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.
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 **
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:
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)
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