在上篇文章 从源码角度深入理解LayoutInflater.Factory 主要介绍了LayoutInflater.Factory是什么,并简单介绍了一下用Factory可以做些什么,本篇文章就具体介绍一下Factory在换肤上的具体应用。
在上一篇博文中我们在Factory中打印了一下输出后的AttributeSet信息如下:
gravity:0x11 background:#ffff0000 layout_width:-1 layout_height:48.0dip text:@2131361814
这里只是打印出了View的属性值和属性名称,当然了资源名称和资源类型也可以打印出来,下面是获取属性和资源的四个相关方法:
//位于AttributeSet类中 public String getAttributeName(int index);//属性名称 public String getAttributeValue(int index);//属性值 //位于Resources类 public String getResourceEntryName(@AnyRes int resid)//资源名称 public String getResourceTypeName(@AnyRes int resid)//资源类型
在对每一个需要换肤的View进行操作的时候,资源类型一般就是两种类型,要么是color要么是一个drawable,但是属性就比较多了,有可能是一个textColor,有可能是background,如单复选按钮还可能是一个button,因此我们可以将属性相关的类设计为一个工厂类SkinFactory,根据不同的属性设置不同的属性类,如TextColorAttr或者BackgroundAttr。另外一个需要注意的就是在进行换肤的时候资源应该都是以引用的形式设置,此时输出的结果都是 一个以@开始的属性值 ,@后面对应的值就是一个资源的ID,然后我们通过下面两个方法就可以获取资源真正的ID了。
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = resources.getIdentifier(resName, “color”, skinPackageName);
换肤的核心就在这里了,只要我们将资源Resources获取到就可以获取到资源中相应的资源了,无论是应用内换肤还是插件式换肤都是一样的,只是获取以及处理资源的方式不同罢了,本篇重点讲解的是插件式换肤,类似QQ方式,可以远程下载皮肤库到本地,然后进行换肤。
首先确定属性的基类,基类中至少需要四个属性:View属性名称和值以及资源名称和类型,另外由于资源类型确定为只有两种类型:color和drawable,可以将资源类型设置为两个常量。当我们获取到插件包中的资源库以后,根据不同的属性实现类必须有不同的实现方法,如果是字体颜色apply()方法中可以是view.setTextColor()方法等等,因此在基类中可以设置一个抽象方法,所有实现该基类的子类必须实现该方法,SkinAttr基类代码如下:
/** * android:textColor="@color/text_default" * View属性名称 attrName:textColor * 资源ID attrValueId R类中对text_default对应的一个整型值 * 资源名称 attrEntryName:text_default * 资源类型 attrEntryType:color */ public abstract class SkinAttr { //资源类型color protected static final String TYPE_NAME_COLOR = "color"; //资源类型drawable protected static final String TYPE_NAME_DRAWABLE = "drawable"; //属性名称 public String attrName; //属性引用资源ID public int attrValueId; //资源名称 public String attrEntryName; //资源类型 public String attrEntryType; //不同子类必须要实现的方法 public abstract void apply(Viewview); @Override public String toString() { return "SkinAttr [attrName=" + attrName + ", attrValueId=" + attrValueId + ", attrEntryName=" + attrEntryName + ", attrEntryType=" + attrEntryType + "]"; } }
根据不同的属性可以创建不同的实现类,如textColor对应TextColorAttr,background对应BackgroundAttr等等。
public class TextColorAttr extends SkinAttr { @Override public void apply(Viewview) { if (viewinstanceof TextView) { TextViewtv = (TextView) view; if (TYPE_NAME_COLOR.equals(attrEntryType)) { tv.setTextColor(SkinManager.getInstance().getColorStateList(attrValueId)); } } } } public class BackgroundAttr extends SkinAttr { @Override public void apply(Viewview) { if (TYPE_NAME_COLOR.equals(attrEntryType)) { view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueId)); } else if (TYPE_NAME_DRAWABLE.equals(attrEntryType)) { Drawablebg = SkinManager.getInstance().getDrawable(attrValueId); view.setBackgroundDrawable(bg); } } }
当所有的属性实现类完成后,接着设计一个工厂类AttrFactory,根据获取的属性名称不同生成不同的属性实现类,将来一旦有其它属性或者自定义属性需要实现换肤只需在该类中添加相应的实现即可。在AttFactory类中需要增加一个判断方法,因为在LayoutInflater.Factory中输出的属性是View的所有属性,但是并不是所有属性都需要实现换肤逻辑,只需要将所有需要实现换肤逻辑的属性定义为常量,在生成SkinAttr实现类之前判断一下该属性是否需要实现换肤逻辑。
public class AttrFactory { public static final String BACKGROUND = "background"; public static final String TEXT_COLOR = "textColor"; public static final String LIST_SELECTOR = "listSelector"; public static final String DIVIDER = "divider"; public static final String BUTTON = "button"; public static SkinAttrget(String attrName, int attrValueId, String attrEntryName, String attrEntryType) { SkinAttrmSkinAttr = null; if (BACKGROUND.equals(attrName)) { mSkinAttr = new BackgroundAttr(); } else if (TEXT_COLOR.equals(attrName)) { mSkinAttr = new TextColorAttr(); }else if (BUTTON.equals(attrName)) { mSkinAttr = new ButtonAttr(); } else if (LIST_SELECTOR.equals(attrName)) { mSkinAttr = new ListSelectorAttr(); } else if (DIVIDER.equals(attrName)) { mSkinAttr = new DividerAttr(); } else { return null; } mSkinAttr.attrName = attrName; mSkinAttr.attrValueId = attrValueId; mSkinAttr.attrEntryName = attrEntryName; mSkinAttr.attrEntryType = attrEntryType; return mSkinAttr; } /** * 判断是否需要换肤 * @param attrName * @return */ public static boolean isSupportedAttr(String attrName) { if (BACKGROUND.equals(attrName)) { return true; } if (BUTTON.equals(attrName)) { return true; } if (TEXT_COLOR.equals(attrName)) { return true; } if (LIST_SELECTOR.equals(attrName)) { return true; } if (DIVIDER.equals(attrName)) { return true; } return false; } }
属性的相关类基本封装好了,一个View在换肤的时候可能需要涉及到多个属性,如字体颜色、背景色等,因此我们可以将View和需要换肤的属性再封装进一个类中,在进行换肤的时候调用一个apply方法,通过资源依次赋值到View的属性中。
public class SkinView { public Viewview; //所有涉及到需要换肤的属性 public List<SkinAttr> attrs; public SkinView() { attrs = new ArrayList<>(); } //将属性依次赋值到View public void apply() { if (view != null && !ListUtils.isEmpty(attrs)) { for (SkinAttrattr : attrs) { attr.apply(view); } } } //在销毁时清除 public void clean() { if (ListUtils.isEmpty(attrs)) { return; } for (SkinAttrat : attrs) { at = null; } } }
走到这里属性封装好了,涉及到换肤的View也封装完毕,接下来就是逻辑实现了,针对每个属性的apply()方法该如何实现呢?我们知道在设置字体颜色的时候一般使用的是 resource.getColor(@ColorRes int id)
,背景图片 resource.getDrawable(@DrawableRes int id)
,只要可以获取到插件包的Resources就可以使用插件包中的资源文件了。
获取插件包中Resources,在Resources的一个构造方法中可以传入AssetManager实例,AssetManager中addAssetPath()方法可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射。下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了。然后再通过AssetManager来创建一个新的Resources对象,通过这个对象我们就可以访问插件apk中的资源了,这样一来问题就解决了。
/** * Add an additional set of assets to the asset manager. This can be * either a directory or ZIP file. Not for use by applications. Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { synchronized (this) { int res = addAssetPathNative(path); makeStringBlocks(mStringBlocks); return res; } }
由于AssetManager的构造方法也是隐藏API,所以获取AssetManager实例也需要使用反射技术,然后调用addAssetPath()方法。将远程获取的资源包存入SDcard,然后使用一个异步任务来操作比较耗时的IO操作,下面就是使用AsyncTask获取资源包中的Resources对象。
new AsyncTask<String, Void, Resources>() { @Override protected void onPreExecute() { if (listener != null) { listener.onStart(); } } @Override protected ResourcesdoInBackground(String... params) { try { if (params.length == 1) { String skinPkgPath = params[0]; Filefile = new File(skinPkgPath); if (file == null || !file.exists()) { return null; } PackageManagermgr = context.getPackageManager(); PackageInfoinfo = mgr.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES); skinPackageName = info.packageName; AssetManagerassetManager = AssetManager.class.newInstance(); MethodaddAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinPkgPath); ResourcessuperRes = context.getResources(); ResourcesskinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); SkinConfig.saveSkinPath(context, skinPkgPath); skinPath = skinPkgPath; isDefaultSkin = false; return skinResource; } return null; } catch (Exception e) { e.printStackTrace(); return null; } } protected void onPostExecute(Resourcesresult) { resources = result; if (resources != null) { if (listener != null) { listener.onSuccess(); } notifySkinUpdate(); } else { isDefaultSkin = true; if (listener != null) { listener.onFailed(); } } } }.execute(skinPackagePath);
获取到资源Resources后,就可以通过Resources得到color或者drawable了。
public int getColor(int resId) { int originColor = ContextCompat.getColor(context, resId); if (resources == null || isDefaultSkin) { return originColor; } String resName = context.getResources().getResourceEntryName(resId); int trueResId = resources.getIdentifier(resName, "color", skinPackageName); int trueColor = 0; try { trueColor = ResourcesCompat.getColor(resources, trueResId, null); } catch (NotFoundException e) { e.printStackTrace(); trueColor = originColor; } return trueColor; }
由于换肤涉及到整个应用,所以我们可以为操作换肤的类设计为一个单例模式的类SkinManager,在SkinManager中传入一个Context对象,可以直接使用ApplicationContext。
public void init(Contextctx) { context = ctx.getApplicationContext(); } public static SkinManagergetInstance() { return InstanceHolder.holder; } private static class InstanceHolder { private static SkinManagerholder = new SkinManager(); }
由于传入的是ApplicationContext,当我们在Activity中使用进行换肤的时候,因为Application是在应用的整个生命周期内的,所以到某个Activity进行垃圾回收时不能被回收,引起内存溢出,因为该Activity持有Application的引用,所以我们需要再设计两个方法。
@Override public void attach(SkinObserverobserver) { if (skinObservers == null) { skinObservers = new ArrayList<SkinObserver>(); } if (!skinObservers.contains(observer)) { skinObservers.add(observer); } } @Override public void detach(SkinObserverobserver) { if (skinObservers == null) return; if (skinObservers.contains(observer)) { skinObservers.remove(observer); } }
当资源获取后,就可以通知Activity进行换肤回调了,如果没有回调,当我们在Activity任务栈回退操作的时候,导致上一个界面仍然保持了换肤之前的状态。所以这时候我们就知道了换肤操作实际上采用的是观察者模式,当Activity进入任务栈的时候SkinManger调用attach()方法,销毁的时候调用detach()方法。
public interface SkinObservable { void attach(SkinObserverobserver); void detach(SkinObserverobserver); void notifySkinUpdate(); } public interface SkinObserver { void onThemeUpdate(); } public class SkinManager implements SkinObservable { ... @Override public void notifySkinUpdate() { if (skinObservers == null) return; for (SkinObserverobserver : skinObservers) { observer.onThemeUpdate(); } } } public class BaseActivity extends AppCompatActivity implements SkinObserver { ... @Override public void onThemeUpdate() { skinFactory.applySkin(); } }
本篇博客暂时介绍到这里,在后续博客中我们继续介绍SkinFactory的设计,如何在SkinFactory中在不影响系统本身创建View的条件下进行换肤操作,当然了换肤操作比实际想象中的要复杂一些,这里讲解的都是通过布局文件生成的View,如果通过Java代码new的View又该如何换肤呢?这里讲解的主要是插件式换肤,但是如果是应用内换肤资源又该如何操作呢?还有App中如果使用了WebView加载的网页等等,如应用市场中网易新闻、开发者头条、知乎都涉及到了WebView对网页的换肤操作,在后续文章中都会逐一揭晓。