Shared element transition : activity into fragment nested in another activity - android

i am trying to add shared element transition into my app.
Scenario is that user clicks on image thumbnail which than opens another activity with full screen image view.
This works fine if shared view is hosted directly within layout of target activity. Works smoothy for enter/exit animation.
But when i'am trying to achieve similar effect within fragment which is nested in target activity this approach doesn't work. Funny thing is that enter animation is not showed, but exit animation is working fine .
Another even more complicated view hierarchy is that if target view (ImageView) is hosted within view pager which is hosted in frame layout of target activity.
Does someone had same issue ?
Edit:
My click listener code
public class OnClickPicture extends OnClickBase {
private ObjectPicture object;
public OnClickPicture(Activity_Parent activity, ObjectPicture object) {
super(activity);
this.object = object;
}
public void onClick(View v) {
picasso.load(object.getFullUrl()).fetch();
Intent intent = new Intent(activity, ActivityPicture.class);
intent.putExtra("picture_object", helper.gson.toJson(object));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && v != null) {
Pair<View, String> p1 = Pair.create(v, "image");
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, p1);
activity.startActivity(intent, options.toBundle());
} else {
activity.startActivity(intent);
}
}
}

The way that transitions work require the new Activity to be created, measured and laid out before any animations can happen. That's so that it can find the view that you want to animate and create the appropriate animation.
In your case this isn't happening because, as stated in the docs, all FragmentTransaction.commit() does is schedule work to be done. It doesn't happen immediately. Therefore when the framework creates your Activity it cant find the view that you want to animate. That's why you don't see an entry animation but you do see an exit animation. The View is there when you leave the activity.
The solution is simple enough. First of all you can try FragmentManager.executePendingTransactions(). That still might not be enough. The transitions framework has another solution:
In the onCreate of Activity postponeEnterTransition(). This tells the framework to wait until you tell it that its safe to create the animation. That does mean that you need to tell it that its safe (via calling startPostponedEnterTransition()) at some point. In your case that would probably be in the Fragments onCreateView.
Here's an example of how that might look like:
Activity B
#Override
protected void onCreate(Bundle savedInstanceState) {
// etc
postponeEnterTransition();
}
Fragment B
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View sharedView = root.findViewById(R.id.shared_view);
sharedview.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
sharedview.getViewTreeObserver().removeOnPreDrawListener(this);
getActivity().startPostponedEnterTransition();
return true;
}
});
}
Thanks to Alex Lockwood for his detailed blog posts about the Transitions framework.

Related

Exit transition does not work for an ImageView Shared Element in a ViewPager in a Fragment

In my app, I have an Activity launching another Activity with a Fragment in it, that contains a ViewPager of images. What I have working currently, is the enter transition where the first Activity launches the second and the transition is correct. This works because, in my ViewPager I put a OnPreDrawListener on it and only resume the activity transition when the image in the pager is loaded. It looks like this:
public class ImagePagerAdapter extends PagerAdapter {
// Constructor and other things..
#Override
public Object instantiateItem(ViewGroup container, final int position) {
ImageView imageView;
if (position == 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Shared element is the first one.
ViewCompat.setTransitionName(imageView, "sharedImage");
}
}
imageView = new ImageView(activity);
// Just a reusable static Helper class.
HelperPicasso.loadImage(images.get(position), imageView, false, new Callback() {
#Override
public void onSuccess() {
imageView.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
imageView.getViewTreeObserver()
.removeOnPreDrawListener(this);
// When the ImageView is ready to be drawn, we can continue our activity/fragment's postponed transition animation.
// Why? Because we want have the first image be the shared element, and we can only set it after instantiation.
ActivityCompat.startPostponedEnterTransition(activity);
return true;
}
});
}
#Override
public void onError() {
ActivityCompat.startPostponedEnterTransition(activity);
}
});
}
}
Aside from the ImageView, I also have a FrameLayout which is a shared element as well, but I mark it with it's transition name in the onCreateView of the fragment.
With this the enter transition works well for me. However, when I press the back button, the FrameLayout's exit transitions works correctly, but the ViewPager image goes blank.
My guess is that the fragment's lifecycle causes the ViewPager (and it's child views) to be destroyed during the exit transition.
I've tried adding ActivityCompat.finishAfterTransition(this) in the onBackPressed callback for the parent Activity, but it doesn't seem to have any effect.
When you have a ViewPager in the middle of a transition then you have to do some extra work in order to have a fluid and "beautiful" transition. You must play with postponeTransition() and startPostponedTransition() in order to play the transitions only when fragments or images finished to load. (It seems that you are already doing it). I recommend you to you to check the next blog: Shared Element Transitions - Part 4: RecyclerView
The target of that article is more RecyclerView + ViewAdapter + Fragments transitions but I´m sure you can adapt it in your scenario. Hope it helps.

