Android Multi Theme Switch Library ,use kotlin language ,coroutine ,and so on ...

Overview

Magic Mistletoe Android多主题(换肤)切换框架

背景

时隔四年,在网易换肤之前的思路下,做了几点改进,现在完全通过反射创建View,并且在SkinLoadManager中提供一个configCustomAttrs以支持自定义View的属性插队替换

摈弃了之前的AsyncTask,使用kotlin 协程进行主题包的资源转换

使用kotlin重构所有Java代码实现

效果展示

默认主题

点击切换主题

最佳使用方式

STEP 1 宿主项目(示例Demo为本项目app模块)依赖多主题框架AAR

//in root build.gradle
allprojects {
 	repositories {
 		maven { url 'https://jitpack.io' }
 	}
 }
// in app/module build.gradle
dependencies {
	        implementation 'com.github.mistletoe5215:MagicMistletoe:1.0.0'
	}

STEP 2 制作多主题包

找个壳工程(本Demo为theme-pkg模块),在res下放置资源文件,注意 资源文件名称需要与宿主app内的资源文件名称保持一致,这样主题切换的时候才可以成功替换

执行打包命令

找到生成的资源apk

重命名为你喜欢的名字.zip(如果有强迫症)

这里是做演示,(实际商用过程中,可将zip包给服务端,进行主题包签名处理,后通过下载的形式down到本地,解签,应用)拷贝主题文件至宿主app的assets目录下

释放assets目录主题文件至私有路径

   private fun copyAssetAndWrite(fileName: String): Boolean {
          try {
              val cacheDir = cacheDir
              if (!cacheDir.exists()) {
                  cacheDir.mkdirs()
              }
              val outFile = File(cacheDir, fileName)
              if (!outFile.exists()) {
                  val res = outFile.createNewFile()
                  if (!res) {
                      return false
                  }
              } else {
                  if (outFile.length() > 10) { 
                      return true
                  }
              }
              val `is`: InputStream = assets.open(fileName)
              val fos = FileOutputStream(outFile)
              val buffer = ByteArray(1024)
              var byteCount: Int
              while (`is`.read(buffer).also { byteCount = it } != -1) {
                  fos.write(buffer, 0, byteCount)
              }
              fos.flush()
              `is`.close()
              fos.close()
              return true
          } catch (e: IOException) {
              e.printStackTrace()
          }
          return false
      }

STEP 3 XML中需要更换主题的控件设置好多主题属性 multiTheme:enable="true"

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
   xmlns:multiTheme="http://schemas.android.com/apk/multi.theme"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@color/main_bg"
   multiTheme:enable="true"
   tools:context=".MainActivity">
   <com.google.android.material.imageview.ShapeableImageView
       android:src="@drawable/ic_avatar"
       android:layout_marginTop="20dp"
       android:layout_width="220dp"
       android:layout_height="220dp"
       android:scaleType="centerCrop"
       android:id="@+id/avatar"
       android:background="@color/main_text"
       multiTheme:enable="true"
       app:shapeAppearance="@style/CircleStyle"
       app:layout_constraintBottom_toTopOf="@+id/copy"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />


   <TextView
       android:background="@color/teal_200"
       android:layout_marginTop="20dp"
       android:layout_width="match_parent"
       android:layout_height="100dp"
       android:gravity="center"
       android:id="@+id/copy"
       android:text="释放主题包到应用内存(切换前必点)"
       android:textColor="@color/main_text"
       multiTheme:enable="true"
       app:layout_constraintBottom_toTopOf="@+id/change_skin"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/avatar" />

   <TextView
       android:background="@color/teal_200"
       android:layout_marginTop="20dp"
       android:layout_width="match_parent"
       android:layout_height="100dp"
       android:gravity="center"
       android:id="@+id/change_skin"
       android:text="切换主题"
       android:textColor="@color/main_text"
       multiTheme:enable="true"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/copy" />

</androidx.constraintlayout.widget.ConstraintLayout>


注意,如果自定义View在代码中去手动用代码显式设置文字颜色、View背景以及图片资源的场景,实现多主题切换,需要调用以下方式获取资源id,而不是直接引用R文件的资源id
 //获取多主题色值ID
 SkinLoadManager.getInstance().getColor(attrValue)
 //获取多主题图片ID
 SkinLoadManager.getInstance().getDrawable(attrValue)
 //获取多主题字符串ID
 SkinLoadManager.getInstance().getTextString(attrValue)
 

