Implementing Dynamic Theme Switching in the LightListen Android Music Player
LightListen adds flexible day/night and brand‑color switching by combining static XML styles and themes for basic modes with a dynamic configuration system—using shared‑preference color storage, view processors, and a traversal controller from the app‑theme‑engine library—to apply colors at runtime with minimal code duplication.
LightListen is a small Android local music player that features multiple colorful skins for both day and night modes.
The color change is achieved by combining two approaches:
Custom Style and Theme
Dynamic Theme Configuration
Custom Style and Theme
Styles and Themes are used to implement day and night modes. A Style is a collection of attributes that define the appearance of a View or Window, such as height, padding, text color, background color, etc. Styles are defined in XML:
<style name="ListItemTitleStyle" parent="TextAppearance.AppCompat.Body1">
<item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>And applied in a layout:
<TextView
android:id="@+id/text_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ListItemTitleStyle" />A Theme is essentially a Style applied to an entire Activity or Application, for example:
<activity android:theme="@style/AppLightTheme.NoActionBar"/>LightListen implements night mode in three steps: custom Style, applying Style attributes, and setting a Theme.
Custom Style
Two Styles are created, each with its own set of color values. Important color attributes include:
colorPrimary – primary theme color
colorAccent – accent color
textColorPrimary – main text color
textColorSecondary – secondary text color
windowBackground – window background color
<style name="AppLightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColorPrimary">@color/colorPrimaryTextBlack</item>
<item name="android:textColorSecondary">@color/colorSubTextBlack</item>
<item name="android:windowBackground">@color/white</item>
</style>
<style name="AppDarkTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="colorPrimary">@color/darkColorPrimary</item>
<item name="colorAccent">@color/darkColorAccent</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/colorSubTextWhite</item>
<item name="android:windowBackground">@color/dark_bg</item>
</style>Custom attributes are declared in <attr name="minibar_background" format="color" /> .
Applying Style Attributes
Custom attributes are accessed with ?attr/ (e.g., <ImageView android:tint="?attr/colorAccent" /> ). System attributes use ?android:attr/ .
Setting Theme
The Theme can be set in the Manifest or programmatically in onCreate :
protected void initTheme() {
if (MusicPreferences.getInstance().isNightMode()) {
setTheme(R.style.AppDarkTheme_NoActionBar);
} else {
setTheme(R.style.AppLightTheme_NoActionBar);
}
}While this approach works well for fixed day/night modes, LightListen also needs multiple theme colors for the day mode, which makes the static Style approach cumbersome.
Dynamic Theme Configuration
To achieve flexible theme switching, LightListen adopts a dynamic configuration inspired by the open‑source app‑theme‑engine library.
compile('com.github.naman14:app-theme-engine:0.5.1@aar') {
transitive = true
}The solution consists of three modules:
Color configuration (stores color values in SharedPreferences)
Color processors (apply colors to Views)
Traversal logic controller (traverses the view hierarchy)
Color Processor
Processors implement a generic interface:
public interface Processor
{
void process(@NonNull Context context, @Nullable String key, @Nullable T target, @Nullable E extra);
}For example, the DefaultProcessor handles most cases. Processors work in three steps: set a tag on a View, parse the tag, and apply the corresponding color.
<TextView
android:tag="text_accent_color"
/>The processing method extracts the tag and calls the appropriate handler:
public void process(@NonNull Context context, @Nullable String key, @Nullable View view, @Nullable Void extra) {
if(view != null && view.getTag() instanceof String) {
String tag = (String)view.getTag();
// handle multiple tags separated by commas
// ...
}
}When the tag is text_accent_color , the processor sets the text color using Config.accentColor(context, key) , which reads the current accent color from the configuration module.
Special Processors
Some Views need special handling, such as ViewPager edge effects. The edge color is changed via reflection:
public static void setEdgeGlowColor(@NonNull ViewPager viewPager, @ColorInt int color) {
if(Build.VERSION.SDK_INT >= 21) {
try {
Field edgeLeft = ViewPager.class.getDeclaredField("mLeftEdge");
edgeLeft.setAccessible(true);
Field edgeRight = ViewPager.class.getDeclaredField("mRightEdge");
edgeRight.setAccessible(true);
EdgeEffectCompat ee = (EdgeEffectCompat)edgeLeft.get(viewPager);
if (ee != null) setEdgeGlowColor(ee, color);
ee = (EdgeEffectCompat)edgeRight.get(viewPager);
if (ee != null) setEdgeGlowColor(ee, color);
} catch (Exception e) { e.printStackTrace(); }
}
}
private static void setEdgeGlowColor(@NonNull EdgeEffectCompat edgeEffect, @ColorInt int color) throws Exception {
if(Build.VERSION.SDK_INT >= 21) {
Field field = EdgeEffectCompat.class.getDeclaredField("mEdgeEffect");
field.setAccessible(true);
EdgeEffect effect = (EdgeEffect) field.get(edgeEffect);
if (effect != null) effect.setColor(color);
}
}Traversal Logic
The traversal controller initializes all processors in a HashMap keyed by class name, then performs a depth‑first search starting from the Activity’s content view. Special ViewGroups like TabLayout are excluded from recursion because they manage their own children.
private static void initProcessors() {
mProcessors = new HashMap();
mProcessors.put("[default]", new DefaultProcessor());
mProcessors.put(ScrollView.class.getName(), new MusicScrollViewProcessor());
// ... other processors ...
}During traversal, the appropriate processor is retrieved either directly by class name or by walking up the superclass chain until a match is found.
Processor processor = mProcessors.get(viewClass.getName());
if(processor == null) {
Class current = viewClass;
do {
current = current.getSuperclass();
if(current == null) break;
processor = mProcessors.get(current.getName());
} while(processor == null);
if(processor == null) processor = mProcessors.get("[default]");
}
return processor;Conclusion
Both approaches have their merits:
Custom Style and Theme – simple, clean, suitable for fixed scenarios.
Dynamic Theme Configuration – more complex but highly flexible, ideal for apps with many theme colors.
Combining them enables LightListen to support both day/night modes and multiple brand colors with minimal code duplication.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.