Android: Prevent fragment from becoming "active" while replacing another

I have the following invocations:
context.getSupportFragmentManager().popBackStack(tag.name(), 1);
context.getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, tag.name()).addToBackStack(tag.name()).commit();
...while I have at least 2 other fragments on the backstack that were opened before. When these two commands will be executed and the latest fragment has been popped off the backstack, for a very short period of time, the fragment before this fragment is going to be active before the popped fragment has been replace by the given one. And that's the problem, because this fragment fetches data from server and is displaying a progress dialog. Sometimes this leads to race conditions and strange behaviour.
How can I prevent the "non-active" fragments from becoming active while replacing another fragment?
Here is a short explanation of the situation:
MainActivity -> opens Fragment1 -> opens Fragment2a -> opens EditActivity -> after "save action", Fragment2a will be popped and a new Fragment2b will be added into the fragment_container of the MainActivity. While this happens, Fragment1 is doing things, but it must not do this. I want to prevent Fragment1 to do any tasks. It should somehow just stay in background and "sleep".
What about using observer pattern? You create an interface with a method to set fragments to busy starte or do some logic.
All fragments implement this interfac and you create a list that contains these interface inside Activity. If a fragment is registered add this fragments to list or remove them with unregister methods for example.
(MyActivity)getActivity.register(this); can be called to register a fragment. Or you can call a method like in active fragment if you wish to set other fragments except this one as busy (MyActivity)getActivity.setActive(this) and inside MyActivity you can declare this method as
public void setActive(IStateController fragment) {
for(IStateController f: listOfSubscribers) {
if(f == fragment) {
// This fragment called the method and should active
// other fragments can be set to busy or waiting state
}
}
}
I really can't say if it works for you but interacting with fragments without being aware of each other can be done this way, or you can check EventBus library.
Just check
if(getActivity() == null || !isAdded){
return; //don't do your code that touches UI if it is not active.
}
but make sure you have your onPause remove your busy indicator then if you don't plan to wait for completion or you will create a memory leak of window leaking.
I'm not sure if I misuse the fragment manager concept so that this situation can occur or if the fragment manager concept is a total crap (like a lot in Android), but I solved it by using some workaround. When I start or replace a new fragment, I store immediately an ID in the application context. When a Fragment is getting started, it checks if is has the correct id. If not, I return a null view and the fragment won't be created. The check looks like this:
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState)
{
return FragmentCreationHelper.createFragmentView(inflater, container, TAG, getFragmentId(), R.layout.fragment_mycoolview);
}
The helper class...
public class FragmentCreationHelper
{
public static View createFragmentView(LayoutInflater inflater, ViewGroup container, String loggingTag, FragmentTag fragmentId, int template)
{
Log.d(loggingTag, "onCreateView()");
if (MyContext.getNextVisibleFragment() == null || fragmentId.equals(MyContext.getNextVisibleFragment()))
{
Log.d(loggingTag, "inflating view...");
return inflater.inflate(template, container, false);
}
else
{
Log.d(loggingTag, "Skipping view inflation. Fragment should not be displayed.");
return null;
}
}
}

Preload some fragment when the app starts

