Mobile Development 15 min read

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.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Implementing Dynamic Theme Switching in the LightListen Android Music Player

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.

mobile developmentAndroidAppThemeEngineDynamicColorstyleTheme
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.