0

I have a complex "floating view" hierarchy that I am moving between different activities. As these views are shared across activities, they should all have a MutableContextWrapper that will allow them to change the base context. In theory I can have multiple floating views simultaneously.

Here is an example XML for a floating view:

<com.test.views.SampleFloatingView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sample_floating_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <com.test.views.MinimizedImage
        android:layout_width="90dp"
        android:layout_height="90dp"
        android:scaleType="center"/>

</com.test.views.SampleFloatingView>

I am inflating this XML using a MutableContextWrapper and add it to a FrameLayout that will hold it (so I can drag it across the screen), but I'n not getting the result I need:

@NonNull
private FrameLayout createSampleFloatingView() {
    MutableContextWrapper mutableContextWrapper = new MutableContextWrapper(getActivity());
    final FrameLayout view = new FrameLayout(mutableContextWrapper);
    LayoutInflater layoutInflater = LayoutInflater.from(mutableContextWrapper);
    View floatingView = layoutInflater.inflate(R.layout.sample_floating_view, null, true).findViewById(R.id.sample_floating_view);
    setViewMutableContext(floatingView);
    view.addView(floatingView);
    return view;
}

private void setViewMutableContext(View view) {
    Context context = view.getContext();
    if (context instanceof MutableContextWrapper) {
        ((MutableContextWrapper) context).setBaseContext(getActivity());
    } else {
        throw new RuntimeException("View " + view + " doesn't have a MutableContextWrapper");
    }
    if (view instanceof ViewGroup) {
        int childCount = ((ViewGroup) view).getChildCount();
        for (int i = 0; i < childCount; i++) {
            setViewMutableContext(((ViewGroup) view).getChildAt(i));
        }
    }
}

The problem is that while the FrameLayout has a MutableContextWrapper the context of the inflated objects is the Activity which creates the view. This means that the code above crashes as SampleFloatingView doesn't have a MutableContextWrapper. If I remove this code a memory leak would later occur once I disconnect the view from the original activity and move it to the next.

An obvious solution to this issue would be to create the whole hierarchy manually and pass the MutableContextWrapper in the constructor (like I do for the FrameLayout), but I rather avoid it for obvious reasons.

My question is whether there is a better way that will allow me to inflate views from XML and force their context to point to a MutableContextWrapper?

Doron Yakovlev Golani
  • 5,188
  • 9
  • 36
  • 60

1 Answers1

0

The root cause for the issue I saw was the way the default LayoutInflater works (the one you get from LayoutInflater.from(context)). It uses the last activity to inflate the views and that was the reason I got the undesired result. I can't say I too thrilled with the solution, but it seems to be working. What you have to do is to create a custom LayoutInflater and set its Factory. According to some documentations I read, the solution I used might have compatibility issues in case you are using support/compat views, but I assume you can go around those issues as well in case you encounter it.

Here is the code for the custom Factory & LayoutInflater classes:

@SuppressWarnings("rawtypes")
public static class MyLayoutInflaterFactory implements LayoutInflater.Factory2 {

    private static final Class<?>[] constructorSignature = new Class[] {Context.class, AttributeSet.class };

    final MutableContextWrapper mutableContextWrapper;

    public MyLayoutInflaterFactory(MutableContextWrapper mutableContextWrapper) {
        this.mutableContextWrapper = mutableContextWrapper;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = null;
        Class<? extends View> clazz = null;
        try {
            clazz = mutableContextWrapper.getClassLoader().loadClass(name).asSubclass(View.class);
            constructor = clazz.getConstructor(constructorSignature);
            constructor.setAccessible(true);
            return constructor.newInstance(mutableContextWrapper, attrs);
        } catch (Throwable t) {
            return null;
        }
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // Perhaps this can be done better
        return onCreateView(name, context, attrs);
    }
}

public static class MyLayoutInflater extends LayoutInflater {

    protected MyLayoutInflater(Context context) {
        super(context);
    }

    @Override
    public LayoutInflater cloneInContext(Context newContext) {
        return new MyLayoutInflater(newContext);
    }
}

And the way I use it to inflate my floating views:

@NonNull
private FrameLayout createSampleFloatingView() {
    MutableContextWrapper mutableContextWrapper = new MutableContextWrapper(getActivity());
    FrameLayout view = new FrameLayout(mutableContextWrapper);
    LayoutInflater layoutInflater = new MyLayoutInflater(mutableContextWrapper);
    LayoutInflaterCompat.setFactory2(layoutInflater, new MyLayoutInflaterFactory(mutableContextWrapper));
    layoutInflater.inflate(R.layout.sample_floating_view, view, true);
    return view;
}
Doron Yakovlev Golani
  • 5,188
  • 9
  • 36
  • 60