Android通过Hook技术实现一键换肤

本文介绍了Android实现一键换肤的技术原理,通过Hook技术动态修改应用内资源,包括TextView的文字颜色、字体大小等。文章详细分析了Android视图创建的源码,讲解了如何在AppCompatActivity中利用Factory2接口实现换肤,并介绍了关键类SkinFactory和SkinEngine的实现,以及动态加载插件apk资源的步骤。注意事项包括资源文件命名的一致性和Gradle SDK版本匹配等。
摘要由CSDN通过智能技术生成

目录

1.什么是一键换肤

2.界面上那些东西可以换肤 

3.利用Hook实现一键换肤

4.Android创建视图源码分析

4.1.自定义Activity设置要显示的布局文件xml

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换 

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图

4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)

4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())

5.Resources/AssetManager

6.实现关键类SkinFactory,SkinEngine

6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;

6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)

6.3实例使用

6.4主app和插件app设置需要换肤资源用相同的名称

7.显示效果

8.注意事项


实现换肤的方案:

a.静态修改theme主题方式

设置多套皮肤的theme;

styles.xml
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="defaultcolor">@color/defalultcolor</item>                                      
    </style>

    <style name="NewTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="defaultcolor">@color/newcolor</item>
    </style>
</resources>

声明属性需要动态替换的样式属性

attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="defaultcolor" format="reference|color" />
</resources>

为控件设置属性样式?attr/defaultcolor

<TextView
        android:layout_marginTop="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是第一个fragment"
        android:textColor="?attr/defaultcolor"
        android:textSize="36dp" />

动态设置主题

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if( 1 ==  1){
            setTheme(R.style.AppTheme);
        }else {
            setTheme(R.style.NewTheme);
        }
        setContentView(R.layout.activity_main);
}

缺点是设置主题在setContentView之前,设置以后需要重启Activity;

b.动态修改样式(应用内/插件化-皮肤apk),不需要重启Activity

应用内

采用通过相同名称+不同皮肤后缀来区分不同皮肤,如实现黑白皮肤,文本颜色item_text_color有一套默认皮肤,一套黑色皮肤定义资源item_text_color,item_text_color_black;

String resName = mOutResource.getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

resName+"_"+不同皮肤后缀

缺点:应用内多套皮肤时可能导致安装包过大;

插件化-皮肤apk

皮肤apk和主apk有相同皮肤资源名称,获取皮肤apk下的资源名称,需要和主apk下资源名称一致,设置在皮肤apk下皮肤资源 ;

String resName = mOutResource.getResourceEntryName(resId);
        int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);

mOutResource皮肤apk资源Resources;

动态修改视图皮肤需要解决两个问题:

1).缓存获取全部需要换肤的视图,换肤(资源)时执行换肤操作;

2).获取Resources资源(插件apk资源Resources),动态设置皮肤;

1.什么是一键换肤

所谓”一键换肤“就是通过一个接口调用,实现app范围内所有资源文件替换,包括文本,颜色,图片,动画等;

2.界面上那些东西可以换肤 

例如TextView文字颜色,字体大小,ImageView的background等等;

res目录所有的资源几乎都可以替换,具体如下:

动画
背景图片
字体
字体颜色
字体大小
音频
视频

3.利用Hook实现一键换肤

什么是hook
如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···

"一键换肤"中的hook思路

  1. "劫持"系统创建View的过程,我们自己来创建View
    系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用.
  2. 收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中
    劫持了 系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来
  3. 加载外部资源包,调用接口进行换肤
    外部资源包,是.apk后缀的一个文件,是通过gradle打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同.

4.Android创建视图源码分析

自己定义Activity继承自兼容AppCompatActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

创建Activity经常在onCreate()方法中调用setContentView(R.layout.xxx);设置要显示视图,那么如何将我们创建的xml布局文件转换为要显示View视图呢?

源码执行流程:

4.1.自定义Activity设置要显示的布局文件xml

setContentView(R.layout.activity_main);

4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换 

由于Android版本比较分散,需要兼容各个版本Android系统,AppCompatActivity定义实现不同版本AppCompatDelegate实现类;

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV7(context, window, callback);
        }
    }

4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法

通过如下实现将layout的xml布局文件转换为View视图添加到父视图上contentParent;

LayoutInflater.from(mContext).inflate(resId, contentParent);

4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件

4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            View result = root;
                //找到跟节点
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                //获取跟节点名字
                final String name = parser.getName();
                //创建xml的layout布局文件跟视图
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                //递归调用创建子视图
                rInflateChildren(parser, temp, attrs, true);
                    // 添加到xml布局文件视图到父视图
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    //将xml布局的跟视图添加做为父视图
                     if (root == null || !attachToRoot) {
                        result = temp;
                    }
            ...部分源码

            return result;
        }
    }

//创建xml布局文件的跟视图

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

//递归创建xml布局文件的子视图,最后调用createView

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值