It's common for a Fragment to be added to a layout when a UI element, such as a button is tapped. If the user taps the button multiple times very quickly, it can happen that the Fragment is added multiple times, causing various issues.
How can this be prevented?
I created a helper method that ensures that the fragment is only added if it doesn't yet exist:
public static void addFragmentOnlyOnce(FragmentManager fragmentManager, Fragment fragment, String tag) {
// Make sure the current transaction finishes first
fragmentManager.executePendingTransactions();
// If there is no fragment yet with this tag...
if (fragmentManager.findFragmentByTag(tag) == null) {
// Add it
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(fragment, tag);
transaction.commit();
}
}
Simple call as such from an Activity or another Fragment:
addFragmentOnlyOnce(getFragmentManager(), myFragment, "myTag");
This works with both the android.app.* and the android.support.app.* packages.
Here my solution, I have try to click show dialog fragment by tap button multiple time and quickly.
FragmentManager fm = getSupportFragmentManager();
Fragment oldFragment = fm.findFragmentByTag("wait_modal");
if(oldFragment != null && oldFragment.isAdded())
return;
if(oldFragment == null && !please_wait_modal.isAdded() && !please_wait_modal.isVisible()){
fm.executePendingTransactions();
please_wait_modal.show(fm,"wait_modal");
}
Related
With Fragment:setRetainInstance(true); the fragment is not re-instantiated on a phones orientation change.
And of course i want my fragments to be kept alive while switching from one fragment to another.
But the Android Studio 4 provides a wizard-template with only
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
From hours of debugging and searching the net if think it would need to inherent from the class FragmentNavigator so i can overwrite FragmentNavigator:naviagte where a new fragment gets created via final Fragment frag = instantiateFragment(.. and then is added with ft.replace(mContainerId, frag);
So i could find my old fragment and use ftNew.show and ftOld.hide instead.
Of course this is a stupid idea, because this navigate method is full of other internal stuff.
And i have no idea where that FrameNavigator is created.
I can retrieve it in the MainActivity:OnCreate with
NavigatorProvider navProvider = navController.getNavigatorProvider ();
Navigator<NavDestination> navigator = navProvider.getNavigator("fragment");
But at that time i could only replace it with my derived version. And there is no replaceNavigtor method but only a addNavigator method, which is called where ?
And anyways this all will be far to complicated and therefore error prone.
Why is there no simple option to keep my fragments alive :-(
In older Wizard-Templates there was the possibility of
#Override
public void onNavigationDrawerItemSelected(int position) {
Fragment fragment;
switch (position) {
case 1:
fragment = fragment1;
break;
case 2:
fragment = fragment2;
break;
case 3:
fragment = fragment3;
break;
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if(mCurrentFragment == null) {
ft.add(R.id.container, fragment).commit();
mCurrentFragment = fragment;
} else if(fragment.isAdded()) {
ft.hide(mCurrentFragment).show(fragment).commit();
} else {
ft.hide(mCurrentFragment).add(R.id.container, fragment).commit();
}
mCurrentFragment = fragment;
}
but i have no idea how to do this with the Android 4.0 template where my MainActivity is only derived as:
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration mAppBarConfiguration;
Ideas welcome :'(
Hi there & sorry for my late answer! I had a similar problem with navigation drawers and navigation component. I tried around a little and found a working solution, which might be helpful for others too.
The key is the usage of a custom FragmentFactory in the FragmentManager of the MainActivity. See the code for this below:
public class StaticFragmentFactory extends FragmentFactory {
private myNavHostFragment1 tripNavHostFragment;
private myNavHostFragment2 settingsNavHostFragment;
#NonNull
#Override
public Fragment instantiate(#NonNull ClassLoader classLoader, #NonNull String className) {
if (MyNavHostFragment1.class.getName().equals(className)) {
if (this.myNavHostFragment1 == null) {
this.myNavHostFragment1 = new MyNavHostFragment1();
}
return this.myNavHostFragment1 ;
} else if (MyNavHostFragment2.class.getName().equals(className)) {
if (this.myNavHostFragment2 == null) {
this.myNavHostFragment2 = new MyNavHostFragment2();
}
return this.myNavHostFragment2;
}
return super.instantiate(classLoader, className);
}
}
The FragmentFactory survives the navigation between different fragments using the NavigationComponent of AndroidX. To keep the fragments alive, the FragmentFactory stores an instance of the fragments which should survive and returns this instance if this is not null. You can find a similar pattern when using a singleton pattern in classes.
You have to register the FragmentFactory in the corresponding activity by calling
this.getSupportFragmentManager().setFragmentFactory(new StaticFragmentFactory())
Please note also that I'm using nesten fragments here, so one toplevel fragment (called NavHostFragmen here) contains multiple child fragments. All fragments are using the same FragmentFactory of their parent fragments. The custom FragmentFactory above returns the result of the super class method, when the fragment to be instantiated is not known to keep alive.
I have a page that hosts a CollectionView and Map.
there is a list of items, the ItemsSource of the CollectionView is set to this list and the map pins are drawn based on this list, there's a requirement that when the user scrolls the CollectionView the opposite map pin is highlighted, and when the pin is clicked the CollectionView is scrolled to that item.
I use ScrollTo and Scrolled event. but the problem is that when the ScrollTo is called the Scrolled event is fired too, and that causes a lag or some unexpected behavior.
I tried to set a flag:
private int centerIndex = -1;
bool userScroll;
private void CollectionView_Scrolled(object sender, ItemsViewScrolledEventArgs args)
{
if (centerIndex != args.CenterItemIndex)
{
if (userScroll)
MessagingCenter.Send<object, int>(this, Keys.GoToLocation, args.CenterItemIndex);
userScroll = true;
centerIndex = args.CenterItemIndex;
}
}
private void ScrollToVehicle(object arg1, Item item)
{
if (collectionView.ItemsSource != null && collectionView.ItemsSource.Cast<Item>().Contains(item))
{
userScroll = false;
collectionView.ScrollTo(item, position: ScrollToPosition.Center, animate: false);
}
}
but the problem is that Scrolled event is called multiple times (inside the if statement)
Try to unsubscribe from the CollectionView_Scrolled before call the ScrollTo
collection.Scrolled -= CollectionView_Scrolled;
I am trying to develop a dynamic user interface. When the user clicks at a certain indicator a graph of it is instantiated together with some manipulation buttons. See the image for an example. The graph is created in an HBox, together with the buttons and next added to a VBox. The problem I cannot solve is: when a button is clicked, how can I access the corresponding element?
The problem simply boils down to this:
Button buttonRemove = new Button ();
buttonRemove.setMinWidth (80);
buttonRemove.setText ("Remove");
buttonMap.getProperties ().put ("--IndicatorRemoveButton", indicator.getName ());
buttonRemove.setOnAction (e -> buttonRemoveClick ());
private Object buttonRemoveClick ()
{
// Which button clicked me??
return null;
} /*** buttonRemoveClick ***/
Any help will be greatly appreciated. I'm kind of stuck with this.
.
It's possible to pass a parameter to the buttonRemoveClick method in the lambda, as long as it's effectively final or a parameter.
private void buttonRemoveClick (HBox group) {...}
buttonRemove.setOnAction (e -> buttonRemoveClick (theGroup));
In this case you could also pass the ActionEvent and get the source to retrieve the Button; this may not be enough to remove the element, but for this you could traverse to the parent until you reach a child of the HBox
private void buttonRemoveClick (ActionEvent event) {
Node currentNode = (Node) event.getSource(); // this is the button
// traverse to HBox of container
Node p;
while ((p = currentNode.getParent()) != containerVBox) {
currentNode = p;
}
// remove part including the Button from container
containerVBox.getChildren().remove(currentNode);
}
buttonRemove.setOnAction (this::buttonRemoveClick);
I have a program with a ComboBox which changes a view. If there are unsaved edits and the user changes the ComboBox, a dialog warns them if they're happy to proceed, and if yes it changes the view... BUT if they say no, I want the ComboBox to revert back to the previous value.
I'm trying to only post code relevant to the problem to be succinct... the warning dialog code happens elsewhere (in my viewController class) and the current group is stored in a class my whole program can see (globalFields).
Here is the code, and as such the problem:
#FXML
private void handleClassesComboBox(ActionEvent event) {
if (classesComboBox.getSelectionModel().getSelectedItem() != null) {
if (viewController.ifUnsavedChangesUserHappyToLose()) {
globalFields.setCurrentGroup(classesComboBox.getSelectionModel().getSelectedItem());
setView();
} else{
classesComboBox.setValue(globalFields.getCurrentGroup());
}
}
So if they are NOT happy to lose changes, I want to revert to the previous selection, but of course this causes a loop as this handleClassesComboBox method is triggered again due to the change.
I'm sure it's obvious, but I can't work out the logic to revert back without the dialog looping over and over.
Also it's my first question here so if I've missed anything or explained the obvious let me know!!
Thanks!
Vin
You could just set a flag:
private boolean checkUserChange = true ;
and then
#FXML
private void handleClassesComboBox(ActionEvent event) {
if (classesComboBox.getSelectionModel().getSelectedItem() != null) {
if (checkUserChange && viewController.ifUnsavedChangesUserHappyToLose()) {
globalFields.setCurrentGroup(classesComboBox.getSelectionModel().getSelectedItem());
setView();
} else{
checkUserChange = false ;
classesComboBox.setValue(globalFields.getCurrentGroup());
checkUserChange = true ;
}
}
}
Background:
According to the accepted answer of this question, a TextArea has an underlying Text node that contains the text and acts as its bounding box. It is useful for calculating the visual (pre-transform) height of the text, even when the text wraps around.
The Text node is not exposed in the public JavaFX 8 API, but it can be gotten via .lookup(). Moreover, it seems it is not initialized till after the Scene is rendered. The above said answer shows how to get it successfully by listening to the Scene.
Problem:
I have taken the logic of the listener, which works, and implemented it as a binding instead. But the binding does not work, and I cannot understand why. The value of the property updated by the listener is successfully set to the new Text node, while the value of the binding is always null because it is not detecting the change of Scene from null to a Scene object.
public ObjectExpression<Text> getObservableTextNode(TextArea textArea) {
// The underlying text node can be looked up only after applying CSS when there is a scene.
// For that reason we return it as an observable.
// Way #1: Listener
ObjectProperty<Text> property = new SimpleObjectProperty<>();
textArea.sceneProperty().addListener((observableNewScene, oldScene, newScene) -> {
if (newScene != null) {
textArea.applyCss();
Node text = textArea.lookup(".text");
property.set((Text) text);
} else {
property.set(null);
}
});
return property;
// Way #2: Binding - does not update
ObjectBinding<Text> ob = Bindings.createObjectBinding(() -> {
if (textArea.sceneProperty().get() != null) {
textArea.applyCss();
return (Text) textArea.lookup(".text");
}
return null;
}, textArea.sceneProperty());
return ob;
}
As far as I can tell, the logics of the listener and binding are the same. Why then is there a difference?