TL;DR:
How should multi-pane apps with deep navigation similar to the Spotify iPad app look and work on Android, and how to implement this?
Long version:
I'm working on an app, where the user sees lists of items and can then delve deeper into these items. These item detail pages can again open lists of related items, that in turn have detail pages and so on. As a phone app, these would be separate Activities that might look and link to each other like this:
In the mock-ups, the user sees an initial overview and then selects "Item #2" from the first list. A new Activity opens up, showing him details for Item #2. Here, he selects to see a list of Things relating to Item #2. The newly openend Activity in the third picture shows this list, and clicking on one opens the details for this thing. He can navigate as deep into the content as he likes.
This works quite well with the usual Android Activities. I'm working on bringing the app to tablets and am thinking on how to best implement this. The plan is to create a multi-pane layout with the same concept. It is very similar to how the iPad Spotify app works (it will be interesting to see how they bring this to Android once they create tablet-specific layouts).
In the tablet layout, each click on an item or list name opens the corresponding child item as a new pane that animates in from the right. The same workflow as in the example above would look like this:
I'm unsure how to best implement this navigation pattern. Multi-pane apps with a limited navigational depth like GMail can be built with a static ViewGroup (LinearLayout would be ok) containing all fragments, and going deeper into the navigation replaces the content of the next container to the right and animates to this (see CommonWares implementation of this on SO).
This suggests that a custom ViewGroup would be the way to go. If it has to display a subpage (i.e. "List of Things"), then it creates a new child in the ViewGroup that is half as wide the screen with the fragment and then scrolls the visible area so that the pane that was just interacted with and the new child are visible. To link this correctly to a FragmentTransaction, so that the back stack works correctly, I'd guess it would be something like this:
View newPane = container.addChild();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(newPane, new ListOfThingsFragment(2));
ft.remove(paneOnRight, fragmentOnRight);
ft.commit();
container.animateToRight();
I don't see a way to do the animation within the FragmentTransaction.
Feedback welcome. My employer is generally favorable with respect to open sourcing frameworks we develop, so if this is something that is of broader interest and if I can come up with a reusable solution, I'd be glad to share it.
I had some research time and came up with a solution to this question (a question that I've wanted to see the solution for LONG time, even before you asked it).
I can't really show the whole code as there's some IP boundaries, but I'll put it here the main parts for this animation to works.
There're two key tools: setCustomAnimations and LayoutTransition
Yes, as far as I've been able to do it, you need to separate set animations to make it work.
So let's get to some code, you'll define your XML with a horizontal LinearLayout and make sure to include the following line on it.
android:animateLayoutChanges="true"
this will auto-generate a standard LayoutTransition which does translate the fragment/view that is staying in the layout and alpha (in or out) the fragment/view that is being included or removed from the layout. Give it a try.
So after this layout is inflated we gonna capture this LayoutTransition and trick it out to our needs:
LayoutTransition lt = myLinearLayout.getLayoutTransition();
lt.setAnimator(LayoutTransition.APPEARING, null);
lt.setAnimator(LayoutTransition.DISAPPEARING, null);
lt.setStartDelay(LayoutTransition.CHANGE_APPEARING, 0);
lt.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
with that code, we're removing the alpha animations and removing any delay from the transition (because we want all the translations to fire together).
And now it's just a few simple fragment transactions to make it work, during initialisation we inflate that layout and put a few fragments on it:
setContentView(R.layout.main); // the layout with that Linear Layout
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.main, frag1, FRAG_1_TAG); // it's good to have tags so you can find them later
ft.add(R.id.main, frag2, FRAG_2_TAG);
ft.add(R.id.main, frag3, FRAG_3_TAG);
ft.hide(frag3);
ft.commit();
now on the transaction it's a simple:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.push_left_in, R.anim.push_left_out, R.anim.push_right_in, R.anim.push_right_out);
Fragment left = getFragmentManager().findFragmentByTag(FRAG_1_TAG);
Fragment right = getFragmentManager().findFragmentByTag(FRAG_3_TAG);
ft.hide(left);
ft.show(right);
ft.addToBackStack(null);
ft.commit();
final notes:
to make deeper navigation it's just a matter of firing FragmentTransactions to add fragments to the LinearLayout and hide or detach the left side fragment.
Also to make the fragments work on the linear layout is important to set their LinearLayout.LayoutParams.weight during runtime, something similar to the following code applied to the fragment view
((LinearLayout.LayoutParams) view.getLayoutParams()).weight = 1;
to make it work on phones as well it's just a matter of applying the common multiple screen support patterns.
last note, be careful on proper managing the layout status during device rotation because it's not all automagically handled by the system.
Happy coding!
We ran into the same problem with our app. The constraints we gave ourselves:
Dynamic numbers of panes
Each pane can be differently sized
Fragments inside of panes must be correctly retained on orientation changes.
In light of those constraints, we built a new layout we call PanesLayout. You can check it out here:
https://github.com/cricklet/Android-PanesLibrary
It basically allows you to easily add any number of dynamically sized panes and attach fragments to those panes. Hope you find it useful! :)
Partial answer to the animation part:
You can do animations with the FragmentTransaction:
ft.setCustomAnimations(android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
Update: see this answer from Reto Meier himself about fragment animation: https://stackoverflow.com/a/4819665/1007169
Related
I would like to create a good base for my app and have it working efficiently. I am a bit lost still with understanding the relationships between fragments and activities (in the basic form I get it, but in cases like the one I need to deal with now I am not sure what to do).
Currently I have 3 fragments in a View Pager, very simple stuff. one of them is for gallery, one of them is for profile and one of them is for forum (imagine the question board here on SO).
Now, inside the forum fragment I have a FAB that should open a new page for asking a new question.
What I am wondering about is, should the new question page be a fragment or an activity? Also, when a question is clicked I would like its thread to open with all the comments. That should be a fragment as well correct?
If fragment, where do I load it into? The View Pager? If so how? Can the view pager host a fragment temporarily? Because the fragment should only be visible when the button has been clicked. Should I place the fragments inside of the forum fragment so it'll serve as it's parent fragment?
I can make it as an activity, but by what I read and know about fragments, it makes sense that it should be a fragment because it is part of the same activity, but when that is that case I am not sure how to do it.
I would actually not use a ViewPager in your case. I use ViewPagers for a series of related screens that deal with a very specific task. For example - a wizard that details instructions where each Fragment might represent a step and it's useful for the user to be able to slide back and forth between instructions.
Given your requirements I would still host everything under one Activity and the individual pages as Fragments inside that activity. Then maybe use something like a bottom nav bar to navigate between the profile, gallery and forum. The screen that is launched by the FAB and another one for viewing the thread of a forum post should be also be separate Fragments as well.
This might be a good opportunity for you to try out the navigation framework under Jetpack. It makes these kinds of things a lot easier to visualize.
See here Android Navigation Framework
I'm using tabs to switch pages in a container view. This works smoothly until I fill these pages with different kind of layouts or objects. The performance drops and it gets lower frames per second (no project butter here at all :D).
Can I use something else than fragments for this? Also if I want to keep the information (lets say I input my name on page 1) even when I have switched to another page?
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_FADE );
transaction.replace( R.id.container, new FirstFragment() );
transaction.commit();
Each button in my tabsholder has its own fragment to change to.
What is making my performance drop? Is it not removing views? What should I use instead?
This is a rather wide question but I wouldn't know where to turn.
I am trying to create a layout in an activity that will look different in landscape and portrait, however it will contain the same sections, just in different places on the screen.
Most examples I find on fragments is always the list and detail, which is not what I'm looking at.
In my example I have, amongst others,
a scrolling image section (carousel)
a page title with some brief details
a carousel of thumbnails
two buttons
full details of the page
Now in the landscape layout, the image section will always be on the left side with down the right side, the page title, carousel of thumbs, two buttons and full details.
In the Portrait the title will appear at top, with image carousel beneath, then thumbs, then buttons etc.
The way it works in my head, is that each section should be a fragment, and then depending on the layout file in the respective res/layout folder corresponding to land/port, the fragments are arranged accordingly. But I'm also thinking is each section a whole fragment? Or could it be a separate view that get's loaded in, but just in a different order depending on the screen orientation.
I hope that made sense?
Does anyone have any good tutorials that explain exactly when you should and shouldn't use fragments. All the usual suspects just list the list/detail example, which is not applicable in my case.
tl;dr Can I use fragments as modules/blocks in various layouts or should I just create other layouts and display them inside the main layouts.
Fragments can be used to have split screen and to store previous state unlike views. It is difficult to manage back button with views
If you just want to change how the screen is laid out, the answer is neither. You can define orientation-specific resources (either a separate layout file for each orientation or different dimensions/constraints for each orientation) and the system will just provide you with the correct resource set for the current orientation.
That said, it sounds like you may be describing a modified version of master/detail flow. If this is the case, fragments are a good way to go since the landscape view is actually several separate screens in the portrait version of the app, so each section needs it's own state and lifecycle, which fragments provide.
Now, I know you said you wanted an example beyond a list and detail view, so here's some more details on when to use fragments:
Fragment, like activities, have state and lifecycle. Custom views do not have lifecycle and are completely dependent on the activity or fragment containing them.
You might use a custom view when you have a widget on screen that is used in multiple places and is just like any other view – bound to the activity when the layout is inflated and controlled from there. It's a way to either reduce duplicating combinations of views in your layout or to draw a custom view that doesn't exist yet.
Fragments are good when you need some state or lifecycle for a section of the app that might get used in multiple places or shouldn't be logically connected to the activity it is contained in. If you use the new Navigation Component, you actually just define one activity and then each screen in the navigation tree is a fragment that gets swapped out as the user navigates around the app. Here each child component on the screen (each "screen" that the user navigates to) has it's own lifecycle, business logic, etc, so mixing the code for all of that in the activity wouldn't make sense.
So the question comes down to what you are trying to build, and this may be a case where the best way to learn the difference is to try each option out as bit. The differences become more clear with practice using them. As a general rule of thumb, personally, I only really use custom views when I am trying to make a new view that doesn't exist elsewhere. If what I'm trying to do is simply a matter of laying out existing views in a new way, the answer is probably some trick in the layout file or layout code inside the activity. If I'm trying to make a stand-alone piece of the app that does stuff, especially if it also appears in multiple places in the app, I'll probably be building a fragment.
Im relatively new to android so im trying my hand at what i though would be an easy-ish app but I've ran into an issue to do with view/activity flow that i cant get an understanding on.
i have a fragmentActivity that uses a Viewpager to create tabs, each of those tabs is its own fragment class, thats all fine and working, but now i need to have one of the tabs display a list, when selected it takes you to another "view", my problem is how to create the first list and then how to handle tha clicking of an item in that list to take you to the new view so that the tabs stay in place and the back button doesn't exit the app.
currently ive swapped out the fragment with a list fragment that uses an arrayAdapter to build itself, this has worked as far as the list goes but i cant for the life of me figure out how to utelise its onclick() method to move on the the next screen, without as i said losing the tabs or having the back button simply exit.
so im not sure if A, the list fragment is the way to go, or B if it is how to move on to a new screen correctly
i can post code if needed but its a very general implementation of the classes mentioned so im not sure code will help
The callback you want to handle a click on an item in the list is onListItemClick.
As far as presenting a new screen, you can use a FragmentTransaction to replace your fragment with a new one, which gets a little hairy if you're doing this inside of a ViewPager. The code would look something like this:
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.some_containging_view, new SomeFragment())
.addToBackStack(null).commit();
Otherwise, you could simply launch a new Activity, which is simpler, but will mean that the tabs won't be present on the new screen unless you duplicate them there.
An elegant solution was finally found here, it uses a wrapper fragment around my list fragment so the pager is just concerned with the wrapper, meanwhile within the wrapper i can perform fragment transactions to my hearts desire, one small note the linked code uses getChildFragmentManager() that needs to be changed to regular fragmentmanager() from support.app for the backstack stuff to work.
in my Activity, I have a layout containing 3 FrameLayouts, one at the top, one at the left and one at the "center".
Now, I sometimes only want to display one or two of them. Atm I am doing it this way:
FrameLayout frame = (FrameLayout) findViewById(R.id.framelayout_menu_left);
frame.setVisibility(...);
frame = (FrameLayout) findViewById(R.id.framelayout_content);
frame.setVisibility(...);
frame = (FrameLayout) findViewById(R.id.framelayout_menu_top);
frame.setVisibility(...);
However this can get really ugly results, e.g. when I switch the "content" Fragment and hide the top and/or left FrameLayout. It all starts flickering as the "content" Fragment jumps to the top and/or left and only afterwards is replaced.
Also, I can obviously not navigate back to another setup, so is there any other way to do this?
Kind regards,
jellyfish
Edit:
Maybe a little drawing makes my question clearer...
A shows a Layout of 3 FrameLayouts containing 3 different Fragments. Each color represents one distinct Fragment.
Now what I want to do is to switch from A to D.
I am doing this by replacing the blue Fragment with the yellow Fragment via a FragmentTransaction.
However, this still keeps the other Frames visible, so I hide them via the code above.
Now, Frame.setVisibility() is called way before commit(), so in B and C the blue Fragment "jumps" to the left and the top and only afterwards (in D) is replaced with the yellow Fragment. This produces a nasty flickering.
As a workaround, I now hide all three FrameLayouts before the transaction and re-show the ones I need once the transaction has finished. But there still is the problem that I can't go back via the back button as this isn't a real transaction.
I would have two suggestions. Firstly, if you both add a fragment transition effect and do the visibility changes after the transaction, that would probably substantially reduce much of your flicker effect
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
Secondly, I've simply given up on having the system manage the fragment stack for me -- it seems that this only works well with simple transactions. Override onBackPressed and do your own logic there.
--randy