I'm using the Support Package v7 for adding Action Bar Tabs to my app.
When the tab's Fragment has a root view with height="fill_parent" the Fragment is covering my tabs.
When I’m changing the Fragment's root view to height="wrap_content" I can see the tab but my Fragment styling is affected.
My code for loading Fragments is the same as in Android Docs:
public class PicoTabListener<T extends Fragment> implements TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
/** Constructor used each time a new tab is created.
* #param activity The host Activity, used to instantiate the fragment
* #param tag The identifier tag for the fragment
* #param clz The fragment's Class, used to instantiate the fragment
*/
public PicoTabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
/* The following are each of the ActionBar.TabListener callbacks */
public void onTabSelected(Tab tab, FragmentTransaction ft) {
// Check if the fragment is already initialized
if (mFragment == null) {
// If not, instantiate and add it to the activity
mFragment = Fragment.instantiate(mActivity, mClass.getName());
ft.add(android.R.id.content, mFragment, mTag);
} else {
// If it exists, simply attach it in order to show it
ft.attach(mFragment);
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (mFragment != null) {
// Detach the fragment, because another one is being attached
ft.detach(mFragment);
}
}
#Override
public void onTabReselected(Tab arg0, FragmentTransaction arg1) {
// do nothing
}
Isn't tab should always be in the top of the screen as the first layer?
Isn't tab should always be in the top of the screen as the first layer ?
No.
If you were using ActionBarSherlock, I think what you are doing would work. And if you use the native action bar implementation, I think what you are doing would work. However, you cannot do this with AppCompat:
ft.add(android.R.id.content, mFragment, mTag);
Use some container inside of your layout, like a FrameLayout, as the target of the transaction, not android.R.id.content. See this issue for more.
Related
The problem I'm having is the Action Bar will not show on Android 2.3.7, but will work fine on 4.x+. The rest of my application works fine with the support v7 and v4 libraries, it's just this one area which is giving me trouble.
Here is what it should look like, as seen on 4.3:
And here is what it looks like on 2.3.7:
Inside my onCreate method (of the class which inherits from ActionBarActivity), I have this:
// setup action bar for tabs
ActionBar actionBar = getSupportActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
Tab tab = actionBar.newTab()
.setText(R.string.details)
.setTabListener(new TabListener<DetailsFragmentOne>(
this, "one", DetailsFragmentOne.class));
actionBar.addTab(tab);
tab = actionBar.newTab()
.setText(R.string.grades)
.setTabListener(new TabListener<DetailsFragmentTwo>(
this, "one", DetailsFragmentTwo.class));
actionBar.addTab(tab);
And here is my TabListener, an inner class:
/**
* This is copied almost verbatim from the ActionBar Tabs API Guide.
* #param <T>
*/
public class TabListener<T extends Fragment> implements ActionBar.TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
/** Constructor used each time a new tab is created.
* #param activity The host Activity, used to instantiate the fragment
* #param tag The identifier tag for the fragment
* #param clz The fragment's Class, used to instantiate the fragment
*/
public TabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
/* The following are each of the ActionBar.TabListener callbacks */
public void onTabSelected(Tab tab, FragmentTransaction ft) {
FragmentTransaction sft = ((FragmentActivity) mActivity).getSupportFragmentManager().beginTransaction();
mFragment = getSupportFragmentManager().findFragmentByTag(mTag);
// Check if the fragment is already initialized
if (mFragment == null) {
// If not, instantiate and add it to the activity
mFragment = Fragment.instantiate(mActivity, mClass.getName());
// calling commit() here because we're not using the provided FragmentTransaction
sft.replace(android.R.id.content, mFragment, mTag).commit();
} else {
// If it exists, simply attach it in order to show it
// calling commit() here because we're not using the provided FragmentTransaction
sft.replace(android.R.id.content, mFragment).commit();
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
FragmentTransaction sft = ((FragmentActivity) mActivity).getSupportFragmentManager().beginTransaction();
mFragment = getSupportFragmentManager().findFragmentByTag(mTag);
if (mFragment != null) {
// calling commit() here because we're not using the provided FragmentTransaction
sft.replace(android.R.id.content, mFragment).commit();
}
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
FragmentTransaction sft = ((FragmentActivity) mActivity).getSupportFragmentManager().beginTransaction();
mFragment = getSupportFragmentManager().findFragmentByTag(mTag);
if (mFragment != null) {
// calling commit() here because we're not using the provided FragmentTransaction
sft.replace(android.R.id.content, mFragment).commit();
}
}
}
I have seen these two other questions and attempted to implement the answers, but am still having the issue.
Implementing a TabListener using the Support Library
Implementing ActionBar tabs with v4 Fragments API
edit:
As requested, the theme which is being applied is simply the support library's AppCompat.Light.DarkActionBar theme with no overrides, seen below:
<style name="Theme.MyTheme" parent="#style/Theme.AppCompat.Light.DarkActionBar">
</style>
If you remove the background of your DetailFragment, the ActionBar and Tabs actually appear behind the DetailFragment. Instead of android.R.id.content, create your own container in your main layout and use R.id.yourcontent when calling replace in your FragmentTransaction. By making this change, it worked for me on 2.3.3 and 4+.
It appears that 2.3.3 adds the ActionBar to the root view element where 4+ is adding it outside of the root view.
sft.replace(R.id.yourcontent, mFragment).commit();
You should read the offcial document.
You must not call commit() for the fragment transaction in each of these callbacks—the system calls it for you and it may throw an exception if you call it yourself. You also cannot add these fragment transactions to the back stack.
I am working on an app that has an "artist list", a list of a good chunk of music bands, performers and/or composers. They are sorted by genre and relative popularity.
From the main activity you go to a "band list" activity that has two tabs in it. One of them is a "genres" tab that has a list of the general music genres and when you click on each genre you get sent to the respective activity that also has two tabs in it. The first one is the "All" tabs and the other one's the "Popular" tab. Each of the tabs holds a fragment that contains a filtered sqlite database (by "filtered" I mean I have one big database that gets filtered to show what I want in different fragments rather than using a new database for each fragment/genre). So now I have 7 genres and 14 classes; 2 ("all" and "popular") for each genre.
What I would like to do now is reduce the number of classes to only two (all/popular) or, if possible, even one, by recycling the same fragment for all lists via filtering the database again. In order to do this I need to send a string from the "genres" fragment, containing the position of the pressed list item (genre) to the activity which contains the "All" and "Popular" tabs (fragments), which will then send that string further, to the fragments, which will use it to filter the sqlite database.
I need to add that I have, of course, read the Android Developers guide on fragment communication, but I do not know how to use their approach in my code, since I have created the fragments a little differently, including not defining them in the xml, so because of that I don't know how to use their getSupportFragmentManager().findFragmentById(R.id.article_fragment); method and some more things like that.
Here is the TabListener via which I am creating all fragments:
public class TabListener<T extends Fragment> implements ActionBar.TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
public TabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if (mFragment == null) {
mFragment = Fragment.instantiate(mActivity, mClass.getName());
ft.add(android.R.id.content, mFragment, mTag);
} else {
ft.attach(mFragment);
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (mFragment != null) {
ft.detach(mFragment);
}
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
}
Thanks,
Aleksa
If you created the fragments programmatically and added them with a tag, u can use the support FragmentManager method: findFragmentByTag(String tag)
After some more trying I eventually gave up on setArguments() and getArguments() and used this in the parent activity:
public int getPosition(){ return position; }
...and this in the child fragment:
position=((ParentActivity) getActivity()).getPosition();
I don't know if it is the most efficient method around but it works great and makes sending some simple data to a fragment a trivial thing for newcomers like me as it should be. :)
My problem is about fragments with tabs. I have got one activity which basically has an empty layout and I add fragments to it dynamically. As soon as I start the activity I add a fragment with a listview to it and when I click on an item of the list I remove the fragment with the listview and add another fragment with details about it. Also some tabs are visible now. Clicking on one of these tabs shows other details about the item from the listview.
I do have working code but it seems that I'm hacking it altogether.
Here is some bits of it:
Activity:
// In onCreate:
#Override
protected void onCreate(Bundle savedInstanceState) {
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of FList
fList = new FList();
// Add the fragment to the 'fragment_container' FrameLayout
getFragmentManager().beginTransaction().add(R.id.fragment_container,fList).commit();
}
}
// After clicking on an item of the listview changeFragment() is called
public void changeFragment() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
fSpeed = new FSpeed();
ft.add(R.id.fragment_container, fSpeed);
// this might be necessary
if (actionBar != null)
actionBar.selectTab(tabSpeed);
ft.commit();
}
// In the activity I also setup the ActionBar with tabs
private void setupActionBar() {
tabSpeed = actionBar.newTab();
// and a few more tabs...
tabSpeed.setTabListener(this);
actionBar.addTab(tabSpeed);
// But the tabs are not visible because of the navigation mode
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
}
// Here is the tabListener - very hacky!
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
if (fm == null)
fm = getFragmentManager();
Fragment f = null;
switch (tab.getPosition()) {
case 0:
if (fSpeed == null)
fSpeed = new FSpeed();
f = findFragment(fSpeed);
if (f != null) {
ft.setCustomAnimations(R.anim.fragment_alpha_0_1, R.anim.fragment_alpha_1_0);
ft.remove(f);
ft.add(R.id.fragment_container, fSpeed);
} else {
Log.d(TAG, "fSpeed is " + f);
return;
}
break;
case 1:
// more tabs - similar code
}
// the findFragment() method
private Fragment findFragment(Fragment selectedFragment) {
if (fm.findFragmentById(R.id.fragment_container) == fSpeed && selectedFragment != fSpeed)
return fSpeed;
// more ifs for other fragments
else
return null;
}
This all worked fine for the minute but after switching x times between the tabs things started looking funny, e.g. fList was visible, tab icons disappeared, ... I could track the bugs down and add some null checks but I think I went the wrong way. This is why I changed the TabListener with the following one:
// Still in activity
public static class MyTabListener<T extends Fragment> implements TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
/**
* * Constructor used each time a new tab is created.
* * * #param
* activity * The host Activity, used to instantiate the fragment
* * #param
* tag * The identifier tag for the fragment
* * #param clz * The
* fragment's Class, used to instantiate the fragment
*/
public MyTabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
/* The following are each of the ActionBar.TabListener callbacks */
public void onTabSelected(Tab tab, FragmentTransaction ft) {
// Check if the fragment is already initialized
if (mFragment == null) {
// If not, instantiate and add it to the activity
mFragment = Fragment.instantiate(mActivity, mClass.getName());
ft.add(R.id.fragment_container, mFragment, mTag);
} else {
// If it exists, simply attach it in order to show it
ft.attach(mFragment);
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (mFragment != null) {
ft.detach(mFragment);
}
}
// The tabs are now assigned to MyTabListener
tabSpeed.setTabListener(new MyTabListener<FSpeed>(this, "speed", FSpeed.class));
Here is where the problem started. After clicking on an item from the listview with the new TabListener onTabSelected and onTabUnSelected are called in turns endlessly.
Question:
My question is how do you go from a fragment with a listview to another fragment after clicking on an item of the listview (and still use the same activity). Is my own written method changeFragment() the wrong approach? Still I would like to use the MyTabListener.
Note 1: I bought Commonsware's book to learn more about fragments but I couldn't find a more complex example with different fragments working together and also couldn't find how the back button is handled, or overridden. For example after clicking on the back button if fragment1, 2 or 3 are visible always show fragment4. If anybody found one in the book could you please tell me the chapter (name)/ page? If there isn't it would be very kind of the Common guys to provide one in the next update or so.
Note 2: Global variables were used in the project like fSpeed, tabSpeed, ...
Note 3: If you need more code or explanation please let me know in the comments. Thanks for helping!
Is my own written method changeFragment() the wrong approach?
You are adding a fragment, not replacing one. To replace a fragment, use replace(), not add().
So basically, I have a simple Fragment that uses an AsyncTask to download a bitmap from a URL and then displays it inside an ImageView.
This is the Fragment's code:
public class TestFragment extends Fragment {
public TestFragment () {}
private String pictureUrl = "https://fbcdn-sphotos-d-a.akamaihd.net/hphotos-ak-prn1/532762_10150739418088211_1186720820_n.jpg";
private TextView sampleText;
private ImageView sampleImage;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_view, container, false);
String textToSet = "Section Three";
sampleText = (TextView) root.findViewById(R.id.textView1);
sampleText.setText(textToSet);
sampleImage = (ImageView) root.findViewById(R.id.imageView1);
DownloadImageTask task = new DownloadImageTask(pictureUrl, root, sampleImage.getId());
task.execute();
return root;
}
}
This same fragment is displayed three times, one per each tab inside an ActionBar. The problem I am having is that each time the user selects a different tab, the previous one is destroyed, and when the user navigates back to the original tab, the content needs to be redownloaded. I want to make sure that the least amount of resources are consumed (saving both Data, and valuable overHead processing).
Here is the Fragment Container activity:
public class LayoutContainer extends Activity{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout_container);
if (savedInstanceState != null) {
return;
}
final ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
Tab tab = actionBar.newTab()
.setText(R.string.title_section1)
.setTabListener(new TabListener<TestFragment>(
this, "one", TestFragment.class));
actionBar.addTab(tab);
tab = actionBar.newTab()
.setText(R.string.title_section2)
.setTabListener(new TabListener<TestFragment>(
this, "two", TestFragment.class));
actionBar.addTab(tab);
tab = actionBar.newTab()
.setText(R.string.title_section3)
.setTabListener(new TabListener<TestFragment>(
this, "three", TestFragment.class));
actionBar.addTab(tab);
}
public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
private Fragment mFragment;
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
public TabListener(Activity activity, String tag, Class<T> clz) {
mActivity = activity;
mTag = tag;
mClass = clz;
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if (mFragment == null) {
mFragment = Fragment.instantiate(mActivity, mClass.getName());
}
ft.replace(android.R.id.content, mFragment, mTag);
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
ft.addToBackStack(null);
ft.commit();
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
}
I currently have it set to call ft.replace() on the OnTabSelected() method, but I had previously tried with ft.add(). Same goes for my current ft.addToBackStack() call inside OnTabUnselected(), I initially had it set for simply calling ft.detach() but that was doing the same thing I described as being my problem. It loads that way, but it has to download the image every single time.
Now, I have tried calling addToBackStack() but the app force closes stating the FragmentTransaction CANNOT be added to the back stack. I also tried in the past to cache the image offline, to avoid data consumption, the app would still need to parse the image from a local URI and use processing power.
Any ideas I can try?
FYI, Yes, I know that the same fragment is loading the same image on all three tabs. This is merely a test project that is intended to help me better understand the lifecycle of Fragments.
So After a lot of reading I realized that the important code block is not done on your TabListener but in fact done on each Fragment that you use.
The fragment WILL be detached when navigating away from it, but that does not mean I cannot retain it state using onSaveInstanceState(...).
Here is simple implementation of what was needed to retain the Fragment's state after it being detached.
public class TimelineFragment extends ListFragment{
...
public TimelineFragment () {}
public void onSaveInstanceState(Bundle outState){
getActivity().getFragmentManager().putFragment(outState, TAG, this);
}
public void onRetoreInstanceState(Bundle inState){
getActivity().getFragmentManager().getFragment(inState, TAG);
}
...
}
We are saving the Fragment by calling putFragment(), and restoring it by calling getFragment(). Pretty darn simple, and I never thought of it.
What I do when a user need an image from the web I first check if I've cached it or the image is at the SD. If the image can't be found at SD or cache I download the image, store it to SD and add it to the cache.
It usually goes something like this:
Bitmap cacheBitmap = Database.getImageFromMemory(exercise.image, getActivity());
if(cacheBitmap != null) {
image.setImageBitmap(cacheBitmap);
progressBar.setVisibility(View.GONE);
}
Bitmap localBitmap = ImageDownloader.decodeSampledBitmap(exercise.image, width, height, getActivity());
if(localBitmap != null) {
image.setImageBitmap(localBitmap);
progressBar.setVisibility(View.GONE);
Database.addBitmapToMemoryCache(exercise.image, localBitmap, getActivity());
} else {
ImageDownloader imageDownloader = new ImageDownloader(this.getActivity().getApplicationContext(), image, progressBar, width, height);
imageDownloader.execute(exercise.image);
}
Now this is not all the code but it gives you an idea about how to handle it. If you need more code feel free to ask, but it seems like you are on the right track.
The Problem
Before I describe my current setup I will describe my problem. I need to have hierarchal navigation between fragments inside separate tabs. Say I drill down on level in a tab, I then switch tabs and then go back to the first, original tab, I need it to be on the drilled down level and then have the ability to go back up to the top level of that tab.
Now onto my setup.
I have an application with one activity called MainActivity. This activity is what controls my tabbed action bar. I have followed many different tutorials to handle the switching of Fragments when a tab is selected and here is the code below that works drawing your attention to the onTabSelected and onTabUnselected methods.
MainActivity -- Tab Listener
public static class MyTabListener<T extends Fragment> implements ActionBar.TabListener {
private final Activity mActivity;
private final String mTag;
private final Class<T> mClass;
private final Bundle mArgs;
private Fragment mFragment;
public MyTabListener(Activity activity, String tag, Class<T> clz) {
this(activity, tag, clz, null);
}
public MyTabListener(Activity activity, String tag, Class<T> clz, Bundle args) {
mActivity = activity;
mTag = tag;
mClass = clz;
mArgs = args;
// Check to see if we already have a fragment for this tab, probably
// from a previously saved state. If so, deactivate it, because our
// initial state is that a tab isn't shown.
mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
if (mFragment != null && !mFragment.isDetached()) {
FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
ft.detach(mFragment);
ft.commit();
}
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
Log.e("FRAGMENT","Selected Fragment With Tag " + mTag);
if (mFragment == null) {
mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
ft.add(android.R.id.content, mFragment, mTag);
} else {
ft.attach(mFragment);
}
}
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (mFragment != null) {
ft.detach(mFragment);
}
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT).show();
}
}
When I am on Tab1 viewing Fragment1', when a button is pressed I then show anotherFragmentcalledFragment2` which I need the ability to go back from. The code below works if I just stay on that tab however when navigating away from the tab and back to it, everything messes up.
MainActivity -- Showing second fragment
Fragment tdFrag = Fragment.instantiate(this, SecondFragment.class.getName());
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(android.R.id.content, tdFrag, "secondfrag");
ft.addToBackStack(null);
ft.commit();
My Thoughts
I think it must be the way I detach the fragments and handle the switching over when a new tab is pressed. Maybe I need to implement some way of storing each individual backstack for each tab and loading the fragments from that instead of the system back stack?
Any help would be great. I have tried to explain it as thoroughly and simply as possible.
Thanks in advance.
Ok so after a bit of experimentation I decided to implement a custom backstack using enumerated types.
I have two types of enumerated types: a ViewType and CurrentTab type. The CurrentTab type tells me what tab I'm currently viewing and the ViewType tells me what view is being viewed in these tabs. I have an instance of a ViewType for each tab I have to store where I am in the hieerarchy.
To implement a backstack I override the method thats called when the back button is pressed.
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
return true;
}
return super.onKeyDone(keyCode,event);
}
So in where it checks for the back button I put my code to check the current tab and the separate view types. That way I know where I am and if I push back I know where I need to go.
I hope this helps any questions just post a comment.