I have an Android application with a navigation drawer. My problem is that some fragment takes few second to load (parser, Map API). I would like to load all my fragment when the app starts.
I'm not sure if it is possible or a good way to do it, but I was thinking of create an instance of each of my fragments in the onCreate method of the main activity. Then, when the user select a fragment in the navigation drawer, I use the existing instance instead of creating a new one.
The problem is that it does not prevent lag the first time I show a specific fragment. In my opinion, the reason is that the fragment constructor does not do a lot of operation.
After searching the web, I can't find an elegant way to "preload" fragment when the application starts (and not when the user select an item in the drawer).
Some post talks about AsyncTask, but it looks like MapFragment operation can't be executed except in the main thread (I got an exception when I try: java.lang.IllegalStateException: Not on the main thread).
here is what I've tried so far:
mFragments = new Fragment[BasicFragment.FRAGMENT_NUMBER];
mFragments[BasicFragment.HOMEFRAGMENT_ID] = new HomeFragment();
mFragments[BasicFragment.CAFEFRAGMENT_ID] = new CafeFragment();
mFragments[BasicFragment.SERVICEFRAGMENT_ID] = new ServiceFragment();
mFragments[BasicFragment.GOOGLEMAPFRAGMENT_ID] = new GoogleMapFragment();
When an item is selected in the nav drawer:
private void selectItem(int position) {
Fragment fragment = mFragments[position];
// here, I check if the fragment is null and instanciate it if needed
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.commit();
mDrawerList.setItemChecked(position,true);
mDrawerLayout.closeDrawer(mDrawerList);
}
I also tried this solution; it allows to prevent a fragment from being loaded twice (or more), but it does not prevent my app from lag the first time I show it. That's why I try to load all fragments when the application starts (using a splash-screen or something) in order to prevent further lags.
Thanks for your help / suggestion.
You can put your fragments in ViewPager. It preloads 2 pages(fragments) by default. Also you can increase the number of preloaded pages(fragments)
mViewPager.setOffscreenPageLimit(int numberOfPreloadedPages);
However, you will need to rewrite your showFragment method and rewrite back stack logic.
One thing you can do is load the resources in a UI-less fragment by returning null in in Fragment#onCreateView(). You can also call Fragment#setRetainInstance(true) in order to prevent the fragment from being destroyed.
This can be added to the FragmentManager in Activity#onCreate(). From there, Fragments that you add can hook in to this resource fragment to get the resources they need.
So something like this:
public class ResourceFragment extends Fragment {
public static final String TAG = "resourceFragment";
private Bitmap mExtremelyLargeBitmap = null;
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return null;
}
#Override
public void onStart() {
super.onStart();
new BitmapLoader().execute();
}
public Bitmap getExtremelyLargeBitmap() {
return mExtremelyLargeBitmap;
}
private class BitmapLoader extends AsyncTask<Void, Void, Bitmap> {
#Override
protected Bitmap doInBackground(Void... params) {
return decodeBitmapMethod();
}
#Override
protected void onPostExecute(Bitmap result) {
mExtremelyLargeBitmap = result;
}
}
}
Add it to the fragment manager in the Activity first thing. Then, whenever you load your other Fragments, they merely have to get the resource fragment from the fragment manager like so:
public class FakeFragment extends Fragment {
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final ResourceFragment resFragment = getFragmentManager().findFragmentByTag(ResourceFragment.TAG);
Bitmap largeBitmap = resFragment.getBitmap();
if (largeBitmap != null) {
// Do something with it.
}
}
}
You will probably have to make a "register/unregister" listener set up because you will still need to wait until the resources are loaded, but you can start loading resources as soon as possible without creating a bunch of fragments at first.
To preload fragments, attach() can be used. So in OP's case it will be:
ft.attach(fragment).commit();
Make sure to store the fragment somewhere and use that one the next time ft.replace() is called.

adding multiple layouts to horizontal swipe view