STEP 4 代码执行切换

   /**
    * 在Application中初始化多主题框架
    **/ 
   SkinLoadManager.getInstance().init(application)
   /**
    * Activity onCreate 前设置`multiThemeFactory`
    **/
   registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
              override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
                  super.onActivityPreCreated(activity, savedInstanceState)
                  activity.layoutInflater.factory = SkinLoadManager.getInstance().multiThemeFactory
              }
  
              override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
  
              }
  
              override fun onActivityStarted(activity: Activity) {
  
              }
  
              override fun onActivityResumed(activity: Activity) {
  
              }
  
              override fun onActivityPaused(activity: Activity) {
  
              }
  
              override fun onActivityStopped(activity: Activity) {
  
              }
  
              override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
  
              }
  
              override fun onActivityDestroyed(activity: Activity) {
  
              }
  
          })
    /**
     * 换肤前,将换肤包从assets目录拷入应用内
     **/
    ArchTaskExecutor.getIOThreadExecutor().execute {
               copyAssetAndWrite(fileName)
               ArchTaskExecutor.getMainThreadExecutor().execute {
                   Toast.makeText(this,"成功",Toast.LENGTH_SHORT).show()
               }
    }
    /**
     * 传入拷贝后的多主题路径,执行多主题切换
     **/
    val dataFile = File(cacheDir, fileName)
    SkinLoadManager.getInstance().loadSkin(dataFile.absolutePath, object : ILoadListener {
                      override fun onStart() {
                          Log.i("Mistletoe", "onStart")
                      }
  
                      override fun onSuccess() {
                          Log.i("Mistletoe", "onSuccess")
                      }
  
                      override fun onFailed(e: SkinLoadException) {
                          Log.e("Mistletoe", "onFailed:${e.message}")
                      }
  
   })
     /**
      * 切换回App默认主题
      **/
    SkinLoadManager.getInstance().restoreDefaultTheme()

拓展使用

有时候我们想自定义一些主题切换属性实现,怎么办?使用SkinLoadManagerconfigCustomAttrs

比如,我们现在想在头像前面盖一个前景色,切换主题后,前景色也跟着切换,它的实现如下

  • 新建前景色属性Attr类,继承com.magic.multi.theme.core.base.BaseAttr,实现其apply方法

  • 如果属性与系统属性名称不一致,需要立即apply,则复写BaseAttr的applyImmediate为true

/**
* Created by mistletoe
* on 7/28/21
**/
class ImageForegroundAttr:BaseAttr() {
   override fun apply(view: View?) {
      when(view){
          is AppCompatImageView ->{
              if ("foreground".equals(attrName, true)) {
                  view.foreground = SkinLoadManager.getInstance().getDrawable(attrValue)
              }
          }
          is ImageView ->{
              if ("foreground".equals(attrName, true)) {
                  view.foreground = SkinLoadManager.getInstance().getDrawable(attrValue)
              }
          }
      }
   }
}
  • foreground(注意,这个key不可以乱取,必须与xml中前景属性的名字一致)与ImageForegroundAttr 分别作为key-value, 放入configCustomAttrs
        //增加一个设置前景图片属性
        val configMap = mutableMapOf<String,Class<out BaseAttr>>().apply {
                  put("foreground",ImageForegroundAttr::class.java)
        }
        SkinLoadManager.getInstance().configCustomAttrs(configMap)
  • 预先在宿主app和主题包中定义image_foreground_modal相同名称的前景drawable资源,宿主app前景色为#4DFFFFFF(30%的灰白) ,皮肤包为#330000FF(20%的淡蓝)

宿主app

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#4DFFFFFF" />
    <corners android:radius="110dp" />
</shape>

主题包zip

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="rectangle">
   <solid android:color="#330000FF" />
   <corners android:radius="110dp" />
</shape>
  • 在布局中声明使用
  <com.google.android.material.imageview.ShapeableImageView
        android:src="@drawable/ic_avatar"
        android:layout_marginTop="20dp"
        android:layout_width="220dp"
        android:layout_height="220dp"
        android:scaleType="centerCrop"
        android:id="@+id/avatar"
        android:background="@color/main_text"
        android:foreground="@drawable/image_foreground_modal"
        multiTheme:enable="true"
        app:shapeAppearance="@style/CircleStyle"
        app:layout_constraintBottom_toTopOf="@+id/copy"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  • 可以观察到

初始头像的前景色

切换主题后的前景色

这样一个前景色的自定义主题切换设置就完成了

