一直想写一个换肤的程序,用来探究学习一下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。**
下面我们会说一下具体实现