A simple and customizable Android fragments navigator with support "swipe to dismiss" gestures and saving a stack of fragments when changing the screen orientation
!!! Fragula is no longer supported. Try Fragula 2 by @massivemadness
- A project configured with the AndroidX
- SDK 21 and and higher
(The app requires vk.com registration)
Download via Gradle:
Add this to the project build.gradle
file:
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
And then add the dependency to the module build.gradle
file:
implementation 'com.github.shikleev:fragula:latest_version'
All you need to do is create a Navigator in the xml of your activity:
<?xml version="1.0" encoding="utf-8"?>
<com.fragula.Navigator
android:id="@+id/navigator"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
tools:context=".MainActivity"/>
And add a first fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
navigator.addFragment(BlankFragment())
}
}
You can pass arguments in the function parameters:
navigator.addFragment(BlankFragment()) {
"ARG_KEY_1" to "Add fragment arg"
"ARG_KEY_2" to 12345
}
Or using kotlin-extensions:
addFragment<BlankFragment> {
"ARG_KEY_1" to "Add fragment arg"
"ARG_KEY_2" to 12345
}
And get them in an opened fragment:
class BlankFragment : Fragment() {
private var param1: String? = null
private var param2: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString("ARG_KEY_1")
param2 = it.getInt("ARG_KEY_2")
}
}
}
navigator.replaceFragment(BlankFragment())
With kotlin-extensions
replaceFragment<BlankFragment>()
Or replace by position with arguments
replaceFragment<BlankFragment>(
position = position,
bundleBuilder = {
"ARG_KEY_1" to "Replace fragment arg"
}
)
Intercept the touch event while the fragment transaction is in progress. In your Activity:
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return if (navigator.isBlockTouchEvent)
true
else
super.dispatchTouchEvent(ev)
}
Intercept onBackPressed:
override fun onBackPressed() {
if (navigator.fragmentCount > 1) {
navigator.goToPreviousFragmentAndRemoveLast()
} else {
super.onBackPressed()
}
}
The fragment opening transaction is executed synchronously and starts after onViewCreated finishes in the fragment being torn off. If you have asynchronous code that displays the results in a fragment, this may affect the arbitrariness of the fragment's opening animation. For such cases, you need to use an interface in your fragment that will report that the fragment transaction is complete.
class BlankFragment : Fragment(), OnFragmentNavigatorListener {
override fun onOpenedFragment() {
//This is called when the animation for opening a new fragment is complete
}
override fun onReturnedFragment() {
//This will be called when you return to this fragment from the previous one
}
}
You can also use other callbacks:
navigator.onPageScrolled = {position, positionOffset, positionOffsetPixels -> }
navigator.onNotifyDataChanged = {fragmentCount ->
// Called after a new fragment is added to the stack or when the fragment is removed from the stack
}
navigator.onPageScrollStateChanged = {state ->
// SCROLL_STATE_IDLE, SCROLL_STATE_SETTLING, SCROLL_STATE_DRAGGING
}
You can get a stack of fragments by accessing the Navigator:
val fragments: List<Fragment> = navigator.fragments
Or using the Fragment Manager to search for a fragment by tag (The Navigator assigns a tag to each fragment depending on the position in the Navigator):
val fragment = supportFragmentManager.findFragmentByTag("0")
if (fragment != null && fragment is MainFragment) {
mainFragment = fragment
}
You can implement your own interface in the target fragment and call its callbacks in the current fragment:
interface ExampleCallback {
fun onReceive()
}
class TargetFragment : Fragment(), ExampleCallback {
override fun onReceive() {
// do something
}
}
Then, on the current fragment, call the getCallback function and call the desired function:
class CurrentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getCallback<ExampleCallback>.onReceive()
}
}
Navigator based on ViewPager, so you can use your own PageTransformer:
class CustomPageTransformer: FragmentNavigator.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.apply {
cameraDistance = width * 100f
pivotY = height / 2f
when {
position > 0 && position < 0.99 -> {
alpha = 1f
rotationY = position * 150
pivotX = width / 2f
}
position > -1 && position <= 0 -> {
alpha = 1.0f - abs(position * 0.7f)
translationX = -width * position
rotationY = position * 30
pivotX = width.toFloat()
}
}
}
}
}
And in your activity:
navigator.setPageTransformer(false, CustomPageTransformer())
navigator.setDurationFactor(1.8f)
The Navigator cannot delete a fragment in the middle or beginning of the fragment stack. This leads to the violation of the order of the fragments and unexpected errors. Use onBackPressed to delete the last fragment or
navigator.goToPreviousFragmentAndRemoveLast()
If you want to remove the last few fragments, use:
navigator.goToPosition(position)
This will also remove all closed fragments from the stack
Gestures conflict when using Motion Layout
If there is a conflict of gestures you can disable the swipe gestures in the Navigator and then turn them back on
MotionLayout.setOnTouchListener { view, motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
navigator.setAllowedSwipeDirection(SwipeDirection.NONE)
}
MotionEvent.ACTION_UP -> {
navigator.setAllowedSwipeDirection(SwipeDirection.RIGHT)
}
MotionEvent.ACTION_CANCEL -> {
navigator.setAllowedSwipeDirection(SwipeDirection.RIGHT)
}
}
return@setOnTouchListener false
}
Also, you can take a look at the sample project for more information.