the best practice to integrate arFragment (sceneForm) with existing Fragment APP - arcore

We have been working on adding AR features to our existing APP for a couple of months with limited progress. Very excited to read the recent development from google on sceneForm and arFragment. our current APP consists three Fragments and one of them will need AR features.
It looks straight forward to us,so We replaced the Fragment in our APP with arFragment. The build is successful and stopped during running with little information for debugging. any suggestion on the proper steps for us to upgrade from Fragment to arFragment? or maybe I missed the points of arFragment here?
in order to show the problem without for you to go through our length code (yet valuable to us), we constructed a dummy project based on the sample project from Google: HelloSceneform. Basically, we changed the static Fragment to dynamic Fragment. Only two files are changed and two files are added, which are attached thereafter. The modified project can be built successfully, but stopped when starting to run.
Thank you
Peter
/////// File modified, HelloSceneformActivity.java:
import android.support.v4.app.FragmentTransaction;
// private ArFragment arFragment;
private ItemOneFragment arFragment;
//arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
arFragment = ItemOneFragment.newInstance();
//Manually displaying the first fragment - one time only
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.frame_layout, arFragment);
transaction.commit();
/////// File modified, activity_ux.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".HelloSceneformActivity">
</FrameLayout>
////// File added fragment_item_one.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ItemOneFragment">
</FrameLayout>
/////// File added, ItemOneragment.java:
package com.google.ar.sceneform.samples.hellosceneform;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.google.ar.sceneform.ux.ArFragment;
public class ItemOneFragment extends ArFragment {
public static ItemOneFragment newInstance() {
ItemOneFragment fragment = new ItemOneFragment();
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_item_one, container, false);
}
}

I have experienced the same when I try to add the ArFragment dynamically to my activity.
It is crashing because I was trying to access the ArSceneView right after I commit the fragment which appeared to be null at that point.
The solution that worked for me was to implement a completion listener that will provide a callback in the Activity when the fragment is done configuring the ARSession.
Below is the basic idea.
public class MyActivity implements MyArFragment.OnCompletionListener{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportFragmentManager().beginTransaction().add(R.id.fragment_holder, new
MyArFragment(), "my_arfragment").commit();
}
#Override
public void onComplete() {
ArFragment arFragment = (ArFragment) getSupportFragmentManager().findFragmentByTag("my_arfragment");
ArSceneView view = arFragment.getArSceneView();
Scene scene = view.getScene();
scene.addOnUpdateListener(this::onUpdateFrame);
}
}
And the fragment:
public class MyFragment extends ArFragment{
public static interface OnCompleteListener {
public abstract void onComplete();
}
private OnCompleteListener mListener;
#Override
public void onAttach(Context context) {
super.onAttach(context);
try {
this.mListener = (OnCompleteListener)context;
}
catch (final ClassCastException e) {
throw new ClassCastException(context.toString() + " must implement
OnCompleteListener");
}
}
#Override
protected Config getSessionConfiguration(Session session) {
//Update session config...
mListener.onComplete();
return config;
}
}

In one of my project I've used the following structure to accomplish the integration of ArFragment. Maybe this will give you some new hints.
I have a root layout with the first element that is a FrameLayout called "body".
This "body" is used as a placeholder to switch the 3 Fragment that exist into the app.
One of these 3 is called "SimulationFragment" and extends ArFragment of Sceneform.
The corresponding layout is composed by the root FrameLayout with some elements and another nested FrameLayout called "ar_frameLayout".
At Runtime I've changed the implementation of onCreateView for the SimulationFragment with a direct call of super.onCreateView() that gives me the base view of ArFragment (this call initializes also the scene from getArSceneView().getScene()).
After that I've added this view into the container view of the simulation fragment that I've previously inflated and then returned it.
Something like this:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View containerView = inflater.inflate(R.layout.fragment_simulation, container, false);
FrameLayout arCoreFrameLayout = containerView.findViewById(R.id.ar_core_frame_layout);
// Inflate the layout for this fragment
View view = super.onCreateView(inflater, container, savedInstanceState);
arCoreFrameLayout.addView(view);
return containerView;
}

