Android换肤实现探索(一)

一直想写一个换肤的程序,用来探究学习一下android绘制过程,这次刚好有机会就进行一个学习。如果有错误的地方,请大家随时指正。

1.换肤核心问题思索
换肤最重要的是什么呢,换肤程序结构分为应用包,皮肤包,以及换肤程序。这样的设计让模块分离,降低程序耦合度,让这个模块有良好的扩展性。
一个标准的换肤流程应该是:加载皮肤(现在本地找,如果没有就去网上下载)–> 换肤模块进行皮肤的替换
我们现在先想想换肤模块这部分可能的关键性技术问题:
1.什么是皮肤,皮肤,顾名思义就是外在的表现,在android中就是各个控件的属性,比如颜色大小,背景等等,换肤就是资源的替换,所以我们就需要有资源包(之后会讲述怎么打出一个自己的皮肤包),看过画皮的人肯定就能理解,这个皮肤外表是不一样的,但是结构还是相同的,鼻子眼睛耳朵,你要换哪里就要有响应的资源。TextView的color,background等就组成了一个TextView的皮肤属性,决定了TextView的外表,换肤就是换属性换资源。
2.怎么获取不同包的资源呢。这个问题我们一会在说,我们先想想在一个应用里面怎么得到资源。在android里面我们获取asset,layout,value等资源,都绕不开一个入口对象Resource,获取这个对象主要有两种方式
(1)通过context对象;
(2)通过PackageManager来获取。
Activity,Service本身是context,可以直接用context.getResources方法来获取到。实际是调用ContextImpl类的getResources方法(这里不赘述原理,可以参考源码),实质上getResources返回的Resources对象是怎么创建的呢,就是这个方法mPackageInfo.getResources(mainThread); mPackageInfo是LoadApk对象,实质上就是调用的LoadApk的getResources方法:

public Resources getResources(ActivityThread mainThread) {  
    if (mResources == null) {  
      mResources = mainThread.getTopLevelResources(mResDir, this);  
    }  
    return mResources;  
} 

我们可以看到实质上就是调用mainThread的getTopLevelResources方法.代码如下:

Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {  
    ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);  
    Resources r;  
    synchronized (mPackages) {  
        // Resources is app scale dependent.  
        if (false) {  
            Slog.w(TAG, "getTopLevelResources: " + resDir + " / "  
                    + compInfo.applicationScale);  
        }  
        WeakReference<Resources> wr = mActiveResources.get(key);  
        r = wr != null ? wr.get() : null;  

        if (r != null && r.getAssets().isUpToDate()) {  
            if (false) {  
                Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
                        + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
            }  
            return r;  
        }  
    }  

    AssetManager assets = new AssetManager();  
    if (assets.addAssetPath(resDir) == 0) {  
        return null;  
    }  

    DisplayMetrics metrics = getDisplayMetricsLocked(false);  
    r = new Resources(assets, metrics, getConfiguration(), compInfo);  
    if (false) {  
        Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
                + r.getConfiguration() + " appScale="  
                + r.getCompatibilityInfo().applicationScale);  
    }  

    synchronized (mPackages) {  
        WeakReference<Resources> wr = mActiveResources.get(key);  
        Resources existing = wr != null ? wr.get() : null;  
        if (existing != null && existing.getAssets().isUpToDate()) {  
            // Someone else already created the resources while we were  
            // unlocked; go ahead and use theirs.  
            r.getAssets().close();  
            return existing;  
        }  
        // XXX need to remove entries when weak references go away  
        mActiveResources.put(key, new WeakReference<Resources>(r));  
        return r;  
    }  
}  
/**以上代码中,mActiveResources对象内部保存了该应用程序所使用到的所有Resources对象,其类型是Hash<ResourcesKey,WeakReference<Resources>>,可以看出这些Resources对象都是以一个弱引用的方式保存,以便在内存紧张时可以释放Resources所占内存。
ResourcesKey的构造需要resDir和compInfo.applicationScale。resdDir变量的含义是资源文件所在路径,实际指的是APK程序所在路径,比如可以是:/data/app/com.haii.android.xxx-1.apk,该apk会对应/data/dalvik-cache目录下的:data@app@com.haii.android.xxx-1.apk@classes.dex文件。
所以,如果一个应用程序没有访问该程序以外的资源,那么mActiveResources变量中就仅有一个Resources对象。这也从侧面说明,mActiveResources内部可能包含多个Resources对象,条件是必须有不同的ResourceKey,也就是必须有不同的resDir,这就意味着一个应用程序可以访问另外的APK文件,并从中读取读取其资源。(PS:其实目前的“换肤”就是采用加载不同的资源apk实现主题切换的)

*/

