My main activity is a Swipe View with tabs in the action bar, which can be used to change to a particular fragment directly. I basically followed the developer guidelines on this. So far this works fine and as expected.
However, I now have a couple of items in the menu (Settings, About), which should not be displayed as part of the ViewPager, but rather should replace the ViewPager completely and set the "navigation up" affordance in the action bar. Following along with the answers to this question I know how to use the BackStack to manipulate the action bar and to show the "navigation up" affordance.
However I'm not sure what the best way to replace the ViewPager would be. As far as I know I can either try to disable all ViewPager functionality and make it appear as it would be a single fragment (e.g. disable tabs and swipe), or I could use nested fragments. Yet, I'm not convinced that either of this options is "clean".
Maybe I'm overlooking something here and there is a more intuitive way to achieve the same? What are you guys thinking about this and how do you implement something "basic" as this?
P.S.: Obviously I could use activities for this, but I think that an Activity is too heavy for a simple "About" text and in my understanding one should try to use Fragments wherever possible these days.
As I can understand, you could put the ViewPager inside a parent as FrameLayout and add() the "about" fragment with addToBackState() method above the ViewPager.
You will avoid to disable or refresh the ViewPager. Just add above it a new fragment.
UPDATE
I'm able to achieve it with add() method and a custom background on the added fragment to avoid the overlap issues. And finally make this background clickable to prevent the click events for the behind ViewPager.
See my activity layout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.example.viewpageroverlap.MainActivity"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<android.support.v4.view.ViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
My Menu item event:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, OverlapFragment.newInstance(990), null).addToBackStack(null).commit();
return true;
}
return super.onOptionsItemSelected(item);
}
My Overlap Fragment layout:
<RelativeLayout 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"
tools:context="com.example.viewpageroverlap.MainActivity$OverlapFragment"
android:background="#FF0000"
android:clickable="true" >
<TextView
android:id="#+id/section_label"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center" />
</RelativeLayout>
This gives me this output:
Note: I used a red background but you can try with Android Resources Color and avoid to use a color declared in your files as android:background="#android:color/white".
WITH TABS
You can do the same as above and reset the navigation with NAVIGATION_MODE_STANDARD:
if (id == R.id.action_settings) {
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
getSupportFragmentManager().beginTransaction()
.add(R.id.container, OverlapFragment.newInstance(990), null).addToBackStack(null).commit();
return true;
}
Then, when the user come back to the ViewPager (when he presses the home button or hardware back button), reset the old navigation as:
// do the same with android.R.id.home inside onOptionsItemSelected
#Override
public void onBackPressed() {
// check if the "about" fragment is still displayed
if(this.getSupportFragmentManager().getBackStackEntryCount() > 0) {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
this.getSupportFragmentManager().popBackStack();
}
}
Just have the ViewPager in its own fragment and replace that when you want to change to another fragment using regular fragment transactions.
If you turn on addToBackStack those transactions will react to the back button in a natrual way.
Related
I'm using Jetpack Navigation version 1.0.0-alpha04 with bottom navigation. It works but the navigation doesn't happen correctly. For example, if I have tab A and tab B and from tab A I go to Page C and from there I go to tab B and come back to tab A again, I will see root fragment in the tab A and not page C which does not what I expect.
I'm looking for a solution to have a different stack for each tab, so the state of each tab is reserved when I come back to it, Also I don't like to keep all this fragment in the memory since it has a bad effect on performance, Before jetpack navigation, I used this library https://github.com/ncapdevi/FragNav, That does exactly what, Now I'm looking for the same thing with jetpack navigation.
EDIT 2: Though still no first class support (as of writing this), Google has now updated their samples with an example of how they think this should be solved for now: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
The major reason is you only use one NavHostFragment to hold the whole back stack of the app.
The solution is that each tab should hold its own back stack.
In your main layout, wrap each tab fragment with a FrameLayout.
Each tab fragment is a NavHostFragment and contains its own navigation graph in order to make each tab fragment having its own back stack.
Add a BottomNavigationView.OnNavigationItemSelectedListener to BottomNavigtionView to handle the visibility of each FrameLayout.
This also takes care of your "...I don't like to keep all this fragment in memory...", because a Navigation with NavHostFragment by default uses fragmentTransaction.replace(), i.e. you will always only have as many fragments as you have NavHostFragments. The rest is just in the back stack of your navigation graph.
Edit: Google is working on a native implementation https://issuetracker.google.com/issues/80029773#comment25
More in detail
Let's say you have a BottomNavigationView with 2 menu choices, Dogs and Cats.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="#+id/dogMenu"
.../>
<item android:id="#+id/catMenu"
.../>
</menu>
Then you need 2 navigation graphs, say dog_navigation_graph.xml and cat_navigation_graph.xml.
The dog_navigation_graph might look like
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/dog_navigation_graph"
app:startDestination="#id/dogMenu">
</navigation>
and the corresponding for cat_navigation_graph.
In your activity_main.xml, add 2 NavHostFragments
<FrameLayout
android:id="#+id/frame_dog"
...>
<fragment
android:id="#+id/dog_navigation_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/dog_navigation_graph"
app:defaultNavHost="true"/>
</FrameLayout>
and underneath add the corresponding for your cat NavHostFragment. On your cat frame layout, set android:visibility="invisible"
Now, in your MainActivity's onCreateView you can
bottom_navigation_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.dogMenu -> showHostView(host = 0)
R.id.catMenu -> showHostView(host = 1)
}
return#setOnNavigationItemSelectedListener true
}
All that showHostView() is doing is toggling the visibility of your FrameLayouts that are wrapping the NavHostFragments. So make sure to save them in some way, e.g. in onCreateView
val hostViews = arrayListOf<FrameLayout>() // Member variable of MainActivity
hostViews.apply {
add(findViewById(R.id.frame_dog))
add(findViewById(R.id.frame_cat))
}
Now it's easy to toggle which hostViews should be visible and invisible.
The issue has been resolved by the Android team in the latest version 2.4.0-alpha01 multiple backstacks along with bottom navigation support is now possible without any workaround.
https://developer.android.com/jetpack/androidx/releases/navigation
First, I want to make an edit to #Algar's answer. The frame that you want to hide should have android:visibility="gone" instead of invisible. The reason for that in your main layout you would have something like this:
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.activity.MainActivity">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar_base" />
<FrameLayout
android:id="#+id/frame_home"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
>
<fragment
android:id="#+id/home_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/home_nav" />
</FrameLayout>
<FrameLayout
android:id="#+id/frame_find"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:visibility="gone">
<fragment
android:id="#+id/find_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/find_nav" />
</FrameLayout>
...
</LinearLayout>
If you wrap your main in a LinearLayout, setting the frame to invisible still make that frame counts, so the BottomNavigation wont appear.
Second, you should create a NavHostFragment instance (ie: curNavHostFragment) to keep track of which NavHostFragment is being visible when a tab in BottomNavigation is clicked. Note: you may want to restore this curNavHostFragment when the activity is destroyed by configuration's changes. This is an example:
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//if this activity is restored from previous state,
//we will have the ItemId of botnav the has been selected
//so that we can set up nav controller accordingly
switch (bottomNav.getSelectedItemId()) {
case R.id.home_fragment:
curNavHostFragment = homeNavHostFragment;
...
break;
case R.id.find_products_fragment:
curNavHostFragment = findNavHostFragment;
...
break;
}
curNavController = curNavHostFragment.getNavController();
So, I have an activity with navigation view. By click on its item I change fragment in activity. All fragment have the same toolbar. But one have this toolbar and TabLayout to it. I would like to know what is better to declare toolbar once on activity like this
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="#layout/toolbar" />
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/toolbar" />
</RelativeLayout>
or declare it in each fragment.
The disadvantage of the first method is default toolbar shadow. When I add tabs in fragment, shadow looks like
When I tried 2 solution. All my toolbar was with back icon instead drawer animated logo.
Thanks.
I had the exact same problem. This is how I solved it:
Move the toolbars to the fragments like you suggested (so you won't have a shadow separating the two). This allows for a way more flexible way to implement (different) toolbars in your layouts too.
Replace the Toolbar's navigation icon by a custom one like this:
toolbar.setNavigationIcon(R.drawable.ic_action_menu);
(I used the Android Asset Studio to easily create an icon with the preferred color)
Now open the NavigationView with the new menu(home) icon. You can do this through the MainActivity (the one with the NavigationView). Create a public method in that Activity that opens the drawer:
public void openDrawer(){
mDrawerLayout.openDrawer(Gravity.LEFT);
}
Now call this method in the OnOptionsItemSelected in your fragments like this:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// handle item selection
switch (item.getItemId()) {
case android.R.id.home: //Menu icon
((MainActivity)getActivity()).openDrawer();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
That's it. Of course the downside is that you must implement the Toolbar in each Fragment. However, this is the only way (that I know of) that enables you to have the Toolbar (+TabLayout) in a Fragment and still be able to control your NavigationView.
You can use AppBarLayout from design support library like:
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
...
app:layout_scrollFlags="scroll|enterAlways" />
<android.support.design.widget.TabLayout
...
/>
</android.support.design.widget.AppBarLayout>
and then you can change visibility of tablayout.
For more information about desing layout library : link
I am using a android.support.v4.widget.DrawerLayout in my activity.
As long as I use the swipe gesture to open it, it works fine.
However, when I want to open it through a button click by calling drawer.openDrawer(Gravity.LEFT), it does not work.
But, if I open it once using the swipe gesture, after that it works normally with button click.
Any idea how I could solve or work around this?
I had the same issue and I've just found out that for some reason the FrameLayout that represents the drawer have the visibility set to "gone", that probably goes to "visible" during the first slideGesture.
So, open your layout xml file, locate your FrameLayout that represents the drawer and simply erase the visibility setting. My opening tag is now as follows:
<FrameLayout
android:layout_width="305dp"
android:layout_height="match_parent"
android:layout_gravity="start">
That worked for me.
Cheers
In my case the visibility on 'NavigationView' was set to gone in the layout. Changing it to visible solved the issue
If you want to open it from the Top Left Toggle you should implement onOptionsItemSelected(MenuItem item)
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// The action bar home/up action should open or close the drawer.
// ActionBarDrawerToggle will take care of this.
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
switch (item.getItemId()) {
case android.R.id.home:
return true;
}
return super.onOptionsItemSelected(item);
}
Please use clearFocus() method for DrawerLayout object.
Encountered the same problem and was able to fix it by specifying a width for the drawer content element inside the layout.
Without layout_width attribute the drawer did not open on the first openDrawer() call,
with the value it does.
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
<FrameLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="right"
android:background="#ffffff"
android:id="#+id/drawer_content">
</FrameLayout>
Too stupid, i did set an empty layout,
problem was just as described above, when the drawer was manually dragged the first time, after that, the button worked, but without dragging it first, the navigation drawer did never open.
Don't set an empty layout.
Removing my EmptyLayout from the ListView which represented the NavigationDrawerContent made it work perfect again.
DONT
drawerContentListView.setEmptyView(getLayoutInflater().inflate(R.layout.navigation_drawer_empty_layout, null));
**** costed me more than an hour.
You can put this code inside your DrawerLayout:
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="#layout/nav_header_main"
app:menu="#menu/activity_main_drawer" />
Select your NavigationView > Set visibility properties to visible
On Android 3.0 and above, the android team is driving hard that you should use fragments over activities. And I see this being useful, but I want to be able to handle click events in my app. I'm using a list fragment on the right side of my app, so doing an onclick (or any click listeners) happens in the activity that hosts the fragment. So I had to move from putting a item in XML to using the fragment manager.
In the design documents they show this picture:
http://developer.android.com/training/basics/fragments/fragment-ui.html
What I want is the Fragment A/B tablet UI. However, nowhere in this page does it actually give you an example of doing this - it seems that fragment manager only works with ONE fragment at a time - which is entirely opposite of what the picture portrays. Which makes me think it uses in XML... but then how would I get an onclick? These documents don't make a lot of sense to me. It shows one thing and then says something else. What if I wanted to remove fragment A on the tablet? Add fragment C that doesn't yet exist? Is that even possible if you tried to use Fragment Manager????
I guess I don't get if Fragment manager uses more than 1 fragment, and if it does, how am I supposed to use this to get an item in the picture like the tablet - the left (A) being a listview, and the right (B) being whatever. Without an ID of the fragment I don't how to access it.
Not sure if this is relevant but here is some of my code:
Adds a fragment to the single framelayout I made like in the guide
//Activity
FragLaunch launchPane = new FragLaunch();
// In case this activity was started with special instructions from an Intent,
// pass the Intent's extras to the fragment as arguments
// firstFragment.setArguments(getIntent().getExtras());
// Add the fragment to the 'fragment_container' FrameLayout
getFragmentManager().beginTransaction()
.add(R.id.launch_frag_container, launchPane).commit();
}
Also, in portrait mode of 7" tablets, I want it to use a viewpager that is swipeable. It worked like a charm when I designed it in XML but now that I have to access the listfragment it doesn't work (no way to access since I can't have two fragments)
XML of FragLaunch's content view:
<LinearLayout 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:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical"
android:orientation="vertical" >
<TextView
android:id="#+id/initial_directions"
style="#style/textScalar.Roboto.Light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="#string/initial_directions"
android:layout_marginBottom="30dp"
tools:context=".Launch" />
</LinearLayout>
I want to have this one appear as Fragment A in the photo:
FragHistory.java/xml for fragment:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="#+id/spamhistory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Spam History" />
<ListView
android:id="#android:id/list"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:drawSelectorOnTop="false" />
</LinearLayout>
Does anyone have any insight on this?
If you want your fragments to be able to communicate then you need to use interfaces, like this.
For onClick events you simply set an onClickListener for the view that you need to receive the onClick event from. Like so:
sampleView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//Your code here
}
});
As for fragment transactions, it says somewhere in there (I can't remember exactly where) that when two fragments are displayed on the screen at once (as with larger screens) that instead of swapping the fragments it simply updates it. All you have to worry about is making the correct calls. So if you want to remove fragment A just call remove(). Or if you want to replace it with fragment C call replace() and pass fragment C in as the parameter.
Can you clarify your question about the ViewPager? What do you mean "have to access it"?
Hope this helps!
EDIT: I apoplogize, I misunderstood your question. Here's kind of a quick run down of how to add more than one fragment to the screen at once.
1. Perform a runtime check to make sure that the device screen is big enough to display more than one fragment.
2. If the screen is big enough, set the content view to use a layout that has a FrameLayout for each fragment that you want to add.
3. After that grab a reference to each fragment that you want to use.
4. Then use the FragmentManager to add a fragment to each layout. Like this:
FirstExampleFragment fragment1 = new FirstExampleFragment();
SecondExampleFragment fragment2 = new SecondExampleFragment();
getSupportFragmentManager().beginTransaction().add(R.id.example_framelayout1, fragment1)
.add(R.id.example_framelayout2, fragment2).commit();
Another great way to allow communication between fragments is to use an event bus, such as the Otto event bus. Otto allows components to publish events and subscribe to events in a decoupled manner.
In your particular case, when a user selects an item in the list, your list fragment can publish an event (which can include the item that has been selected) and your content fragment can subscribe for these events and update its content accordingly when it receives a new event. This all being done without the two fragments being directly coupled and without having to define additional interfaces.
I know this doesn't answer your entire question, but thought it might be useful when it comes to the communications between your fragments....YMMV.
I want to implement a layout that has the following design:
Landscape:
List of items on the left, details on the right
Portrait:
List of items, only details after click
To get this working I followed those tutorials:
Tutorial 1
Tutorial 2
It worked as expected.
But instead of starting a new activity when in portrait and clicking a Listitem,
I want the fragment to be replaced by the detail, so that I can animate the transition.
So i played around and got problems with the views. After that I read the following article, and modified my layout to use placeholders an add the fragments programmatically:
Article
The action that is triggered when the listitem is clicked just replaces the list fragment in his container with the detail fragment.
But now if I revert to landscape, the list part of the layout is showing the detail instead of the list, because i replaced the content.
Is there any way to solve this problem?
At the moment I managed to get this working by using the two fragment placeholders in both layouts, landscape and portrait, with wrap content, and hiding and showing the fragments, but I don't know if this is the right approach? Perhaps a ViewFlipper would be better?
Also the animation here does not work properly, cause I hide and show in the same transaction.
Also if I am in Landscape mode and click a listitem and data is showing, and I return to portrait, I want the data view to be shown, not the list, but ONLY if data is already shown.
Atm I managed this with an flag passed to the intent data and again show/hide the correct view.
Any alternative ideas?
Thank you very much, have been trying for hours now!
EDIT
The main problem I have is, that the slide in/out animation I set is not played correctly, as i hide and show the fragments in one transaction. It just hides the one fragment and slides in the other, so the first fragment is not slided out :/
To see what I mean, here is how I implemented it in a fragment test app:
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<FrameLayout
android:id="#+id/details_Fragment_Placeholder"
android:layout_width="wrap_content"
android:layout_height="fill_parent" >
</FrameLayout>
<FrameLayout
android:id="#+id/main_Fragment_Placeholder"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</FrameLayout>
</LinearLayout>
The ListFragment onListItemClick:
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
FragmentManager fragmentManager = getFragmentManager();
Screen2Fragment fragment2 = (Screen2Fragment) fragmentManager
.findFragmentByTag(Screen2Fragment.TAG);
if (fragment != null) {
if ((getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)) {
.beginTransaction();
transaction.setCustomAnimations(R.anim.slide_in_right,
R.anim.slide_out_left, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
Screen1ListFragment fragment1 = (Screen1ListFragment) fragmentManager
.findFragmentByTag(Screen1ListFragment.TAG);
transaction.hide(fragment1);
transaction.show(fragment2);
transaction.addToBackStack("ReturnToScreen1");
transaction.commit();
}
}
}
When you place the list fragment initially you need to put it into the back stack, then override the back button in the detail fragment to pop the list view off the back stack when pressed.