当前位置: 首页 > news >正文

安卓主题换肤功能

主题换肤

文章目录

  • 主题换肤
    • 第一章 前言
      • 第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 接口

名称参数返回值说明
addSkipKeyMapSkipNameMap<String, String> map 映射表void采用 Map 批量添加映射表的方案
addSkipKeyMapSkipNameString skipKey 皮肤的键
String skipName 皮肤的值
void采用 键值对的方式, 添加映射表方案
clearKeyMapNamevoid清除键值对映射的皮肤表
loadSkinContext context 上下文对象
String skipKey 皮肤的键
void加载皮肤资源
loadSkinContext context 上下文对象
String skipKey 皮肤的键
boolean isNotifySkinChanged 是否通知皮肤自动更新
void加载皮肤资源
loadSkinContext context 上下文对象
String skipKey 皮肤的键
InputStream is 输入流
boolean isFinishCloseStream 是否通知皮肤自动更新
void加载皮肤资源
loadSkinContext context 上下文对象
String skinKey 皮肤的键
boolean isNotifySkinChanged 是否通知皮肤自动更新
InputStream is 输入流
boolean isFinishCloseStream 是否操作完毕以后, 需要关闭输入流
void加载皮肤资源
onRegisterISkinListener listener 监听器void注册监听器
unRegisterISkinListener listener 监听器void注销监听器
restoreDefaultSkinvoid恢复默认皮肤
updateViewContext 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>






http://www.xdnf.cn/news/198001.html

相关文章:

  • 安卓基础(强制转换)
  • 社交电商和泛娱乐平台出海南美市场支付方式与策略
  • ASP.NET MVC​ 入门指南四
  • 【quantity】3 Unit 物理量计算库(quantity.rs)
  • c语言的指针详解
  • js补环境工具使用技巧、补环境实例、重点环境检测点详解
  • Qt开发:XML文件的写入与读取
  • AI与机器人外科手术:如何用智能化技术提升手术精度与安全性?
  • 【android bluetooth 协议分析 06】【l2cap详解 10】【通过avdtp连接流程,感受l2cap通道的生命周期变化】
  • [JavaScript]对象关联风格与行为委托模式
  • WSL释放空间
  • ‌wangEditor 所有菜单项分类、说明及隐藏方法
  • Java项目场景题深度解析
  • Termux - Android终端应用与Linux环境
  • Java读Excel:解析阿里云easyExcel导入文件的行号
  • vmare pro安装报错用户在命令行上发出了EULAS_AGREED=1,表示不接受许可协议的错误解决方法
  • 高压开关柜局部放电信号分析系统
  • C/C++链表的常用操作实现
  • three.js后处理原理及源码分析
  • HTML5好看的水果蔬菜在线商城网站源码系列模板7
  • 文档在线协同工具ONLYOFFICE教程:如何使用宏突出显示具有特定提示文本的空文本字段
  • window 图形显示驱动-在 WDDM 1.2 中提供无缝状态转换(下)
  • 系统架构师2025年论文《论面向对象的软件设计——UML 在面向对象软件架构中的应用》
  • leetcode 876. 链表的中间结点
  • Python 实现的运筹优化系统数学建模详解(动态规划模型)
  • 第二阶段:基础加强阶段总体介绍
  • 网络安全怎么入门?快速了解
  • 基于大模型的公安预审办案笔录分析的挑战与应对策略-3
  • 2025汽车制造企业数字化转型路径参考
  • TypeScript之基础知识