You might also like...
MVVM ,Hilt DI ,LiveData ,Flow ,SharedFlow ,Room ,Retrofit ,Coroutine , Navigation Component ,DataStore ,DataBinding , ViewBinding, Coil
MVVM ,Hilt DI ,LiveData ,Flow ,SharedFlow ,Room ,Retrofit ,Coroutine , Navigation Component ,DataStore ,DataBinding , ViewBinding, Coil

RickMorty This is a simple app which has been implemented using Clean Architecture alongside MVVM design to run (online/offline) using : [ MVVM ,Hilt

Clean Android multi-module offline-first scalable app in 2022. Including Jetpack Compose, MVI, Kotlin coroutines/Flow, Kotlin serialization, Hilt and Room.

Android Kotlin starter project - 2022 edition Android starter project, described precisely in this article. Purpose To show good practices using Kotli

Kotlin multi-platform application navigation library.

navigation Kotlin multi-platform application navigation library. Supports Jetpack Compose. val navigator = rememberNavigatorByKey("Greeting") { key -

Kotlin multi-platform simple File I/O library

KmpIO This is a Kotlin multiplatform (KMP) library for basic Text file, Binary file, and zip/archive file IO. It was initially implemented with the an

It is a project that contains lessons and examples about Kotlin programming language. 🇰
It is a project that contains lessons and examples about Kotlin programming language. 🇰

Kotlin Tutorials What is Kotlin? I added the platforms it supports and great resources. You can access the article from the link below: https://medium

The Okila server project uses the Spring boot framework and uses the Kotlin language

Okila-Server The Okila server project uses the Spring boot framework and uses the Kotlin language Format Response //The response successfully format

Kotlin multi platform project template and sample app with everything shared except the UI. Built with clean architecture + MVI
Kotlin multi platform project template and sample app with everything shared except the UI. Built with clean architecture + MVI

KMMNewsAPP There are two branches Main News App Main The main branch is a complete template that you can clone and use to build the awesome app that y

A showcase music app for Android entirely written using Kotlin language

Bandhook Kotlin This project is a small replica of the app I developed some time ago. Bandhook can still be found on Play Store At the moment it will

101 examples for Kotlin Programming language.

This is a collection of runnable console applications that highlights the features of Kotlin programming language. The use of console application enab

Releases(1.5.0)
Owner
Mistletoe
Mistletoe
Muhammad Valian Masdani 2 Jul 5, 2022
An application with the use of Kotlin can change the color of the text, and the background with the press of a button and switch.

An application with the use of Kotlin can change the color of the text, and the background with the press of a button and switch.

Robert Velasquez 2 Jul 20, 2022
Demonstration of Object Pool Design Pattern using Kotlin language and Coroutine

Object Pool Design Pattern with Kotlin Demonstration of Thread Safe Object Pool Design Pattern using Kotlin language and Coroutine. Abstract The objec

Enes Kayıklık 7 Apr 12, 2022
An Interpreter/Transpiler for the Folders esoteric programming language, a language with no code and just folders

Folders2kt ?? An Interpreter/Transpiler of the Folders esoteric programming language, a language with no code and just folders, written in Kotlin Show

Jens Klingenberg 18 Jan 4, 2023
A complete Kotlin application built to demonstrate the use of Modern development tools with best practices implementation using multi-module architecture developed using SOLID principles

This repository serves as template and demo for building android applications for scale. It is suited for large teams where individuals can work independently on feature wise and layer wise reducing the dependency on each other.

Devrath 11 Oct 21, 2022
New Relic Kotlin Instrumentation for Kotlin Coroutine. It successfully handles thread changes in suspend states.

new-relic-kotlin-coroutine New Relic Kotlin Instrumentation for Kotlin Coroutine. It successfully handles thread changes in suspend states. Usage 1- U

Mehmet Sezer 7 Nov 17, 2022
R2DBC Sharding Example with Kotlin Coroutine

R2DBC Sharding Example with Kotlin Coroutine A minimal sharding example with R2DBC and coroutine, where user table is divided into 2 different shards.

K.S. Yim 0 Oct 4, 2021
Kreds - a thread-safe, idiomatic, coroutine based Redis client written in 100% Kotlin

Kreds Kreds is a thread-safe, idiomatic, coroutine based Redis client written in 100% Kotlin. Why Kreds? Kreds is designed to be EASY to use. Kreds ha

Abhijith Shivaswamy 117 Dec 23, 2022
🚀 🥳 MVVM based sample currency converter application using Room, Koin, ViewModel, LiveData, Coroutine

Currency Converter A demo currency converter app using Modern Android App Development techniques Tech stack & Open-source libraries Minimum SDK level

Abinash Neupane 2 Jul 17, 2022