I have an app which is using the one-activity-multiple-fragments architecture using navigation component.
For each fragment, I have a slide in/out animation like so:
anim_in.xml:
<?xml version="1.0" encoding="utf-8"?>
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="100%p"
android:toXDelta="0%"
android:duration="#android:integer/config_shortAnimTime" />
anim_out.xml:
<?xml version="1.0" encoding="utf-8"?>
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0%p"
android:toXDelta="100%"
android:duration="#android:integer/config_shortAnimTime" />
nav_graph.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
android:id="#+id/nav_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.monettests.MainFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_main" >
<action
android:id="#+id/action_mainFragment_to_settingsFragment"
app:enterAnim="#anim/anim_in"
app:popEnterAnim="#anim/anim_in"
app:popExitAnim="#anim/anim_out"
app:destination="#id/settingsFragment" />
<action
android:id="#+id/action_mainFragment_to_feedbackFragment"
app:enterAnim="#anim/anim_in"
app:popEnterAnim="#anim/anim_in"
app:popExitAnim="#anim/anim_out"
app:destination="#id/feedbackFragment" />
</fragment>
<fragment
android:id="#+id/settingsFragment"
android:name="com.example.monettests.SettingsFragment"
android:label="Settings"
tools:layout="#layout/fragment_settings" />
<fragment
android:id="#+id/feedbackFragment"
android:name="com.example.monettests.FeedbackFragment"
android:label="Send Feedback"
tools:layout="#layout/fragment_feedback" />
</navigation>
Navigation code in MainFragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_main, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.menuMain_settingsItem -> {
findNavController().navigate(R.id.action_mainFragment_to_settingsFragment)
true
}
R.id.menuMain_feedbackItem -> {
findNavController().navigate(R.id.action_mainFragment_to_feedbackFragment)
true
}
else -> {
false
}
}
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
The problem with this is that it just animates the fragment itself but not the action bar.
What I have tried:
Manually animating the action bar in each fragment using listeners (unsuccessful)
Searching documentation for a way to do this (tried for hours but unsuccessful - shocked that there isn't a way to do this in the SDK)
I understand that this is desired fragment behavior, but I want to give the end user the illusion of animating between different pages and for that it's best if the action bar itself is part of the transition.
Is there any way to have a fragment transition that animates the fragment and the action bar, similar to how it's done with activities?
Related
I have a Fragment with bottom tabs named TabsFragment
class BottomTabsFragment : Fragment(R.layout.fragment_tabs) {
private val bottomNavigationView: BottomNavigationView by lazy {
requireView().findViewById(R.id.bottom_navigation_view)
}
private val tabsNavigation: NavController by lazy {
(childFragmentManager.findFragmentById(R.id.tabs_host_fragment) as NavHostFragment).navController
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
bottomNavigationView.setupWithNavController(tabsNavigation)
}
}
Here is a menu for bottom navigation view:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#id/dashboard_navigation"
android:title="Dashboard" />
<item
android:id="#id/search_vehicle_navigation"
android:title="Search" />
<item
android:id="#id/todos_navigation"
android:title="Todos" />
<item
android:id="#id/activities_navigation"
android:title="Activities" />
<item
android:id="#id/more_navigation"
android:title="More" />
</menu>
Graph for bottom menu:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/tabs_navigation"
app:startDestination="#id/search_vehicle_navigation">
<include app:graph="#navigation/dashboard_navigation" />
<include app:graph="#navigation/search_vehicle_navigation" />
<include app:graph="#navigation/todos_navigation" />
<include app:graph="#navigation/activities_navigation" />
<include app:graph="#navigation/more_navigation" />
</navigation>
Everything is done according Google tutorials and samples and it does not work. Only 1 default fragment that is setted as StartDestination is saved (Becouse it is in stack to be displayed when user press back button):
https://developer.android.com/guide/navigation/navigation-multi-module
https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample
My project uses fragment navigation to transition between destinations,
In the video it is seen that the page is offset up or down.
I would like the page to only enter the screen from left to right.
Where might the problem be?
thanks for your help.
app video
Host Activity
class ProductHostActivity: AppCompatActivity() {
private lateinit var navHostFragment: NavHostFragment
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_product_host)
navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
navController = findNavController(R.id.fragment)
navController.setGraph(R.navigation.nav_product)
}
}
HomeFragment
button_add_to_cart.setOnClickListener {
findNavController().navigate(R.id.action_productDetailFragment_to_productIntroDetailFragment)
}
navigation
<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
android:id="#+id/nav_login"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.presentation.view.homeFragment"
tools:layout="#layout/fragment_product_detail">
<action
android:id="#+id/action_homeFragment_to_detailFragment"
app:destination="#id/detailFragment"
app:enterAnim="#anim/slide_in_right_test"
app:exitAnim="#anim/hold_page"
app:popEnterAnim="#anim/hold_page"
app:popExitAnim="#anim/slide_out_right_test" />
</fragment>
<fragment
android:id="#+id/detailFragment"
android:name="com.fragment.ProductReadMoreFragment"
android:label="detailFragment" />
</navigation>
in animation
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="3000"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>
out animation
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="3000"
android:fromXDelta="0%"
android:toXDelta="100%" />
</set>
hold page
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="0%" android:toYDelta="0%"
android:duration="3000"/>
</set>
In my app, i have an Activity, which has a FrameLayout in it. In this FrameLayout, there is a fragment, containing a ToolBar and a RecyclerView.
In this toolbar, i have a search button, which should start an Activity on item click. However, when i try to use onOptionsItemSelected, the apps gets built and installed succesfully, but when i try to tap that button in question, nothing happens. The Logcat, doesnt say anything either.
Can some points me what im doing wrong? Are there simpler or other easy ways to manage on ToolBar item clicks?
Fragment.kt
class FragmentTrack : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_track, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
topToolbar.setNavigationOnClickListener {
val dialog = FragmentBottomSheetDrawer()
dialog.show(childFragmentManager, dialog.tag)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_toolbar, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.fsvSearch -> Toast.makeText(context, "Clicked search button", Toast.LENGTH_SHORT).show()
}
return true
}
}
fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:theme="#style/Theme.Design.NoActionBar">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/topToolbar"
android:background="#color/colorPrimaryDark"
app:navigationIcon="#drawable/ic_outline_menu_24"
app:popupTheme="#style/popupMenuThemeDark"
app:titleTextColor="#android:color/white"
app:title="Revo"
app:menu="#menu/menu_toolbar"
app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvTracks"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipToPadding="false"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
toolbar_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/fsvSearch"
android:icon="#drawable/ic_search_24px"
android:title="#string/search"
app:showAsAction="always"/>
<item
android:id="#+id/fsvOrder"
android:icon="#drawable/ic_sort_24px"
android:title="#string/sort"
app:showAsAction="ifRoom">
<menu>
<group android:id="#+id/sortMenu" android:checkableBehavior="single">
<item
android:id="#+id/sortIncrease"
android:title="#string/increasing"/>
<item
android:id="#+id/sortDecrease"
android:title="#string/decreasing"/>
<item
android:id="#+id/sortMArtist"
android:title="#string/artist"/>
<item
android:id="#+id/sortAlbum"
android:title="#string/albums"/>
<item
android:id="#+id/sortYear"
android:title="#string/year"/>
<item
android:id="#+id/sortDate"
android:title="#string/date"/>
</group>
</menu>
</item>
<item
android:id="#+id/fsvGrid"
android:icon="#drawable/ic_grid_on_24px"
android:title="#string/grid"
app:showAsAction="ifRoom">
<menu>
<group android:id="#+id/gridMenu" android:checkableBehavior="single">
<item
android:id="#+id/gridOne"
android:title="1"/>
<item
android:id="#+id/gridTwo"
android:title="2"/>
<item
android:id="#+id/gridThree"
android:title="3"/>
<item
android:id="#+id/gridFour"
android:title="4"/>
</group>
</menu>
</item>
</menu>
I found a solution thanks to some external help. Its possible to work on the Toolbars item in an easier way.
In the onViewCreated method, we must add:
topToolbar.inflateMenu(R.menu.menu_toolbar)
topToolbar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.fsvSearch -> //your code
}
true
}
Also, if the menu gets duplicated, remove the app:menu tag in the Toolbars xml
thanks Meltix for this, I've done a lot of research but most of the posts in SO are about Java and old fashioned ActionBar. There is not much info about material ToolBar and kotlin examples.
In my case I'm working on an old app developed mostly in Java but now developing new functionalities in Kotlin. In my fragment, I wasn't able to change the ActionBar's icons and behaviour (I wanted a particular ActionBar for my fragment). The solution I found is to hide the ActionBar and create a Toolbar inflating a custom menu inside. Also I added a back arrow button (thanks to John: here).
I've came to the conclusion (maybe I'm wrong) that you can't manage ActionBars from kotlin fragments, that's why you have to use toolbars. Here is my code in case it can help someone.
MyFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// hide the old actionBar
(activity as AppCompatActivity).supportActionBar!!.hide()
// create the toolbar
val toolbar = binding?.myToolbar
// add the back arrow button, see function below
val resId = getResIdFromAttribute(toolbar.context, android.R.attr.homeAsUpIndicator)
toolbar.navigationIcon = ContextCompat.getDrawable(toolbar.context, resId)
// tapping on the arrow will pop the current fragment
toolbar.setNavigationOnClickListener { parentFragmentManager?.popBackStackImmediate() }
// change toolbar title
toolbar.title = "Some title"
// inflate a custom menu, see code below
toolbar.inflateMenu(R.menu.my_custom_menu)
toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.infoButton -> someAction()
}
true
}
}
// get back arrow icon
#AnyRes
fun getResIdFromAttribute(context: Context, #AttrRes attr: Int): Int {
if (attr == 0) return 0
val typedValueAttr = TypedValue()
context.theme.resolveAttribute(attr, typedValueAttr, true)
return typedValueAttr.resourceId
}
my_fragment.xml
...
<androidx.appcompat.widget.Toolbar
android:id="#+id/myToolBar"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:visibility="visible"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:background="#color/azulOscuro"
>
</androidx.appcompat.widget.Toolbar>
my_custom_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:jsmovil="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="#+id/infoButton"
android:icon="#drawable/ic_info_blanco"
android:title="#string/tutorial"
android:textColor="#color/blanco"
jsmovil:showAsAction="always"
/>
</menu>
Result
I'm showing a fragment using the androidx navigation architecture component.
The fragment is shown by an ArcMotion shared element transition, but the exit transition should be a simple slide down.
I'm showing the fragment the following way:
findNavController().navigate(
EntriesFragmentDirections.actionEntriesFragmentToNewBookEntryFragment(
bookEntryType = mEntriesViewPager.currentItem,
enterTransition = R.transition.new_book_entry_enter,
exitTransition = R.transition.new_book_entry_exit
),
FragmentNavigatorExtras(fabEntriesNewBookEntry to getString(R.string.transition_new_book_entry_fragment))
)
And I apply the transitions in the fragment in onCreate:
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(args.enterTransition)
exitTransition = TransitionInflater.from(requireContext()).inflateTransition(args.exitTransition)
The enter transition is working well, but the exit transition is still the reversed enter transition.
The enter transition xml:
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:duration="#android:integer/config_mediumAnimTime">
<transition
class="at.guger.moneybook.ui.transition.MorphTransform"
app:endColor="?colorSurface"
app:endCornerRadius="#dimen/zero"
app:startColor="?colorSecondary"
app:startCornerRadius="#dimen/fabRadius" />
</transitionSet>
The exit transition xml:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="#android:integer/config_mediumAnimTime">
<slide android:slideEdge="bottom" />
</transitionSet>
The solution here is to inflate the transitions in the fragment's onCreate:
private val args: NewBookEntryFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
if (Utils.isLollipop() && args.enterTransition > 0 && args.exitTransition > 0) {
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(args.enterTransition)
sharedElementReturnTransition = null
returnTransition = TransitionInflater.from(requireContext()).inflateTransition(args.exitTransition)
}
The navigation call looks the following way:
findNavController().navigate(
EntriesFragmentDirections.actionEntriesFragmentToNewBookEntryFragment(
bookEntryType = mEntriesViewPager.currentItem,
enterTransition = R.transition.new_book_entry_enter,
exitTransition = R.transition.new_book_entry_exit
),
FragmentNavigatorExtras(fabEntriesNewBookEntry to getString(R.string.transition_new_book_entry_fragment))
)
new_book_entry_enter.xml:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:duration="#android:integer/config_mediumAnimTime">
<transition
class="at.guger.moneybook.ui.transition.MorphTransform"
app:endColor="?colorSurface"
app:endCornerRadius="#dimen/zero"
app:startColor="?colorSecondary"
app:startCornerRadius="#dimen/fabRadius" />
</transitionSet>
new_book_entry_exit.xml:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="#android:integer/config_mediumAnimTime">
<slide
android:interpolator="#android:interpolator/accelerate_quint"
android:slideEdge="bottom" />
</transitionSet>
I have two fragments (A, B), among which I can swap; what I wanted to achieve was a slide up / down animation between them every time I swapped them.
I tried using two object animator like this:
//slide up
<objectAnimator
android:interpolator="#android:interpolator/linear"
android:propertyName="translationY"
android:valueType="intType"
android:valueFrom="1920"
android:valueTo="0"
android:duration="1000" />
//Slide down
<objectAnimator
android:interpolator="#android:interpolator/linear"
android:propertyName="translationY"
android:valueType="intType"
android:valueFrom="0"
android:valueTo="1920"
android:duration="1000" />
but it didn't work because the two fragment were overlapping.
So how can I do that animation?
Fragment A:
class FragmentA : Fragment(){
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonA.setOnClickListener {
activity.supportFragmentManager.beginTransaction()
.setCustomAnimations(R.animator.slide_dowm, R.animator.slide_up)
.replace(R.id.container, FragmentB()).commit()
}
}
}
Fragment B:
class FragmentB : Fragment(){
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonB.setOnClickListener {
activity.supportFragmentManager.beginTransaction()
.setCustomAnimations(R.animator.slide_up, R.animator.slide_down)
.replace(R.id.container, FragmentB()).commit()
}
}
}
Google released the new Navigation UI library
So, now we can do the same fragment transitions from a your_named_navigation.xml resource (main > res > navigation > your_named_navigation.xml),
this an snippet code of my implementation:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
app:startDestination="#+id/first_fragment">
<fragment
android:id="#+id/first_fragment"
android:name="com.yourpackage.FirstFragment"
android:label="#string/title_first"
tools:layout="#layout/fragment_first">
<action
android:id="#+id/second_fragment_action"
app:destination="#id/second_fragment"
app:enterAnim="#anim/slide_in_right"
app:exitAnim="#anim/slide_out_left"
app:popEnterAnim="#anim/slide_in_left"
app:popExitAnim="#anim/slide_out_right" />
</fragment>
<fragment
android:id="#+id/second_fragment"
android:name="com.yourpackage.SecondFragment"
android:label="#string/title_second"
tools:layout="#layout/fragment_second">
<action ...next fragment/>
</fragment>
</navigation>
it also helps to handle clicks on back button and up button,
so, after have NavigationUi implementation in our proyect, we can call from our firstFragment instance the Navigation.findNavController method
myButton.setOnClickListener(View.OnClickListener {
//This opens our second fragment creating a stack of fragments
Navigation.findNavController(it).navigate(R.id.second_fragment_action)
})
The next Google's Codelab helped me, maybe can help you, greetings
animator/slide_in_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="x"
android:valueFrom="1000"
android:valueTo="0"
android:valueType="floatType" />
</set>
animator/slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="x"
android:valueFrom="0"
android:valueTo="-1000"
android:valueType="floatType" />
</set>
Class Subcategory
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// TODO Auto-generated method stub
// return super.onCreateView(inflater, container, savedInstanceState);
View view = (ViewGroup) inflater.inflate(R.layout.product_frame, null);
getFragmentManager().beginTransaction()
.replace(R.id.sub_header, new Sub_Header()).commit();
getFragmentManager()
.beginTransaction()
.setCustomAnimations(R.animator.slide_in_left,
R.animator.slide_out_right, 0, 0)
.replace(R.id.product_frame, new Sub_Catagory_Grid()).commit();
view.getWidth();
return view;
}
Some links
Fragment transaction animation: slide in and slide out
How to apply a fade-in/fade-out animation when replacing a fragment