如果mActivityResources对象中没有包含所要的Resources对象,那么,就重新建立一个Resources对象

r = new Resources(assets, metrics, getConfiguration(), compInfo);

可以看出构造一个Resources需要一个AssetManager对象,一个DisplayMetrics对象,一个Configuration对象,一个CompatibilityInfo对象,后三者传入的对象都与设备或者Android平台相关的参数,因为资源的使用与这些信息总是相关。还有一个AssetManager对象,其实它并不是访问项目中res/assets下的资源,而是访问res下所有的资源。以上代码中的addAssetPath(resDir)非常关键,它为所创建的AssetManager对象添加一个资源路径。
看到这里,大家应该都明白怎么去获取资源了吧,没错,就是把资源的路径传递进去。
3.资源得到了,接下来就要考虑最重要的一部分了,获取到的资源该怎么用?我们先来认识一下LayoutInflater这个类。LayoutInflater的方法inflate经常使用,作用就是为了实例一个布局,但是今天我们要用到的是另外的方法,setFactory()和setFactory2.这两个方法基本一致的,不过setFactory2是在sdk>=11之后引入的。还好我们有v4包的LayoutInflaterCompat兼容各个版本。方法如下

 LayoutInflaterCompat
- setFactory(LayoutInflater inflater, 
             LayoutInflaterFactory factory)

我们新建一个Activity,并在oncreate中编写下面的代码:

 public class MainActivity extends AppCompatActivity
{
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
        {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
            {
                Log.e(TAG, "name = " + name);
                int n = attrs.getAttributeCount();
                for (int i = 0; i < n; i++)
                {
                    Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
                }

                return null;
            }
        });
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

然后,我们运行项目,你会发现打印的log为(部分log):

MainActivity﹕ name = TextView
MainActivity﹕ layout_width , -2
MainActivity﹕ layout_height , -2
MainActivity﹕ text , @2131099670

从字面上理解onCreateView是创建View,那么我们修改部分代码,添加如下代码:

if (name.equals("TextView"))
{
    Button button = new Button(context, attrs);
    return button;
}

我们会发现神奇的事情:TextView返回变成了Button
由这个可以得到,setFactory的作用就是在加载布局的时候可以改变布局以及控件的外貌,这个就是我们要的换肤主要方法了。
PS:
现在开发App的时候,我们一般Activity都继承于AppCompatActivity,而在AppCompatActivity中,实际上也调用了setFactory方法。

如果你自己还调用了setFactory就可能带来一些问题,因为setFactory并不能重复调用。并且setFactory必须在setContentView之前。

**换肤需要解决的核心问题有两个:
外部资源的加载
定位到需要换肤的View
第一个资源加载的问题可以通过构造AssetManager,反射调用其addAssetPath就可以完成。
第二个问题,就可以利用在onCreateView中,根据view的属性来定位,例如你可以让需要换肤的view添加一个自定义的属性skin_enabled=true(最开始有打印属性),并且利用一些手段拿到构造到的view,就能在View构造阶段定位的需要换肤的View。**

下面我们会说一下具体实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值