安卓主题换肤功能
主题换肤
文章目录
- 主题换肤
- 第一章 前言
- 第01节 提出问题
- 第02节 原理说明
- 第03节 效果演示
- 第二章 案例
- 第01节 项目结构
- 第02节 核心API
- 第03节 skin包
- 第04节 ui包
- 第05节 layout资源
- 第06节 drawable资源
- 第07节 colors 资源
第一章 前言
第01节 提出问题
需求场景
在一些 App 或者 系统中, 会出现主题换肤的功能。常见的需求场景:1、白天黑夜模式:白天场景下, 显示文字偏向深色, 背景显示亮色。 黑夜模式下, 显示文字偏向亮色, 背景显示深色。2、哀悼模式:一些灾难性的纪念日, 互联网应用, 出现浅灰色, 表示当前的一种沉重心情3、节日主题:根据当前的节日, 展示出不同样式的效果。 例如: 春节、清明节、中秋节、国庆节...4、节气主题:根据中国的24节气, 选择不同样式效果展示。 例如: 春分、秋分、夏至、冬至、芒种、谷雨...5、特殊主题:根据一些特定的活动下, 显示不同的主题效果。 例如: 11.11 购物节、 12.12购物节、6.18购物节...6、环球实事:根据最新的环球实事, 显示不同的主题效果。 例如: 奥运会期间... 315两会期间..7、付费主题:特殊的主题, 有付费VIP要求
那么这些主题效果,在 App或者系统中,如何实现呢?
方案一: App定义基础的主题样式 color 或者 style 根据 style 来分别显示不同的主题效果
方案二:下载皮肤包, 加载不同的皮肤包, 实现皮肤变换的效果对比说明: 针对于方案一, 定义 style 样式的实现, 这里可能在场景比较少的情况下, 可以实现。如: 白天黑夜模式, 可以这样进行处理但是如果主题换肤的场景, 特别多的情况下, 我们采用方案一, 那么会存在下面的几种弊端:1、会增加 APP 的体积大小2、资源耦合性会特别高, 一次资源文件的修改, 可能多处都要做调整。因此, 大多数更多复杂场景的情况下, 我们采用的是方案二的实现。 也就是 下载皮肤包, 实现主题换肤下面我们介绍一下, 如何实现皮肤包, 换肤的功能。
第02节 原理说明
基本思路
我们通过 AssetManager 和 PackageInfo 去动态加载资源包下面的文件。1、在每次界面显示时, 初始化监听器
2、在每次界面销毁时, 释放掉监听器
3、在每次皮肤变更时, 通知监听器, 将所有注册过 监听器的类 (承载UI的类 Activity、Fragment、Dialog、ViewGroup、View) 去更新UI
4、在每次界面初始化时, 通知UI显示最新的主题样式
原理图解
第03节 效果演示
效果图演示(默认样式)
效果图演示(春天样式)
效果图演示(夏天样式)
效果图演示(秋天样式)
效果图演示(冬天样式)
第二章 案例
第01节 项目结构
项目结构说明
第02节 核心API
对外提供 皮肤 API 接口
名称 | 参数 | 返回值 | 说明 |
---|---|---|---|
addSkipKeyMapSkipName | Map<String, String> map 映射表 | void | 采用 Map 批量添加映射表的方案 |
addSkipKeyMapSkipName | String skipKey 皮肤的键 String skipName 皮肤的值 | void | 采用 键值对的方式, 添加映射表方案 |
clearKeyMapName | 无 | void | 清除键值对映射的皮肤表 |
loadSkin | Context context 上下文对象 String skipKey 皮肤的键 | void | 加载皮肤资源 |
loadSkin | Context context 上下文对象 String skipKey 皮肤的键 boolean isNotifySkinChanged 是否通知皮肤自动更新 | void | 加载皮肤资源 |
loadSkin | Context context 上下文对象 String skipKey 皮肤的键 InputStream is 输入流 boolean isFinishCloseStream 是否通知皮肤自动更新 | void | 加载皮肤资源 |
loadSkin | Context context 上下文对象 String skinKey 皮肤的键 boolean isNotifySkinChanged 是否通知皮肤自动更新 InputStream is 输入流 boolean isFinishCloseStream 是否操作完毕以后, 需要关闭输入流 | void | 加载皮肤资源 |
onRegister | ISkinListener listener 监听器 | void | 注册监听器 |
unRegister | ISkinListener listener 监听器 | void | 注销监听器 |
restoreDefaultSkin | 无 | void | 恢复默认皮肤 |
updateView | Context context 上下文 View view 需要修改的View或者ViewGroup 类 AttrsName attrsName 需要修改的属性 int resourceId 需要修改的指定资源ID | void | 更新界面 |
操作步骤介绍
1、需要在 Application 或者入口 Activity 当中, 初始化皮肤映射表
2、在UI界面(Activity、Fragment、Dialog、View、ViewGroup) 当中 生命周期启动时, 注册监听器
3、在UI界面(Activity、Fragment、Dialog、View、ViewGroup) 当中 生命周期启动结束时, 注销监听器
4、在UI界面(Activity、Fragment、Dialog、View、ViewGroup) 当中 生命周期启动时, 调用更新的方法(具体更新 updateView 完成)
第03节 skin包
枚举 AttrsName
// 控制皮肤操作的属性 attribute
public enum AttrsName {textColor,drawableImage,backgroundColor,drawableSelector
}
监听接口 ISkinListener
// 通知皮肤变更了
public interface ISkinListener {void onSkinChanged();
}
皮肤属性 SkinAttribute
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;// 皮肤的属性
public class SkinAttribute {private Resources mSkinResources;private String mSkinPackageName;private static final String TAG = SkinAttribute.class.getSimpleName();/**** 设置皮肤资源** @param skinResources*/void setSkinResources(Resources skinResources) {this.mSkinResources = skinResources;}/**** 设置皮肤的包名称** @param skinPackageName*/void setSkinPackageName(String skinPackageName) {this.mSkinPackageName = skinPackageName;}/*** 清除属性数据*/void clearAttribute() {mSkinResources = null;mSkinPackageName = null;}/*** 获取资源ID** @param resId 原始资源ID* @return 皮肤资源ID*/float getDimension(Context context, int resId) {float dimensionDefault = context.getResources().getDimension(resId);if (mSkinResources == null) {return dimensionDefault;}String resName = context.getResources().getResourceEntryName(resId);String resType = context.getResources().getResourceTypeName(resId);int skinResId = mSkinResources.getIdentifier(resName, resType, mSkinPackageName);return skinResId == 0 ? dimensionDefault : mSkinResources.getDimension(skinResId);}/*** 获取资源ID** @param resId 原始资源ID* @return 皮肤资源ID*/Drawable getSelector(Context context, int resId) {Drawable selectorDefault = context.getResources().getDrawable(resId);if (mSkinResources == null) {return selectorDefault;}String resName = context.getResources().getResourceEntryName(resId);String resType = context.getResources().getResourceTypeName(resId);int skinResId = mSkinResources.getIdentifier(resName, resType, mSkinPackageName);return skinResId == 0 ? selectorDefault : mSkinResources.getDrawable(skinResId);}/*** 获取资源ID** @param resId 原始资源ID* @return 皮肤资源ID*/int getColor(Context context, int resId) {int colorDefault = context.getResources().getColor(resId);if (mSkinResources == null) {return colorDefault;}String resName = context.getResources().getResourceEntryName(resId);String resType = context.getResources().getResourceTypeName(resId);int skinResId = mSkinResources.getIdentifier(resName, resType, mSkinPackageName);return skinResId == 0 ? colorDefault : mSkinResources.getColor(skinResId);}/*** 获取Drawable资源** @param resId 原始资源ID* @return 皮肤资源ID*/Drawable getDrawable(Context context, int resId) {Drawable drawableDefault = context.getResources().getDrawable(resId);if (mSkinResources == null) {return drawableDefault;}String resName = context.getResources().getResourceEntryName(resId);String resType = context.getResources().getResourceTypeName(resId);int skinResId = mSkinResources.getIdentifier(resName, resType, mSkinPackageName);return skinResId == 0 ? drawableDefault : mSkinResources.getDrawable(skinResId);}/**** 更新界面* @param context 上下文* @param view 需要更新的 View 对象* @param attrsName 操作的属性名称* @param resourceId 操作的资源ID*/protected void updateView(Context context, View view, AttrsName attrsName, int resourceId) {// 文字的颜色if (attrsName == AttrsName.textColor) {if (view instanceof TextView) {TextView textView = (TextView) view;int textColor = getColor(context, resourceId);textView.setTextColor(textColor);}}// 选择器if (attrsName == AttrsName.drawableSelector) {Drawable selector = getSelector(context, resourceId);view.setBackground(selector);}// 背景颜色if (attrsName == AttrsName.backgroundColor) {int backgroundColor = getColor(context, resourceId);view.setBackgroundColor(backgroundColor);}// 图片资源if (attrsName == AttrsName.drawableImage) {Drawable drawable = getDrawable(context, resourceId);if (view instanceof ImageView) {((ImageView) view).setImageDrawable(drawable);}}}
}
皮肤下载 SkinDownLoad
import android.content.Context;import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;// 皮肤的下载类
public final class SkinDownLoad {private static final int DEFAULT_BUFFER_SIZE = 1024;/*** 加载皮肤到本地来自于 Assets 文件夹下面的路径** @param context 上下文的对象* @param skipName 在Assets当中文件的路径和名称* @return 返回的是皮肤文件的绝对路径*/static String loadFileByAssets(Context context, String skipName) {File file = createSkipDir(context, skipName);try {InputStream inputStream = context.getAssets().open(skipName);load(inputStream, file, true);} catch (IOException e) {if (e instanceof FileNotFoundException) {throw new SkinNotFoundException(SkinNotFoundException.MESSAGE_ERROR_SOURCE);}throw new RuntimeException(e);}return file.getAbsolutePath();}/**** 加载皮肤到本地, 来自于外部的 InputStream* @param context 上下文* @param skipFileName 保存在本地的名称* @param inputStream 外部的输入流对象* @param isFinishCloseStream 完成之后, 是否需要释放资源* @return 返回的是皮肤文件的绝对路径*/static String loadFileByInputStream(Context context, String skipFileName,InputStream inputStream, boolean isFinishCloseStream) {File file = createSkipDir(context, skipFileName);load(inputStream, file, isFinishCloseStream);return file.getAbsolutePath();}/**** 创建本地文件夹地址** @param context 上下文对象* @param skipFileName 皮肤的名称 (xxx.apk)*/private static File createSkipDir(Context context, String skipFileName) {String skipDirectory = context.getDataDir() + "/skip/";File file = new File(skipDirectory);if (!file.exists()) {boolean mkdirs = file.mkdirs();}// 本地的皮肤文件return new File(file, skipFileName);}/**** 具体下载皮肤文件的操作** @param inputStream 皮肤的输入流对象* @param localSkipFile 本地皮肤文件*/private static void load(InputStream inputStream, File localSkipFile, boolean isFinishCloseStream) {BufferedInputStream bis = null;BufferedOutputStream bos = null;try {bis = new BufferedInputStream(inputStream, DEFAULT_BUFFER_SIZE);bos = new BufferedOutputStream(new FileOutputStream(localSkipFile), DEFAULT_BUFFER_SIZE);byte[] array = new byte[DEFAULT_BUFFER_SIZE];int len;while ((len = bis.read(array)) != -1) {bos.write(array, 0, len);bos.flush();}} catch (IOException e) {throw new RuntimeException(e);} finally {if (isFinishCloseStream) {if (bis != null) {try {bis.close();} catch (IOException e) {throw new RuntimeException(e);}}}if (bos != null) {try {bos.close();} catch (IOException e) {throw new RuntimeException(e);}}}}
}
皮肤管理 SkinManager
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;// 皮肤管理类
public class SkinManager {// 管理所有的 UI 进行界面的处理private final List<WeakReference<ISkinListener>> list = new ArrayList<>();// 皮肤的属性类private final SkinAttribute skipAttribute;private static final String TAG = SkinManager.class.getSimpleName();private static volatile SkinManager instance;private SkinManager() {skipAttribute = new SkinAttribute();}public static SkinManager getInstance() {if (instance == null) {synchronized (SkinManager.class) {if (instance == null) {instance = new SkinManager();}}}return instance;}/**** 清除键值对映射的皮肤表*/public void clearKeyMapName() {SkinMapTable.getInstance().clear();}/**** 采用 键值对的方式, 添加映射表方案** @param skipKey 皮肤的键* @param skipName 皮肤的值*/public void addSkipKeyMapSkipName(String skipKey, String skipName) {SkinMapTable.getInstance().putMapData(skipKey, skipName);}/**** 采用 Map 批量添加映射表的方案* @param map 键值对*/public void addSkipKeyMapSkipName(Map<String, String> map) {SkinMapTable.getInstance().putMapData(map);}/*** 加载皮肤资源** @param context 上下文对象* @param skipKey 皮肤的Key信息*/public void loadSkin(Context context, String skipKey) {loadSkin(context, skipKey, true);}/*** 加载皮肤资源** @param context 上下文对象* @param skipKey 皮肤的Key信息* @param isNotifySkinChanged 是否立即更新*/public void loadSkin(Context context, String skipKey, boolean isNotifySkinChanged) {try {String skipName = SkinMapTable.getInstance().getSkipName(skipKey);if (TextUtils.isEmpty(skipName)) {throw new SkinNotFoundException(SkinNotFoundException.MESSAGE_MAP_MATCHING);}String skinPath = SkinDownLoad.loadFileByAssets(context, skipName);loadAssetManagerByPackageInfo(context, skinPath);Log.d(TAG, "皮肤加载成功: " + skinPath);} catch (Exception e) {Log.e(TAG, "皮肤加载失败: " + e.getMessage());}// 通知UI更新if (isNotifySkinChanged) {notifySkinChanged();}}/*** 加载皮肤资源, 通过 InputStream 加载** @param context 上下文对象* @param skipKey 皮肤的 Key信息* @param is 流对象* @param isFinishCloseStream 是否自动关闭流对象*/public void loadSkin(Context context, String skipKey, InputStream is, boolean isFinishCloseStream) {loadSkin(context, skipKey, true, is, isFinishCloseStream);}/*** 加载皮肤资源, 通过 InputStream 加载** @param context 上下文对象* @param skinKey 皮肤的 Key信息* @param isNotifySkinChanged 是否立即更新* @param is 流对象* @param isFinishCloseStream 是否自动关闭流对象*/public void loadSkin(Context context, String skinKey, boolean isNotifySkinChanged, InputStream is, boolean isFinishCloseStream) {try {String skinName = SkinMapTable.getInstance().getSkipName(skinKey);if (TextUtils.isEmpty(skinName)) {throw new SkinNotFoundException(SkinNotFoundException.MESSAGE_MAP_MATCHING);}String skinPath = SkinDownLoad.loadFileByInputStream(context, skinKey, is, isFinishCloseStream);loadAssetManagerByPackageInfo(context, skinPath);Log.d(TAG, "皮肤加载成功: " + skinPath);} catch (Exception e) {Log.e(TAG, "皮肤加载失败: " + e.getMessage());}// 通知UI更新if (isNotifySkinChanged) {notifySkinChanged();}}/*** 加载 AssetManager 下面的资源数据** @param context 上下文* @param skinPath 皮肤路径*/private void loadAssetManagerByPackageInfo(Context context, String skinPath) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {// 获取AssetManager实例AssetManager assetManager = AssetManager.class.newInstance();// 反射调用addAssetPath方法Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, skinPath);Resources superRes = context.getResources();// 创建新的Resources实例Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());skipAttribute.setSkinResources(resources);// 获取皮肤包名PackageInfo packageInfo = context.getPackageManager().getPackageArchiveInfo(skinPath, 0);skipAttribute.setSkinPackageName(packageInfo == null ? "" : packageInfo.packageName);}/*** 恢复默认皮肤*/public void restoreDefaultSkin() {skipAttribute.clearAttribute();notifySkinChanged();Log.d(TAG, "还原至默认皮肤");}/*** 注册监听器** @param listener 监听器*/public void onRegister(ISkinListener listener) {if (listener != null) {list.add(new WeakReference<>(listener));}}/*** 注销监听器** @param listener 监听器对象*/public void unRegister(ISkinListener listener) {if (listener != null) {int index = -1;for (int i = 0; i < list.size(); i++) {ISkinListener element = list.get(i).get();if (Objects.equals(listener, element)) {index = i;break;}}if (index >= 0) {list.remove(index);}}}/**** 更新界面* @param context 上下文* @param view 需要更新的 View 对象* @param attrsName 操作的属性名称* @param resourceId 操作的资源ID*/public void updateView(Context context, View view, AttrsName attrsName, int resourceId) {if (view == null) {Log.e(TAG, context.getClass().getSimpleName() + "中 updateView的View 对象是空值, 请确认该 View 已经被初始化 findViewById ?");return;}skipAttribute.updateView(context, view, attrsName, resourceId);}/*** 通知皮肤改变(内部调用)*/private void notifySkinChanged() {// 这里可以通过EventBus或其他方式通知Activity更新UI// 例如: EventBus.getDefault().post(new SkinChangeEvent());for (WeakReference<ISkinListener> reference : list) {reference.get().onSkinChanged();}}
}
皮肤映射表 SkinMapTable
import java.util.HashMap;
import java.util.Map;// 皮肤映射表
public class SkinMapTable {private final Map<String, String> map = new HashMap<>();private static volatile SkinMapTable instance;private SkinMapTable() {// 初始化init();}public static SkinMapTable getInstance() {if (instance == null) {synchronized (SkinMapTable.class) {if (instance == null) {instance = new SkinMapTable();}}}return instance;}private void init() {map.put("default", "skin_default.apk");map.put("spring", "skin_spring.apk");map.put("summer", "skip_summer.apk");map.put("autumn", "skip_autumn.apk");map.put("winter", "skip_winter.apk");}/*** 加载 皮肤和映射表信息** @param map 皮肤映射表, 支持直接添加 Map 集合*/void putMapData(Map<String, String> map) {this.map.putAll(map);}/**** 加载 皮肤和映射表信息** @param skipKey 皮肤的键* @param skipName 皮肤的值*/void putMapData(String skipKey, String skipName) {map.put(skipKey, skipName);}/**** 通过映射表中的 skipKey, 获取皮肤名称** @param skipKey 皮肤的键* @return 皮肤的值*/String getSkipName(String skipKey) {return map.getOrDefault(skipKey, "");}/**** 清除映射表*/void clear() {map.clear();}
}
皮肤异常类 SkinNotFoundException
/**** 皮肤文件未找到的异常类*/
public class SkinNotFoundException extends RuntimeException {// 皮肤文件没有找到, 检测映射表当中的 KEY 和 VALUE 是否匹配static final String MESSAGE_MAP_MATCHING = "皮肤没有找到, 检测 映射表SkinMapTab中key 和 SkinManager.loadSkin中的skinKey 是否匹配";static final String MESSAGE_ERROR_SOURCE = "皮肤没有找到, 原始皮肤资源 assets中是否存在 映射表SkinMapTab中 skipName 对应的皮肤资源";public SkinNotFoundException() {super();}public SkinNotFoundException(String message) {super(message);}public SkinNotFoundException(String message, Throwable cause) {super(message, cause);}public SkinNotFoundException(Throwable cause) {super(cause);}public SkinNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
第04节 ui包
启动类 BootApplication
import android.app.Application;import com.example.hello.skin.SkinManager;import java.util.HashMap;
import java.util.Map;// 启动类
public class BootApplication extends Application {@Overridepublic void onCreate() {super.onCreate();Map<String, String> map = new HashMap<>();map.put("spring", "skin_spring.apk");map.put("summer", "skin_summer.apk");map.put("autumn", "skin_autumn.apk");map.put("winter", "skin_winter.apk");map.put("default", "skin_default.apk");// 清除之前的皮肤映射表SkinManager.getInstance().clearKeyMapName();// 添加新的皮肤映射表SkinManager.getInstance().addSkipKeyMapSkipName(map);}
}
自定义布局 ViewGroup
import android.content.Context;
import android.util.AttributeSet;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;import com.example.hello.R;
import com.example.hello.skin.AttrsName;
import com.example.hello.skin.ISkinListener;
import com.example.hello.skin.SkinManager;// 自定义布局 VuewGroup
public class CustomLayout extends ConstraintLayout implements ISkinListener {public CustomLayout(@NonNull Context context) {super(context);}public CustomLayout(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public CustomLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();// 注册SkinManager.getInstance().onRegister(this);onSkinChanged();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();// 注销SkinManager.getInstance().unRegister(this);}@Overridepublic void onSkinChanged() {SkinManager.getInstance().updateView(getContext(), this, AttrsName.backgroundColor, R.color.bg_color);}
}
自定义视图 View
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;import androidx.annotation.Nullable;import com.example.hello.R;
import com.example.hello.skin.AttrsName;
import com.example.hello.skin.ISkinListener;
import com.example.hello.skin.SkinManager;// 自定义视图 View
public class CustomView extends View implements ISkinListener {public CustomView(Context context) {super(context);}public CustomView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();// 注册SkinManager.getInstance().onRegister(this);onSkinChanged();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();// 注销SkinManager.getInstance().unRegister(this);}@Overridepublic void onSkinChanged() {SkinManager.getInstance().updateView(getContext(), this, AttrsName.backgroundColor, R.color.view_color);}
}
首页 HomeActivity
import android.content.Context;
import android.os.Bundle;import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;import com.example.hello.R;
import com.example.hello.skin.AttrsName;
import com.example.hello.skin.ISkinListener;
import com.example.hello.skin.SkinManager;// 首页的 Activity 主要关注 导航栏 和 自定义 View ViewGroup
public class HomeActivity extends AppCompatActivity implements ISkinListener {private AppCompatTextView titleView;private AppCompatImageView imageViewHome;private AppCompatImageView imageViewPoster;private ConstraintLayout titleBar;private final Context context = HomeActivity.this;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_home);// 注册SkinManager.getInstance().onRegister(this);titleBar = findViewById(R.id.rl_title_bar);titleView = findViewById(R.id.text_view_title);imageViewHome = findViewById(R.id.image_view_home);imageViewPoster = findViewById(R.id.image_view_poster);imageViewHome.setOnClickListener(view -> finish());onSkinChanged();}@Overrideprotected void onDestroy() {super.onDestroy();// 注销SkinManager.getInstance().unRegister(this);}@Overridepublic void onSkinChanged() {SkinManager.getInstance().updateView(context, titleView, AttrsName.textColor, R.color.text_color);SkinManager.getInstance().updateView(context, imageViewHome, AttrsName.drawableImage, R.mipmap.icon_home);SkinManager.getInstance().updateView(context, imageViewPoster, AttrsName.drawableImage, R.drawable.icon_bg_poster);SkinManager.getInstance().updateView(context, titleBar, AttrsName.backgroundColor, R.color.bg_color);SkinManager.getInstance().updateView(context, imageViewPoster, AttrsName.drawableImage, R.drawable.icon_bg_poster);}
}
入口 MainActivity
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;import com.example.hello.R;
import com.example.hello.skin.AttrsName;
import com.example.hello.skin.ISkinListener;
import com.example.hello.skin.SkinManager;// 入口 MainActivity 主要是设置不同主题皮肤 和 图片 文字 按钮选择器等
public class MainActivity extends AppCompatActivity implements ISkinListener {private AppCompatTextView titleView;private AppCompatTextView buttonViewTest;private AppCompatTextView buttonViewSpring;private AppCompatTextView buttonViewSummer;private AppCompatTextView buttonViewAutumn;private AppCompatTextView buttonViewWinter;private AppCompatTextView buttonViewDefault;private AppCompatImageView imageViewHome;private AppCompatImageView imageViewPoster;private ConstraintLayout titleBar;private final Context context = MainActivity.this;private static final String TAG = MainActivity.class.getSimpleName();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 注册SkinManager.getInstance().onRegister(this);titleBar = findViewById(R.id.rl_title_bar);buttonViewTest = findViewById(R.id.button_view_test);buttonViewSpring = findViewById(R.id.button_view_spring);buttonViewSummer = findViewById(R.id.button_view_summer);buttonViewAutumn = findViewById(R.id.button_view_autumn);buttonViewWinter = findViewById(R.id.button_view_winter);buttonViewDefault = findViewById(R.id.button_view_default);titleView = findViewById(R.id.text_view_title);imageViewHome = findViewById(R.id.image_view_home);imageViewPoster = findViewById(R.id.image_view_poster);imageViewHome.setOnClickListener(view -> {Intent intent = new Intent(context, HomeActivity.class);startActivity(intent);});buttonViewSpring.setOnClickListener(view -> {// 加载皮肤SkinManager.getInstance().loadSkin(this, "spring");});buttonViewSummer.setOnClickListener(view -> {// 加载皮肤SkinManager.getInstance().loadSkin(this, "summer");});buttonViewAutumn.setOnClickListener(view -> {// 加载皮肤SkinManager.getInstance().loadSkin(this, "autumn");});buttonViewWinter.setOnClickListener(view -> {// 加载皮肤SkinManager.getInstance().loadSkin(this, "winter");});buttonViewDefault.setOnClickListener(view -> {// 重置皮肤SkinManager.getInstance().restoreDefaultSkin();});}@Overrideprotected void onDestroy() {super.onDestroy();// 注销SkinManager.getInstance().unRegister(this);}@Overridepublic void onSkinChanged() {SkinManager.getInstance().updateView(context, titleView, AttrsName.textColor, R.color.text_color);SkinManager.getInstance().updateView(context, imageViewHome, AttrsName.drawableImage, R.mipmap.icon_home);SkinManager.getInstance().updateView(context, imageViewPoster, AttrsName.drawableImage, R.drawable.icon_bg_poster);SkinManager.getInstance().updateView(context, titleBar, AttrsName.backgroundColor, R.color.bg_color);SkinManager.getInstance().updateView(context, buttonViewTest, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewSpring, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewSummer, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewAutumn, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewWinter, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewDefault, AttrsName.drawableSelector, R.drawable.selector_button_bg);SkinManager.getInstance().updateView(context, buttonViewTest, AttrsName.textColor, R.color.btn_color);SkinManager.getInstance().updateView(context, buttonViewSpring, AttrsName.textColor, R.color.btn_color);SkinManager.getInstance().updateView(context, buttonViewSummer, AttrsName.textColor, R.color.btn_color);SkinManager.getInstance().updateView(context, buttonViewAutumn, AttrsName.textColor, R.color.btn_color);SkinManager.getInstance().updateView(context, buttonViewWinter, AttrsName.textColor, R.color.btn_color);SkinManager.getInstance().updateView(context, buttonViewDefault, AttrsName.textColor, R.color.btn_color);}
}
第05节 layout资源
入口 activity_main
<?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"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".ui.MainActivity"><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/rl_title_bar"android:layout_width="match_parent"android:layout_height="120dp"android:background="@color/bg_color"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/image_view_home"android:layout_width="80dp"android:layout_height="80dp"android:src="@mipmap/icon_home"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/text_view_title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Main Activity"android:textColor="@color/text_color"android:textSize="32sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/image_view_poster"android:layout_width="match_parent"android:layout_height="180dp"android:layout_marginTop="2dp"android:scaleType="centerCrop"android:src="@drawable/icon_bg_poster"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/rl_title_bar" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_test"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="50dp"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="30dp"android:text="测试按钮选择器"android:textSize="40sp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/image_view_poster" /><androidx.appcompat.widget.LinearLayoutCompatandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/image_view_poster"><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_spring"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="5dp"android:layout_weight="1"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="10dp"android:text="春天"android:textSize="30sp" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_summer"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="5dp"android:layout_weight="1"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="10dp"android:text="夏天"android:textSize="30sp" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_autumn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="5dp"android:layout_weight="1"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="10dp"android:text="秋天"android:textSize="30sp" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_winter"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="5dp"android:layout_weight="1"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="10dp"android:text="冬天"android:textSize="30sp" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/button_view_default"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="5dp"android:layout_weight="1"android:background="@drawable/selector_button_bg"android:clickable="true"android:focusable="true"android:gravity="center"android:padding="10dp"android:text="默认"android:textSize="30sp" /></androidx.appcompat.widget.LinearLayoutCompat></androidx.constraintlayout.widget.ConstraintLayout>
首页 activity_home
<?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"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".ui.MainActivity"><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/rl_title_bar"android:layout_width="match_parent"android:layout_height="120dp"android:background="@color/bg_color"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/image_view_home"android:layout_width="80dp"android:layout_height="80dp"android:src="@mipmap/icon_home"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/text_view_title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Home Activity"android:textColor="@color/text_color"android:textSize="32sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/image_view_poster"android:layout_width="match_parent"android:layout_height="180dp"android:layout_marginTop="2dp"android:scaleType="centerCrop"android:src="@drawable/icon_bg_poster"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/rl_title_bar" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guide_line_middle"android:layout_width="wrap_content"android:layout_height="match_parent"android:orientation="vertical"app:layout_constraintGuide_percent="0.50" /><com.example.hello.ui.CustomLayoutandroid:layout_width="match_parent"android:layout_height="500dp"android:layout_marginTop="2dp"android:background="@color/custom_layout_color"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/image_view_poster"><com.example.hello.ui.CustomViewandroid:layout_width="100dp"android:layout_height="100dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></com.example.hello.ui.CustomLayout></androidx.constraintlayout.widget.ConstraintLayout>
第06节 drawable资源
背景选择正常的情况下 bg_rounded_normal
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="@color/bg_rounded_normal_solid" /> <!-- 白色背景 --><corners android:radius="8dp" /> <!-- 圆角半径 --><strokeandroid:width="1dp"android:color="@color/bg_rounded_normal_stroke" />
</shape>
背景选择按下的情况下 bg_rounded_press
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="@color/bg_rounded_press_solid" /> <!-- 灰色背景 --><corners android:radius="8dp" /> <!-- 圆角半径 --><strokeandroid:width="1dp"android:color="@color/bg_rounded_press_stroke" /></shape>
背景选择器 selector_button_bg
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><!-- 按下状态 --><item android:drawable="@drawable/bg_rounded_press" android:state_pressed="true" /><!-- 默认状态 --><item android:drawable="@drawable/bg_rounded_normal" />
</selector>
第07节 colors 资源
<?xml version="1.0" encoding="utf-8"?>
<resources><color name="black">#FF000000</color><color name="white">#FFFFFFFF</color><color name="grey">#66666666</color><color name="bg_color">#FF000000</color><color name="btn_color">#FF000000</color><color name="text_color">#FFFFFFFF</color><color name="view_color">#FFFF9900</color><color name="custom_layout_color">#FF000000</color><color name="bg_rounded_press_solid">#CCCCCC</color><color name="bg_rounded_press_stroke">#66666666</color><color name="bg_rounded_normal_solid">#FFFFFF</color><color name="bg_rounded_normal_stroke">#66666666</color></resources>