ItemOneFragment extends ArFragment but overrides its onCreateView method to inflate its own layout file. I think that's the problem. ArFragment can't find its ArSceneView and other code elements and will just not work.

I really liked the solution of #kdroider, but he forgot to remove reference to activity in onDetach(), which might be quite important. Also, rewote the code in Kotlin:
import android.content.Context
import com.google.ar.core.Config
import com.google.ar.core.Session
import com.google.ar.sceneform.ux.ArFragment
class AsyncArFragment : ArFragment() {
private var onArReadyListener: OnArReadyListener? = null
interface OnArReadyListener {
fun onArReady()
}
override fun getSessionConfiguration(session: Session): Config {
onArReadyListener?.onArReady()
return super.getSessionConfiguration(session)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
onArReadyListener = context as OnArReadyListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString() + " must implement OnArReadyListener")
}
}
override fun onDetach() {
super.onDetach()
onArReadyListener = null
}
}
And don't forget to implement OnArReadyListener interface.
WARNING. Replacing fragments with transaction
AnotherFragment -> AsyncArFragment works okay
AsyncArFragment -> AnotherFragment -> AsyncArFragment doesn't work for some reason (onResume() and then immediately onDestroy() are called). If you know how to fix it, please write in comments.

For anyone coming here through google (like me):
Depending on your use case it might not be necessary to override the ArFragment - simply make sure to access the scene after arFragment.setOnSessionInitializationListener is called.

Related

How to get the parent router layout in vaadin flow? [duplicate]

I need to create a buffer between the application screens.
I think to make a buffer in the mainLayout but I can't access it from the child layers
I tried to do it through 'static', then the buffer is common for all users, and this is incorrect
Cookies are also unsuitable because the data structure is complex
Code with static buffer:
in the example, a table is created, when you select elements in the table, they are written to the buffer, and if there are elements in the buffer, they are marked in the table
MainLayout.java
public class MainLayout extends AppLayout implements RouterLayout {
public static Set<Test> buffer;
public MainLayout() {
buffer = new HashSet<Test>();
/*...*/
}
}
BasicView.java
#Route(value = "BasicView", layout = MainLayout.class)
#RouteAlias(value = "/BasicView", layout = MainLayout.class)
public class BasicView extends VerticalLayout {
private final Grid<Test> tests;
public BasicView(#Autowired TestService){
/*...*/
tests = new Grid<>(Test.class, false);
tests.addSelectionListener(event -> {
MainLayout.buffer = event.getAllSelectedItems();});
/*...*/
for(Test el : MainLayout.buffer)
{
tests.select(el);
}
/*...*/
}
}
You can actually traverse parents of the layout upto main layout. E.g. you can do something lie below
Optional<Component> parent = this.getParent();
Buffer buffer = null;
while (parent.isPresent()) {
Component p = parent.get();
if (p instanceof MainLayout) {
MainLayout main = (MainLayout) p;
buffer = main.getBuffer();
}
parent = p.getParent();
}
However I am not sure if it is the best approach. If you happen to use e.g. Spring Boot, it would be more natural to have this buffer as VaadinSessionScoped bean, and Autowire it where needed. The next version of Vaadin will also add specific RouteScope, which allows more pin-point scoping if needed.
See also old Vaadin Forum discussion about this: https://vaadin.com/forum/thread/17917385/vaadin-14-accessing-components-of-mainview

Gluon Afterburner Presenter.initialize() not called

Link to repository: https://gitlab.com/RichardGladman/medimindr
I've created a new Multiple view with Afterburner project using the IntelliJ plugin and added a new view to the project. It displays some about information as I thought it would be easy. The view displays just fine with the exception that the AboutPresenter doesn't get initialised meaning the AppBar is not populated.
The presenter class
package com.thefifthcontinent.medimindr.views;
... imports snipped ...
public class AboutPresenter extends GluonPresenter<MediMindr> {
#FXML
private View about;
public void initialize() {
about.showingProperty().addListener((obs, oldValue, newValue) -> {
if (newValue) {
AppBar appBar = getApp().getAppBar();
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e ->
getApp().getDrawer().open()));
appBar.setTitleText("About");
}
});
}
}
The creation in the AppViewManager
public static final AppView ABOUT_VIEW = view(
bundle.getString("drawer.about"),
AboutPresenter.class,
MaterialDesignIcon.DASHBOARD,
SHOW_IN_DRAWER);
I can't see a difference between what I have done and what was created with the project.
It turns out they are still configured in the pom.xml. I thought it would be auto injection using Afterburner.

