I'm working on an app that currently uses a content transition on an ImageView from one fragment to another on the same activity. It is working fine however I have realised that my destination fragment needs to have it's own activity.
So let's say i have Activity A which contains Fragment 1
And I have Activity B which contains Fragment 2.
I need to perform a shared element transition from Fragment 1 to Fragment 2.
Here is what i have done so far:
In the callback method from Fragment 1 to Activity A I'm passing the selected entity and also the imageview i want to transition from.
Activity A
#Override
public void OnPhotographSelected(Photograph selectedPhoto,ImageView image) {
Intent i= new Intent(this, PhotoDetailActivity.class);
i.putExtra("photo_OBJ", selectedPhoto);
i.putExtra("transitionName", image.getTransitionName());
startActivity(i, ActivityOptions.makeSceneTransitionAnimation(this, image, "mainPhoto").toBundle());
}
Activity B
#Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo_detail);
Photograph photoObj=new Photograph();
Bundle b = getIntent().getExtras();
String transitionName="";
if(b!=null)
{
photoObj=(Photograph)b.getSerializable("photo_OBJ");
transitionName=b.getString("transitionName");
}
PhotoDetailFragment pdf = PhotoDetailFragment.newInstance(photoObj);
pdf.setSharedElementReturnTransition(TransitionInflater.from(this).inflateTransition(R.transition.change_image_transform));
pdf.setSharedElementEnterTransition(TransitionInflater.from(this).inflateTransition(R.transition.change_image_transform));
pdf.setImageTransitionId(transitionName);
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.photo_detail_content, pdf);
trans.commit();
}
Fragment 2
mainImg.setTransitionName(mImageTransitionID);
Activity Theme
<item name="android:windowActivityTransitions">true</item>
I'm not seeing any content transition occur at runtime. As i mentioned earlier I had this transition working correctly from fragment to fragment within the same activity.
Also worth noting is that Fragment 1 is a gridview so i have to maintain the transitionNames myself so they are all unique, that why you are seeing setTransitionName calls at runtime.
Any idea's why i'm not see the transition run?
Try to use postponeEnterTransition() in your second activity inside onCreate() and yourActivity.startPostponedEnterTransition() in your fragment after you created your view in onViewCreated().
If you're using AppCompat try supportPostponeEnterTransition() and supportStartPostponedEnterTransition() or ActivityCompat.postponeEnterTransition(yourActivity) and ActivityCompat.startPostponedEnterTransition(yourActivity).
Credits to:
http://www.androiddesignpatterns.com/2015/03/activity-postponed-shared-element-transitions-part3b.html
Related
I am extremely new to Android development. I basically have an activity with some buttons, for example "seeTreePicture" and "seeSeaPicture". When I press a button, I want to use a fragment I called "ContentViewer" to display a random tree/sea picture, and also have buttons under the picture to destroy the ContentViewer fragment instance and go back to the menu. The issue is, if I try to use a Fragment Transaction anywhere other than onCreate() of the activity, I get a null pointer exception when I try to access the view in the fragment.
My activity and things related to the fragment:
public class SeeActivity extends AppCompatActivity {
DisplayFragment displayFragment;
Button seeTreeButton;
Button seeSeaButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_see);
seeTreeButton = findViewById(R.id.seeTreeButton);
seeSeaButton = findViewById(R.id.seeSeaButton);
displayFragment = new DisplayFragment();
seeTreeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.fragmentContainer, displayFragment);
fragmentTransaction.commit();
fragmentTransaction.addToBackStack(null);
displayFragment.changeImage(randomTree);
}
});
}
}
In my fragment, change image simply changes the image source of the ImageView:
public void changeImage(int treeResource)
{
img = getView().findViewById(R.id.imageView);
img.setImageResource(treeResource);
}
I get a null pointer exception for trying to access the view from getView() in the fragment, meaning that onCreateView wasn't invoked. Yet if I put the same transaction in the onCreate() method of the activity, it works. What am I doing wrong?
The issue with your code is that you are accessing getView() of your fragment before the fragment has gone through the initialization of the view. The reason why you don't have the crash when you execute the transaction in your activity's onCreate() method is that by the time you click on a button your fragment has already gone through onCreateView() and initialized its view. Check out fragment lifecycle guide and bear in mind that you should not access your fragment view before it was created or after it was destroyed. For more information about why your fragment view is not initialized instantly check out this guide.
As for the solution, consider setting arguments for your fragment before adding it to your transaction like here:
Bundle args = new Bundle();
args.putInt(DisplayFragment.IMG_RESOURCE_ARG, randomTree);
displayFragment.setArguments(args);
Then in your onCreateView() or onViewCreated() methods of your fragment restore the arguments like here and set the image resource:
#Override
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
int resourceId = requireArguments().getInt(IMG_RESOURCE_ARG);
img = view.findViewById(R.id.imageView);
img.setImageResource(resourceId);
}
Because the fragment transaction doesn't occur instantly. It occurs async. So the actual work of creating views hasn't occurred yet. Your options are to either use commitNow() instead of commit (which will do it synchronously, but require much more time and possibly cause your app to visibly pause) or to wait for the fragment transaction to actually complete. That can easily be done by putting it in a Runnable and passing that runnable to runOnCommit
I have followed the official documentation http://developer.android.com/guide/practices/tablets-and-handsets.html for Tablet support to create dual pane layout that works as shown below, in that in small screens (phones) it uses one Fragment inside one Activity to display a list of objects and another fragment inside another Activity.
Every other documentation I read talks about a one way flow from Master to details, now I want to go back the other way, from details to master and I am stuck.
In the details, I have added an Item that I want to display in the list and I want this to be dynamic such that I can add few items and each time I hit save I want the List to grow.
This is what I have done so far
In FragmentA(List Fragment) I have a method that (re)loads the data and call notifyDataSetChanged on the adapter.
I added a method in the call back that is called each time an item is added. And both Activity A and Activity B implements this listener
So when I add an item in FragmentB(Details Fragment) I call the listener and on Activity A which is housing the dual pane layout I try this
public void OnNewCustomerAdded() {
Fragment frag = null;
frag = getSupportFragmentManager().findFragmentByTag("CustomertListFragment");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.detach(frag);
ft.attach(frag);
ft.commit();
}
Unfortunately that throws a NPE, and also if I call the methods directly in the Fragment to reload data, that throws an NPE. The only thing that works with some side effects is this
public void OnNewClientAdded() {
Intent intent = getIntent();
finish();
startActivity(intent);
}
So how can I safely restart a Fragment inside an Activity without restarting the other Fragment.
Wow, this is quite tricky, never expected it to be this challenging. Well this is how I solve it.
First I removed the second activity and reworked the code to show single pane in handheld devices and dual pane in tablets using just one Activity instead of two. This is not necessary but it helps when you are dealing with one set of lifecycles and listeners.
Then to actually have items added in the DetailsFragment appear immediately in the ListFragment while still having the Details Fragment open. I finished the containing Activity, restarted it and passed it an intent that tells it to start up the DetailsFragment
Remember that the ListFragment is set to start up no matter which device size. So you just need to start the DetailsFragment and since there is already a call back that does that it makes it easy so here is the code
//Callback method for when an item is added, called from the Details
//Fragment
public void OnNewCustomerAdded() {
Intent mIntent = getIntent();
mIntent.putExtra(Constants.SHOULD_START_CUSTOMER_DETAILS, true);
finish();
startActivity(mIntent);
}
Then in the onCreate of the Activity, after the ListFragment has been started, you do this
boolean shouldStartCustomerDetails = getIntent().getBooleanExtra(Constants.SHOULD_START_CLIENT_DETAILS, false);
if (shouldStartClientDetails){
OnCustomerListItemSelected(0);
}
The OnCustomertListItemSelected is the standard mCallback listener that you get if you created a Master/Details Activity in Android Studio, I just modified it to suit my app like so
/**
* Callback method from {#link OnCustomerListItemSelectedListener}
* indicating that the item with the given ID was selected.
*/
#Override
public void OnCustomerListItemSelected(long id) {
if (mTwoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
CustomerDetailsFragment fragment = CustomerDetailsFragment.newInstance(id);
getSupportFragmentManager().beginTransaction()
.replace(R.id.customeractivity_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
CustomerDetailsFragment fragment =
CustomerDetailsFragment.newInstance(getIntent().getLongExtra(Constants.ARG_ITEM_ID, 0));
getSupportFragmentManager().beginTransaction()
.add(R.id.customeractivity_list_container, fragment)
.commit();
}
}
Hi i created a project with a default "Navigation Drawer Activity".
So i have a MainActivity with a fragment with is replaced for each item on menu.
One of the menus is "Customers" with shows a list of customers.
From customers fragment i can see the Interests of this customers, with is a Fragment(CustomerListFragment) calling the interests(InterestsListFragment).
There is even more levels, but to be short that's enough.
This is the code on MainActivity that i use to call fragment from fragment and pass data between
public void passData(Object[] data, Fragment f) {
Bundle args = new Bundle();
args.putSerializable("PASSED_DATA", data);
f.setArguments(args);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, f)
.addToBackStack("")
.commit();
}
And i use like :
mCallbacks.passData(new Object[]{c}, new OpportunityListFragment());
The problem is that when i rotate the phone does not matter from wich level of activity i have, it comes back to the first fragment called(CustomerListFragment), and if i click "Back" on cellphone it gets back to where i was when i rotate the phone.
What do i have to do, to avoid this kind of problem? why it gets back to the first activity evoked if i am replacing fragments?
The answer from ste-fu is correct but let's explore programmatically. There is a good working code in Google documentation # Handling Runtime Changes. There are 2 code snippets that you have to do.
1) Code snippet:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
Note: Code uses FragmentManager to find the current Fragment. If fragment is null, then the UI or app has not been executed. if not null, then you can get data from RetainedFragment object.
2) Need to retain the Fragment state.
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
Note: setRetainInstance is used in OnCreate. And subclassing the Fragment is recommended, naming it RetainedFragment, used on snippet 1.
When you change screen orientation your parent Activity is destroyed and recreated. Unless you persist the level structure in some fashion, it will always appear as when you first started the activity. You can either use the bundle object, or for more complicated objects you need to persist it to a database.
Either way, onSaveInstanceState is your friend. Then in your onCreate method you need to check the bundle or database, and the set the fragment accordingly.
I'm having a little trouble understanding the behavior of fragments inside an activity. Consider the following scenario: I have a holder activity and 2 or more fragments inside.
The onCreate method for the activity is like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_holder);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction().add(R.id.container, new Frag1(), "ZZZ").commit();
}
}
I have a button in Frag1 which is linked to a callBack in the activity:
#Override
public void bam(String s) {
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction beginTransaction = fragmentManager.beginTransaction();
beginTransaction.replace(R.id.container, new Frag2());
beginTransaction.addToBackStack(null);
beginTransaction.commit();
}
At this point Frag2 is on the stack and the only visible Fragment. I used replace and addToBackStack because I need the back navigation.
My problem is that when I rotate the screen while inside Frag2, the super.onCreate(savedInstanceState) method from the activity calls the constructor for Frag1.
Is there any way to avoid the call to Frag1's constructor until the user presses the back button?
Fragments added to the backstack stay in memory and cannnot be garbage collected. They are kept as actual references to fragments. The reason it is recreated is because you still have an instance of the fragment. You can still call it's methods and fields as you can with any other object; it's simply not visible to the user and trying to manipulate its views may fail.
If the only purpose of adding the fragment to the backstack is navigation, this can be accomplished by not putting the fragment in the backstack to beging with, thus letting that instance of the fragment fall out of memory, then by overriding the onBackPressed() in the activity you can re-create() your fragment 1. You are free to cache any data you need as well.
The purpose of the backstack is to preserve the fragments state. When it's written to the backstack onDestroyView() is called, but it's viewHierarchy is saved with onSaveInstancestate(). This saves stuff like text in TextViews, scroll positions, etc.
If there's resource intensive stuff in Fragment 1's initialization you can also try moving it to a later lifecycle event, like onResume().
You can set properties for activity in manifest file so that your activity wont get destroyed on configuration change like as below :
<activity
android:name=".HomeActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan|adjustResize" >
</activity>
android:configChanges="keyboardHidden|orientation|screenSize" ; these are the properties.
Or you can do a cross check, by matching tags of fragment, while adding or replacing fragments.For this you need to code as mention below :
1) Adding tag while adding fragment :
getFragmentManager().beginTransaction().add(R.id.container, new Frag1(), "TAG NAME").commit();
2) Then check for existing fragment in onCreate() of activity as below :
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_holder);
fragment1 = getSupportFragmentManager().findFragmentByTag("TAG NAME");
if(fragment1 == null) { //if fragment null, then add fragment
getFragmentManager().beginTransaction().add(R.id.container, new Frag1(), "TAG NAME").commit();
}
}
I have added a viewpager to an activity which contains two page.
In onCreate of activity I add fragments to a fragmentAdapter:
public void onCreate(Bundle savedInstanceState)
{
......
FragmentAdapter fragmentAdapter = new FragmentAdapter
(
getSupportFragmentManager(),
new Fragment[]{FragmentGame.builder(id), FragmentComments.builder(id)},
new String[]{getString(R.string.gameInfo), getString(R.string.comments)}
);
ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
viewPager.setAdapter(fragmentAdapter);
public static FragmentGame builder(long id)
{
FragmentGame fragmentGame = new FragmentGame();
// fragmentGame.id = id;
Bundle bundle = new Bundle();
bundle.putLong(Constants.EXTRA_ID, id);
fragmentGame.setArguments(bundle);
return fragmentGame;
}
First time that activity is created the onCreateView of fragment is called as it's expected.
The strange behaviour is when the screen is rotated for the first time the onCreateView of fragment is called twice but the second call only has the correct id and for the first call id is 0.
On second screen rotation, onCreateView is called three times and again only the last one has id.
By more screen rotation, onCreateView calls increase.
I found some related question about fragment and screen rotation but I can't figure out why this happens and how to do it the right way.
----- UPDATE ------
The ID problem is solved as I replaced bundle with direct value setting.
If you don't want to reload your fragment on orientation change, write following for the activity in which you are loading the fragment in manifest file.
<activity
android:name="your activity name"
android:configChanges="orientation|screenSize" // add this to your activity
android:label="#string/app_name">
</activity>
Every time you rotate your device you are creating a new fragment and adding it to the FragmentManager.
All of your previously created fragments are still in the FragmentManager therefore the count increases by one each time.
If you wish to retain a value in your fragment, you need to store it in the arguments otherwise any values it contains would be lost when the system re-creates the fragment.
public static FragmentGame builder(long id) {
Bundle args = new Bundle();
args.putInt("id", id);
fragmentGame f = new fragmentGame();
f.setArguments(args);
}
Rather than creating a new fragment, I suspect you really want to retrieve your previously created one when you rotate the device. use getSupportFragmentManager().findFragmentById() or getSupportFragmentManager().findFragmentByTag() to do this.
Edit: Extra code
// Add fragment to the manager
FragmentTransaction trans=getSupportFragmentManager().beginTransaction();
trans.add(f,"myTag");
trans.commit();
// retrieve the fragment
Fragment f= getSupportFragmentManager().findFragmentByTag("myTag");
Just attempt to retrieve the fragment, if the value is null, create a new fragment and add it to the manager otherwise use the retrieved one.