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:
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?
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.
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.
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.
<fragment>
Ensure the root element of the XML has the following xmlns:app
attribute:
xmlns:app="http://schemas.android.com/apk/res-auto"
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" />
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
subclassAdd an instance variable to hold a custom theme ID:
private @StyleRes int themeResId;
Add a constant to handle the case where no custom theme is passed in:
private static final int NO_CUSTOM_THEME = 0;
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();
}
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.