Vaadin TextField that will only default to numeric keyboard on iOS

Do you know of a way to setup a TextField so that the numerical keyboard is shown instead of the regular one (cfr. a type attribute "number" in the input element)? Users are finding it annoying to always have to switch to the numeric keyboard for certain fields (these have to be filled out several hundred times per day!). Most related posts pertain to restricting the input to numbers which is not a problem.
Thanks,
William
For Vaadin:
Simply using Number Field
Number Field: Mobile browser shows dedicated input controls. Decrease and increase buttons for the value can be shown optionally.
NumberField dollarField = new NumberField("Dollars");
See the documentation example.
For Native iOS:
Simply set the keyboard type to NumberPad:
self.someTextField.keyboardType = UIKeyboardType.NumberPad
See the documentation for all the keyboard types.
There is a closed github issue about this here, but I'm not sure how the there mentioned slotting should work now. If this is now already possible without the workaround below, please feel free to let me know.
As far as I can tell, adding the attribute type="number" to the <vaadin-text-field> does not work, because this attribute should be on the actual <input> element within.
There is a workaround to do this: https://github.com/Artur-/vaadin-examples/blob/master/example-textfield-type/src/main/java/org/vaadin/artur/MainView.java#L42
TextField textfield = new TextField("Number Input");
textField.getElement().getNode().runWhenAttached(ui -> {
ui.getPage().executeJavaScript("$0.focusElement.type=$1", textField, "number");
});
In lieu of built-in components / add-ons, I found a lightweight solution in one of the samples from the archetype-application-example GitHub project.
In summary (only using relevant parts):
NumberTypeField.java:
package com.example;
import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.ui.TextField;
/**
* A field for entering numbers. On touch devices, a numeric keyboard is shown
* instead of the normal one.
*/
public class NumberTypeField extends TextField {
public NumberTypeField() {
// Mark the field as numeric.
// This affects the virtual keyboard shown on mobile devices.
AttributeExtension ae = new AttributeExtension();
ae.extend(this);
ae.setAttribute("type", "number");
}
public NumberTypeField(String caption) {
this();
setCaption(caption);
}
}
AttributeExtension.java:
package com.example;
import com.vaadin.annotations.JavaScript;
import com.vaadin.server.AbstractJavaScriptExtension;
import com.vaadin.ui.TextField;
/**
* A JavaScript extension for adding arbitrary HTML attributes for components.
*/
#JavaScript("attribute_extension_connector.js")
public class AttributeExtension extends AbstractJavaScriptExtension {
private static final long serialVersionUID = 1L;
public void extend(TextField target) {
super.extend(target);
}
#Override
protected AttributeExtensionState getState() {
return (AttributeExtensionState) super.getState();
}
public void setAttribute(String attribute, String value) {
getState().attributes.put(attribute, value);
}
AttributeExtensionState.java:
package com.example;
import com.vaadin.shared.JavaScriptExtensionState;
import java.util.HashMap;
/**
* Shared state class for {#link AttributeExtension} communication from server
* to client.
*/
public class AttributeExtensionState extends JavaScriptExtensionState {
private static final long serialVersionUID = 1L;
public HashMap<String, String> attributes = new HashMap<String, String>();
}
attribute_extension_connector.js (put in same source folder, e.g., com.example):
window.com_example_AttributeExtension = function() {
this.onStateChange = function() {
var element = this.getElement(this.getParentId());
if (element) {
var attributes = this.getState().attributes;
for (var attr in attributes) {
if (attributes.hasOwnProperty(attr)) {
element.setAttribute(attr, attributes[attr]);
}
}
}
}
}

Get active Window in UI

I'm using a Vaadin-UI with a servlet like this:
#WebServlet(value = "/*", asyncSupported = true)
#VaadinServletConfiguration(productionMode = false, ui = MyUI.class, widgetset = "widgetsets.MyWidgetSet")
public class MyServlet extends VaadinServlet {
}
#Theme("mytheme")
#PreserveOnRefresh
#Push
public class MyUI extends UI {
#Override
protected void init(VaadinRequest request) {
Navigator navigator = new Navigator(this, this);
navigator.addView("myView", MyView.class);
// add some other views
navigator.navigateTo("myView");
setNavigator(navigator);
}
}
public class MyView extends VerticalLayout implements View {
#Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
// init UI stuff
}
}
It works fine so far but what I want to do now, is to use the ConfirmDialog Add-on from inside my view. My problem is that all of the ConfirmDialog.show() methods require a Window as a parameter. How can I obtain the active Window object isnide View or UI classes? UI.getCurrent().getWindows() is empty and all the examples I found use an Application class.
Version 1 of the addon uses an existing window (create one and pass it, as Vaadin only has Page and UI by default, a Window is just an component). With version 2 of the addon you pass only the UI, where the dialog (which subclasses Window) will show itself.

