Search code examples
androidandroid-layoutandroid-fragmentsandroid-themeandroid-styles

How can I define one specific fragment instance's style/theme in XML?


I've created a UI component in my app that is used in multiple places (some light, some dark) and needs to be lifecycle-aware, so I've converted it into a fragment. I instantiate this fragment exclusively using <fragment> tags in my activities' XML layout files.

I'm struggling to find a way to enable the fragment to render its own controls and text in a colour that's appropriate for its background as set by its parent element. Currently, no matter what theme or style I set on the parent view or the <fragment> tag itself, the fragment controls are shown as if on a light background:

the fragment as rendered on a light background the fragment as rendered on a dark background

I've tried setting android:theme on the <fragment> to both @style/ThemeOverlay.AppCompat.Dark and @style/ThemeOverlay.AppCompat.Dark.ActionBar in an attempt to get the text light-coloured on the dark background, but with no luck.

I would prefer not to program this colour switching into the fragment itself, because I feel like the theming/styling framework must surely be able to handle this.

Ideally, I would also be able to incorporate an icon into this fragment at a later stage with its colour matching the text. My priority right now though is getting the correct text colour.

What is the correct way to tell one specific instance of a fragment, “you are on a dark background instead of a light one,” or “you need to render your text and controls in a light colour instead of a dark one,” and have it render accordingly?

Update

I've posted an answer that does it by implementing a custom attribute on the <fragment> tag and initialising from it in my fragment's code.

Previous update

Chris Banes' article on Theme vs. Style states:

One thing to note is that android:theme in Lollipop propogates to all children declared in the layout:

<LinearLayout
    android:theme="@android:style/ThemeOverlay.Material.Dark">

    <!-- Anything here will also have a dark theme -->

</LinearLayout>

Your children can set their own theme if needed.

This is distinctly not happening in my case with the use of fragments—it seems that the android:theme attribute of the <fragment> tag in the XML is not being considered at all during the inflation, but should be.

My fragment's onCreateView method is quite standard:

@Override
public View onCreateView(
        @NonNull LayoutInflater inflater,
        @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState
) {
    return inflater.inflate(R.layout.fragment_task_search, container, false);
}

I suppose either the container is not getting the theme to begin with, or the inflater is not “attaching” that information to the inflated child view. I'm not proficient enough in Android development to determine which (if either) is the case.


Solution

  • I have achieved the desired effect by explicitly specifying the parent container's theme in a custom attribute on the <fragment> tag, and manually reading it during the fragment's initialisation.

    XML layout file using the <fragment>

    1. Ensure the root element of the XML has the following xmlns:app attribute:

      xmlns:app="http://schemas.android.com/apk/res-auto"
      
    2. Add a custom attribute of app:customTheme to the <fragment> tag:

      <fragment
          android:name="net.alexpeters.myapp.TaskSearchFragment"
          app:customTheme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
      

    Attributes XML file values/attrs.xml

    Add the declare-styleable element shown below:

        <?xml version="1.0" encoding="utf-8"?>
        <resources>
            <!-- ↓↓↓ -->
            <declare-styleable name="TaskSearchFragment">
                <attr name="customTheme" format="reference" />
            </declare-styleable>
            <!-- ↑↑↑ -->
        </resources>
    

    Fragment subclass

    1. Add an instance variable to hold a custom theme ID:

      private @StyleRes int themeResId;
      
    2. Add a constant to handle the case where no custom theme is passed in:

      private static final int NO_CUSTOM_THEME = 0;
      
    3. Override the onInflate method as follows:

      @Override
      public void onInflate(
              @NonNull Context context,
              AttributeSet attrs,
              Bundle savedInstanceState
      ) {
          super.onInflate(context, attrs, savedInstanceState);
          TypedArray a = context.obtainStyledAttributes(
                  attrs,
                  R.styleable.TaskSearchFragment
          );
          themeResId = a.getResourceId(
                  R.styleable.TaskSearchFragment_customTheme,
                  NO_CUSTOM_THEME
          );
          a.recycle();
      }
      
    4. Add some logic to the onCreateView method to inject the custom theme reference into the inflater:

      @Nullable
      @Override
      public View onCreateView(
              @NonNull LayoutInflater inflater,
              @Nullable ViewGroup container,
              @Nullable Bundle savedInstanceState
      ) {
          // ↓↓↓
          if (themeResId != NO_CUSTOM_THEME) {
              inflater = inflater.cloneInContext(
                  new ContextThemeWrapper(getActivity(), themeResId)
              );
          }
          // ↑↑↑
          return inflater.inflate(R.layout.fragment_task_search, container, false);
      }
      

    I'd prefer to just infer the parent container's theme somehow, but I don't know whether this is possible.

    Failing that, I'd prefer to use a standard attribute (i.e. android:theme) instead of a custom one, but I don't know whether this is possible either.