I have added a horizontal scroll view and in the layout I have added reference to two fragments such that both the fragments are visible in the scroll.
the problem is when I enter values in the left layout I want the value to be displayed in the right layout.
I am using an interface for the same, but its not working. as in I keep getting null pointer exception for the same.. How to get around this??
this is the main class fragment
public static SliderRightAdapter sliderRightAdapterFragment; <-- this calls the second layout
#Override
public View onCreateView(LayoutInflater inflater,
#Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
// TODO Auto-generated method stub
View view = inflater.inflate(R.layout.f_pp, container, false);
sliderRightAdapterFragment = (SliderRightAdapter) getActivity()
.getSupportFragmentManager().findFragmentById(
R.id.secondary_sliderProduct_description);
return view;
}
.
.
.
.
this is where i am getting null pointer exception on sliderRightAdapterfragment
interfaceObject.onButtonClick(CGS, sliderRightAdapterFragment);
and this is where it is initialized...
sliderRightAdapterFragment = PDFinterface.sliderRightAdapterFragment;
From what I'm seeing, you're letting the fragments communicate directly with each other.
I'd recommend against it, since this can quickly lead to maintaining unused references, as fragments can have quite diverse lifecycles. The idea behind fragments is to have small segments of functionality which can communicate with their parent some information.
I think a better way to approach would be to use the parent activity as a form of 'manager' between these two fragments.
Instead of maintaining a static reference to the other fragment, store a normal reference to the activity, preferably in the form of a listener interface the activity implements. Then you'd use the findFragmentById/Tag to notify the other fragment of the change. All in all, this would look something like this:
Fragment:
public interface ActivityListener {
void onText(String text);
}
private ActivityListener listener;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if(activity instanceof ActivityListener) {
listener = (ActivityListener) activity;
}
// else some error
}
#Override
public void onDetach(Activity activity) {
// to make absolutely sure you don't maintain a reference to a killed activity
listener = null;
}
Activity:
#Override
public void onText(String text) {
Fragment frag = getSupportFragmentManager.findFragmentById(<someId>);
// notify with text information
}
Something like this should work fine.

Handling orientation changes with Fragments

I'm currently testing my app with a multipane Fragment-ised view using the HC compatibility package, and having a lot of difficultly handling orientation changes.
My Host activity has 2 panes in landscape (menuFrame and contentFrame), and only menuFrame in portrait, to which appropriate fragments are loaded. If I have something in both panes, but then change the orientation to portrait I get a NPE as it tries to load views in the fragment which would be in the (non-existent) contentFrame. Using the setRetainState() method in the content fragment didn't work. How can I sort this out to prevent the system loading a fragment that won't be shown?
Many thanks!
It seems that the onCreateViewMethod was causing issues; it must return null if the container is null:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (container == null) // must put this in
return null;
return inflater.inflate(R.layout.<layout>, container, false);
}
Probably not the ideal answer but if you have contentFrame for portrait and in your activity only load up the menuFrame when the savedInstanceState is null then your content frame fragments will be shown on an orientation change.
Not ideal though as then if you hit the back button (as many times as necessary) then you'll never see the menu fragment as it wasn't loaded into contentFrame.
It is a shame that the FragmentLayout API demos doesn't preserve the right fragment state across an orientation change. Regardless, having thought about this problem a fair bit, and tried out various things, I'm not sure that there is a straightforward answer. The best answer that I have come up with so far (not tested) is to have the same layout in portrait and landscape but hide the menuFrame when there is something in the detailsFrame. Similarly show it, and hide frameLayout when the latter is empty.
Create new Instance only for First Time.
This does the trick:
Create a new Instance of Fragment when the
activity start for the first time else reuse the old fragment.
How can you do this?
FragmentManager is the key
Here is the code snippet:
if(savedInstanceState==null) {
userFragment = UserNameFragment.newInstance();
fragmentManager.beginTransaction().add(R.id.profile, userFragment, "TAG").commit();
}
else {
userFragment = fragmentManager.findFragmentByTag("TAG");
}
Save data on the fragment side
If your fragment has EditText, TextViews or any other class variables
which you want to save while orientation change. Save it
onSaveInstanceState() and Retrieve them in onCreateView() method
Here is the code snippet:
// Saving State
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("USER_NAME", username.getText().toString());
outState.putString("PASSWORD", password.getText().toString());
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.user_name_fragment, parent, false);
username = (EditText) view.findViewById(R.id.username);
password = (EditText) view.findViewById(R.id.password);
// Retriving value
if (savedInstanceState != null) {
username.setText(savedInstanceState.getString("USER_NAME"));
password.setText(savedInstanceState.getString("PASSWORD"));
}
return view;
}
You can see the full working code HERE

Categories

Resources