ViewMapListener JSF not being called

I'm trying to port the JSF #ViewScoped annotation to CDI. The reason is more educational rather than based on need. I chose this particular scope mainly due to the lack of a better concrete example of a custom scope one might want to implement in CDI.
That said, my starting point was Porting the #ViewScoped JSF annotation to CDI. But, this implementation does not take into account a seemingly very important responsibility of Context (i.e. destroying) mentioned in the API:
The context object is responsible for creating and destroying contextual instances by calling operations of Contextual. In particular, the context object is responsible for destroying any contextual instance it creates by passing the instance to Contextual.destroy(Object, CreationalContext). A destroyed instance must not subsequently be returned by get(). The context object must pass the same instance of CreationalContext to Contextual.destroy() that it passed to Contextual.create() when it created the instance.
I decided to add this functionality by having my Context object:
keep track of what Contextual objects it creates for which UIViewRoots;
implement the ViewMapListener interface and register itself as listener for each UIViewRoot by calling UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);
destroy any created Contextuals when the ViewMapListener.processEvent(SystemEvent event) is called and unregister itself from that UIViewRoot.
Here's my Context implementation:
package com.example;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PreDestroyViewMapEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.ViewMapListener;
public class ViewContext implements Context, ViewMapListener {
private Map<UIViewRoot, Set<Disposable>> state;
public ViewContext() {
this.state = new HashMap<UIViewRoot, Set<Disposable>>();
}
// mimics a multimap put()
private void put(UIViewRoot key, Disposable value) {
if (this.state.containsKey(key)) {
this.state.get(key).add(value);
} else {
HashSet<Disposable> valueSet = new HashSet<Disposable>(1);
valueSet.add(value);
this.state.put(key, valueSet);
}
}
#Override
public Class<? extends Annotation> getScope() {
return ViewScoped.class;
}
#Override
public <T> T get(final Contextual<T> contextual,
final CreationalContext<T> creationalContext) {
if (contextual instanceof Bean) {
Bean bean = (Bean) contextual;
String name = bean.getName();
FacesContext ctx = FacesContext.getCurrentInstance();
UIViewRoot viewRoot = ctx.getViewRoot();
Map<String, Object> viewMap = viewRoot.getViewMap();
if (viewMap.containsKey(name)) {
return (T) viewMap.get(name);
} else {
final T instance = contextual.create(creationalContext);
viewMap.put(name, instance);
// register for events
viewRoot.subscribeToViewEvent(
PreDestroyViewMapEvent.class, this);
// allows us to properly couple the right contaxtual, instance, and creational context
this.put(viewRoot, new Disposable() {
#Override
public void dispose() {
contextual.destroy(instance, creationalContext);
}
});
return instance;
}
} else {
return null;
}
}
#Override
public <T> T get(Contextual<T> contextual) {
if (contextual instanceof Bean) {
Bean bean = (Bean) contextual;
String name = bean.getName();
FacesContext ctx = FacesContext.getCurrentInstance();
UIViewRoot viewRoot = ctx.getViewRoot();
Map<String, Object> viewMap = viewRoot.getViewMap();
if (viewMap.containsKey(name)) {
return (T) viewMap.get(name);
} else {
return null;
}
} else {
return null;
}
}
// this scope is only active when a FacesContext with a UIViewRoot exists
#Override
public boolean isActive() {
FacesContext ctx = FacesContext.getCurrentInstance();
if (ctx == null) {
return false;
} else {
UIViewRoot viewRoot = ctx.getViewRoot();
return viewRoot != null;
}
}
// dispose all of the beans associated with the UIViewRoot that fired this event
#Override
public void processEvent(SystemEvent event)
throws AbortProcessingException {
if (event instanceof PreDestroyViewMapEvent) {
UIViewRoot viewRoot = (UIViewRoot) event.getSource();
if (this.state.containsKey(viewRoot)) {
Set<Disposable> valueSet = this.state.remove(viewRoot);
for (Disposable disposable : valueSet) {
disposable.dispose();
}
viewRoot.unsubscribeFromViewEvent(
PreDestroyViewMapEvent.class, this);
}
}
}
#Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
}
Here's the Disposable interface:
package com.example;
public interface Disposable {
public void dispose();
}
Here's the scope annotation:
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.enterprise.context.NormalScope;
#Inherited
#NormalScope
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.TYPE, ElementType.METHOD,
ElementType.FIELD, ElementType.PARAMETER})
public #interface ViewScoped {
}
Here's the CDI extension declaration:
package com.example;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
public class CustomContextsExtension implements Extension {
public void afterBeanDiscovery(#Observes AfterBeanDiscovery event) {
event.addContext(new ViewContext());
}
}
I added the javax.enterprise.inject.spi.Extension file under META-INF/services containing com.example.CustomContextsExtension to properly register the above with CDI.
I can now make beans like (notice the use of the custom #ViewScoped implementation.):
package com.example;
import com.concensus.athena.framework.cdi.extension.ViewScoped;
import java.io.Serializable;
import javax.inject.Named;
#Named
#ViewScoped
public class User implements Serializable {
...
}
The beans are created properly and properly injected into JSF pages (i.e. the same instance is returned per view, new ones are created only when the view is created, the same instances are injected over multiple requests to the same view). How do I know? Imagine the above code littered with debugging code which I purposefully stripped out for clarity and since this is already a huge post.
The problem is that my ViewContext.isListenerForSource(Object source) and ViewContext.processEvent(SystemEvent event) are never called. I was expecting that at least upon session expiration those events would be called, since the view map is stored in the session map (correct?). I set the session timeout to 1 minute, waited, saw the timeout happen, but my listener was still not called.
I also tried adding the following to my faces-config.xml (mostly out of the lack of ideas):
<system-event-listener>
<system-event-listener-class>com.example.ViewContext</system-event-listener-class>
<system-event-class>javax.faces.event.PreDestroyViewMapEvent</system-event-class>
<source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>
Finally, my environment is JBoss AS 7.1.1 with Mojarra 2.1.7.
Any clues would be greatly appreciated.
EDIT: Further Investigation.
PreDestroyViewMapEvent doesn't seem to be fired at all while PostConstructViewMapEvent is fired as expected - every time a new view map is created, specifically during UIViewRoot.getViewMap(true). The documentation states that PreDestroyViewMapEvent should be fired every time clear() is called on the view map. That leaves to wonder - is clear() required to be called at all? If so, when?
The only place in the documentation that I was able to find such a requirement is in FacesContext.setViewRoot():
If the current UIViewRoot is non-null, and calling equals() on the
argument root, passing the current UIViewRoot returns false, the clear
method must be called on the Map returned from UIViewRoot#getViewMap.
Does this ever happen in the normal JSF lifecycle, i.e. without programmatically calling UIViewRoot.setViewMap()? I can't seem to find any indication.
This is related to an issue with the JSF spec that is being fixed in the JSF2.2 spec, see here. Also, I created an issue with Apache DeltaSpike so they may try to fix it, see here. If it's fixed in DeltaSpike, then it may end up being merged into CODI and / or Seam as well.
The view map is stored in a LRU map, because you never know which view will be post back. Unfortunately, the PreDestroyViewMapEvent is not called before removing from this map.
A workaround is to reference your object from within a WeakReference. You can use ReferenceQueue or check the reference when to call your destruction code.

Resources