Recently, the project needs to use the function of dynamic skin change without restart. We originally planned to use Android skin-support, which has the most star on Github

However, after a closer look, I found that it was too complicated and I had not maintained and solved a large number of issues for 2 years, so I finally gave up

After exploration, Databinding+LiveData is a low-cost way to achieve reboot-free skin peening

  • Dynamic peels without restart (no need for recreate())
  • No need to make skin packs
  • No additional dependencies (Databinding+LiveData itself is almost a development requirement)
  • Less invasive
  • AppCompat and Material components are supported by default (a few properties require additional support or adaptation)
  • Custom View/ third-party View adaptation process is simple (just write a binding adapter)
  • LayoutInflater.Factory is not required

Define the skin

The following code defines three skins — Default,Day, and Night — that can be dynamically skinned by calling appTheme.update (theme)

The skin only supports ColorStateList, because that’s all you need for most scenarios

Drawable/String and other resources can support this if desired

data class Theme(
    val content: Int.val background: Int.)object Themes {
    val Default = Theme(Color.RED, Color.GRAY)
    val Day = Theme(Color.BLACK, Color.WHITE)
    val Night = Theme(Color.MAGENTA, Color.BLACK)
}

object AppTheme {
    val background = MutableLiveData<ColorStateList>()
    val content = MutableLiveData<ColorStateList>()

    init {
        update(Themes.Default)
    }

    fun update(theme: Theme) {
        background.value = ColorStateList.valueOf(theme.background)
        content.value = ColorStateList.valueOf(theme.content)
    }
}   
Copy the code

Use skins in layout files

By introducing the AppTheme singleton directly, LiveData will be associated with the life cycle without worrying about resource release


      
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="ezy.demo.theme.AppTheme" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:background="#EEEEEE">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            app:background="@{AppTheme.INSTANCE.background}"
            android:gravity="center"
            android:orientation="vertical">

            <SeekBar
                android:layout_width="300dp"
                android:layout_height="wrap_content"
                android:max="100"
                android:progress="50"
                android:progressBackgroundTint="@{AppTheme.INSTANCE.background}"
                android:progressTint="@{AppTheme.INSTANCE.content}"
                android:thumb="@android:drawable/ic_btn_speak_now"
                android:thumbTint="@{AppTheme.INSTANCE.content}" />

            <TextView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_margin="10dp"
                android:gravity="center"
                android:text="Hello World!"
                android:textColor="@{AppTheme.INSTANCE.content}"
                app:drawableTint="@{AppTheme.INSTANCE.content}"
                app:drawableTopCompat="@android:drawable/ic_media_pause" />


            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/btn_default"
                    android:layout_width="100dp"
                    android:layout_height="40dp"
                    android:gravity="center"
                    android:text="Default"
                    android:textColor="@{AppTheme.INSTANCE.content}" />

                <TextView
                    android:id="@+id/btn_day"
                    android:layout_width="100dp"
                    android:layout_height="40dp"
                    android:gravity="center"
                    android:text="Day"
                    android:textColor="@{AppTheme.INSTANCE.content}" />

                <TextView
                    android:id="@+id/btn_night"
                    android:layout_width="100dp"
                    android:layout_height="40dp"
                    android:gravity="center"
                    android:text="Night"
                    android:textColor="@{AppTheme.INSTANCE.content}" />
            </LinearLayout>
        </LinearLayout>


    </FrameLayout>

</layout>
Copy the code

Association life cycle

In fact, Databinding+ObserverableField can also implement reboot-free skin, but ObserverableField can’t be associated with a life cycle and resource release is a bit more troublesome


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        // The association lifecycle
        binding.lifecycleOwner = this

        binding.btnDefault.setOnClickListener { AppTheme.update(Themes.Default) }
        binding.btnDay.setOnClickListener { AppTheme.update(Themes.Day) }
        binding.btnNight.setOnClickListener { AppTheme.update(Themes.Night) }

    }
}
Copy the code

Extend supported skin attributes

DataBinding already supports a large number of attributes, some of which are not, and you need to implement them yourself

It’s really just a matter of writing a binding adapter that supports either third-party controls or custom controls

Here are some examples

@SuppressLint("RestrictedApi")
@BindingMethods(
    BindingMethod(type = ImageView::class, attribute = "tint", method = "setImageTintList")
)
object ThemeAdapter {
    @BindingAdapter("background")
    @JvmStatic
    fun adaptBackground(view: View, value: ColorStateList?). {
        view.setBackgroundColor(Color.WHITE)
        view.backgroundTintList = value 
    }
    @BindingAdapter("drawableTint")
    @JvmStatic
    fun adaptDrawableTint(view: TextView, value: ColorStateList?). {
        if (view is AppCompatTextView) {
            view.supportCompoundDrawablesTintList = value
        }
    } 
    @BindingAdapter("android:progressBackgroundTint")
    @JvmStatic
    fun adaptProgressBackgroundTint(view: SeekBar, value: ColorStateList?). {
        view.progressBackgroundTintList = value 
    }
}
Copy the code

Demo

Github.com/czy1121/The…