Android 源码系列之从源码的角度深入理解LayoutInflater.Factory之主题切换(中)

        转载请注明出处:http://blog.csdn.net/llew2011/article/details/51287391

        在上篇文章Android 源码系列之<四>从源码的角度深入理解LayoutInflater.Factory之主题切换(上)中我们主要讲解了LayoutInflater渲染xml布局文件的流程,文中讲到如果在渲染之前为LayoutInflater设置了Factory,那么在渲染每一个View视图时都会调用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口实现主题切换功能。如果你不清楚LayoutInflater的渲染流程,请点击这里。今天我们就从实战出发来实现自己的主题切换功能。

        既然主题切换是依赖Factory的,那么就需要定义自己的Factory了,自定义Factory其实就是实现系统的Factory接口,代码如下:

public class SkinFactory implements Factory {

	@Override
	public View onCreateView(String name, Context context, AttributeSet attrs) {
		
		Log.e("SkinFactory", "==============start==============");
		
		int attrCounts = attrs.getAttributeCount();
		for(int i = 0; i < attrCounts; i++) {
			String attrName = attrs.getAttributeName(i);
			String attrValue = attrs.getAttributeValue(i);
			Log.e("SkinFactory", "attrName = " + attrName + "       attrValue = " + attrValue);
		}
		
		Log.e("SkinFactory", "==============end==============");
		
		return null;
	}
}
        自定义SkinFactory什么都没有做,仅仅在onCreateView()方法中循环打印了attrs包含的属性名和对应的属性值,然后返回了null。创建完SkinFactory之后就是如何使用它了, 上篇文章中我们讲过在Activity中可以通过getLayoutInflater()方法获取LayoutInflater实例对象,获取到该对象之后就可以给该其赋值Factory了,代码如下:
public class MainActivity extends Activity {
	
	private LayoutInflater mInflater;
	private SkinFactory mFactory;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mFactory = new SkinFactory();
		mInflater = getLayoutInflater();
		mInflater.setFactory(mFactory);
		
		setContentView(R.layout.activity_skin);
	}
}
        需要注意的是给Activity的LayoutInflater设置Factory时一定要在调用setContentView()方法之前,否则不起作用。设置好Factory之后,我们看看一下activity_skin.xml布局文件是如何定义的,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_app_bg" >

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Factory的小练习"
        android:textColor="@color/color_title_bar_text" />

</FrameLayout>
        布局文件居中显示了一个TextView,并且给TextView设置文本为"Factory的小练习",运行一下程序,打印结果如下:

        这里只贴出了TextView的打印数据,从打印出的数据可以发现如果属性值是以@开头就表示该属性值是一个应用(以后可以通过@符号来判断当前属性是否是引用)。因为我们可以在attrs中拿到View在布局文件中定义的所有属性,所以可以猜想:如果给View添加自定义属性,在onCreateView()方法中通过解析这个自定义属性就可以判别出要做主题切换的View了。这个猜想正不正确,我们来试验一下。

        在values文件夹下创建attrs.xml属性文件,定义属性名为enable,属性值为boolean类型(true表示需要主题切换,false表示不需要主题切换),代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="enable" format="boolean" />
</resources>
        定义完属性后,若要使用该属性需要先申明命名空间,比如系统自带的:xmlns:android="http://sckemas.android.com/apk/res/android",申明命名空间有两种方法:xmlns:skin="http://schemas.android.com/apk/包名"或者是xmlns:skin="http://schemas.android.com/apk/res-auto"。我们采用第二种写法,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:skin="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_app_bg" >

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Factory的小练习"
        skin:enable="true"
        android:textColor="@color/color_title_bar_text" />

</FrameLayout>
        在activity_skin.xml布局文件中给TextView添加了自定义的enable属性并把值设为true,添加完属性后编译器报错提示说TextView没有该属性,只要手动清理一下就好了。然后运行代码,打印结果如下:

        看到打印结果我们心里好happy呀,采用给View添加自定义的属性这种方式是OK的,接下来我们就可以根据该属性区分出哪些View需要做主题切换了。做主题切换的前提是缓存那些需要做主题切换的View,但是View做主题切换可能需要更改背景,文字等。也就说一个View可能要更改多个属性,那这个属性就要求在不同的场景下对应不同的类型,所以可以抽象出代表属性的类BaseAttr,BaseAttr类有属性名,属性值,属性类型等成员变量,还要有一个抽象方法(该方法在不同的场景下有不同的实现,比如当前属性为background,那在BackgroundAttr实现中就应该是设置背景;若当前属性为textColor,那在TextColorAttr实现中就应该是设置文字颜色)。所以BaseAttr可以抽象如下:

public abstract class BaseAttr {

	public String attrName;
	public int attrValue;
	public String entryName;
	public String entryType;
	
	public abstract void apply(View view);
}

        定义好BaseAttr类之后就可以定义具体的实现类了,比如背景属性类BackgroundAttr,字体颜色改变类TextColorAttr等,BackgroundAttr代码如下:

public class BackgroundAttr extends BaseAttr {
	@Override
	public void apply(View view) {
		if(null != view) {
			view.setBackgroundXXX();
		}
	}
}

        抽象出属性类BaseAttr之后我们还要考虑缓存View的问题,因为一个View可能要对应多个BaseAttr,所以我们还要封装一个类SkinView,该类表示一个View对应多个BaseAttr,它还要提供更新自己的方法,所以代码如下:

public class SkinView {

	public View view;
	public List<BaseAttr> viewAttrs;
	
	public void apply() {
		if(null != view && null != viewAttrs) {
			for(BaseAttr attr : viewAttrs) {
				attr.apply(view);
			}
		}
	}
}
        抽象属性类BaseAttr和SkinView定义完了,接下来就可以在SkinFactory中做缓存逻辑了,代码如下:
public class SkinFactory implements Factory {

	private static final String DEFAULT_SCHEMA_NAME = "http://schemas.android.com/apk/res-auto";
	private static final String DEFAULT_ATTR_NAME = "enable";
	
	private List<SkinView> mSkinViews = new ArrayList<SkinView>();
	
	@Override
	public View onCreateView(String name, Context context, AttributeSet attrs) {
		View view = null;
		final boolean skinEnable = attrs.getAttributeBooleanValue(DEFAULT_SCHEMA_NAME, DEFAULT_ATTR_NAME, false);
		if(skinEnable) {
			view = createView(name, context, attrs);
			if(null != view) {
				parseAttrs(name, context, attrs, view);
			}
		}
		return view;
	}
	
	public final View createView(String name, Context context, AttributeSet attrs) {
		View view = null;
		if(-1 == name.indexOf('.')) {
			if("View".equalsIgnoreCase(name)) {
				view = createView(name, context, attrs, "android.view.");
			}
			if(null == view) {
				view = createView(name, context, attrs, "android.widget.");
			}
			if(null == view) {
				view = createView(name, context, attrs, "android.webkit.");
			}
		} else {
			view = createView(name, context, attrs, null);
		}
		return view;
	}
	
	View createView(String name, Context context, AttributeSet attrs, String prefix) {
		View view = null;
		try {
			view = LayoutInflater.from(context).createView(name, prefix, attrs);
		} catch (Exception e) {
		}
		return view;
	}
	
	private void parseAttrs(String name, Context context, AttributeSet attrs, View view) {
		int attrCount = attrs.getAttributeCount();
		final Resources temp = context.getResources();
		List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>();
		for(int i = 0; i < attrCount; i++) {
			String attrName = attrs.getAttributeName(i);
			String attrValue = attrs.getAttributeValue(i);
			if(isSupportedAttr(attrName)) {
				if(attrValue.startsWith("@")) {
					int id = Integer.parseInt(attrValue.substring(1));
					String entryName = temp.getResourceEntryName(id);
					String entryType = temp.getResourceTypeName(id);
					
					BaseAttr viewAttr = createAttr(attrName, attrValue, id, entryName, entryType);
					if(null != viewAttr) {
						viewAttrs.add(viewAttr);
					}
				}
			}
		}
		
		if(viewAttrs.size() > 0) {
			SkinView skinView = new SkinView();
			skinView.view = view;
			skinView.viewAttrs = viewAttrs;
			mSkinViews.add(skinView);
		}
	}

	// attrName:textColor   attrValue:2130968576   entryName:common_bg_color   entryType:color
	private BaseAttr createAttr(String attrName, String attrValue, int id, String entryName, String entryType) {
		BaseAttr viewAttr = null;
		if("background".equalsIgnoreCase(attrName)) {
			viewAttr = new BackgroundAttr();
		} else if("textColor".equalsIgnoreCase(attrName)) {
			viewAttr = new TextColorAttr();
		}
		if(null != viewAttr) {
			viewAttr.attrName = attrName;
			viewAttr.attrValue = id;
			viewAttr.entryName = entryName;
			viewAttr.entryType = entryType;
		}
		return viewAttr;
	}

	private boolean isSupportedAttr(String attrName) {
		if("background".equalsIgnoreCase(attrName)) {
			return true;
		} else if("textColor".equalsIgnoreCase(attrName)) {
			return true;
		}
		return false;
	}

	public void applaySkin() {
		if(null != mSkinViews) {
			for(SkinView skinView : mSkinViews) {
				if(null != skinView.view) {
					skinView.apply();
				}
			}
		}
	}
}

        SkinFactory中定义了装载SkinView类型的mSkinViews缓存集合,当解析到符合条件的View时就会缓存到该集合中。在onCreateView()方法中调用AttributeSet的getAttributeBooleanValue()方法检测是否含有enable属性,如果有enable属性并且属性值为true时我们自己调用系统API来创建View,如果创建成功就解析该View,分别获取其attrName,attrValue,entryName,entryType值取完之后创建对应的BaseAttr,然后加入缓存集合mSkinViews中,否则返回null。

        创建完SkinFactory之后还需要创建一个主题资源管理器SkinManager,主题切换就是通过该管理器来决定的。所以其主要有以下功能:实现读取额外主题资源功能,恢复默认主题功能,更新主题功能等。

       先看一下如何读取额外主题资源问题。做主题切换需要准备多套主题,这些主题其实就是一些图片,颜色等。有了素材之后我们还要考虑如何提供给APP素材的形式,是直接提供一个Zip包文件还是说做成一个apk文件的形式提供给APP?如果提供Zip包接下来的处理是解压该Zip包得到里边的素材然后解析读取,理论上来说这种方式是可行的,但是操作起来有点复杂。所以我们采用apk的形式,若希望访问素材apk中的资源如同在APP中访问资源一样,我们得获取到素材apk的Resources实例,下面我直接提供一种通用的可以获取apk的Resources实例代码,代码如下:

public final Resources getResources(Context context, String apkPath) {
	try {
		AssetManager assetManager = AssetManager.class.newInstance();
		Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
		addAssetPath.setAccessible(true);
		addAssetPath.invoke(assetManager, apkPath);
		
		Resources r = context.getResources();
		Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
		return skinResources;
	} catch (Exception e) {
	}
	return null;
}

        这段代码可以有效的获取到apk中的Resources实例,然后通过该Resources实例访问资源就如同我们在APP中直接访问自己资源一般,如果你对Android的资源访问机制很熟悉的话,很清楚这段代码为什么要这么写。不清楚也没关系,先暂时这么用,我会在后续文章中从源码的角度分析一下Android的资源访问机制并解释这么写的原因。

        好了,现在我们已经解决了访问素材资源的问题,那接下来就是编写我们的SkinManager类了,SkinManager类的功能是来加载素材资源文件的,在加载文件时可能有失败的情况,所以需要给APP回调来通知加载资源的结果,我们定义接口ILoadListener,代码如下:

public interface ILoadListener {
	void onStart();
	void onSuccess();
	void onFailure();
}
        ILoadListener接口有三个方法,分别表示资源开始加载的回调,加载成功后的回调和加载失败后的回调。我们接着完成我们SkinManager代码,如下所示:
public final class SkinManager {

	private static final Object mClock = new Object();
	private static SkinManager mInstance;
	
	private Context mContext;
	private Resources mResources;
	private String mSkinPkgName;
	
	private SkinManager() {
	}
	
	public static SkinManager getInstance() {
		if(null == mInstance) {
			synchronized (mClock) {
				if(null == mInstance) {
					mInstance = new SkinManager();
				}
			}
		}
		return mInstance;
	}
	
	public void init(Context context) {
		enableContext(context);
		mContext = context.getApplicationContext();
	}
	
	public void loadSkin(String skinPath) {
		loadSkin(skinPath, null);
	}
	
	public void loadSkin(final String skinPath, final ILoadListener listener) {
		enableContext(mContext);
		if(TextUtils.isEmpty(skinPath)) {
			return;
		}
		new AsyncTask<String, Void, Resources>() {
			@Override
			protected void onPreExecute() {
				if(null != listener) {
					listener.onStart();
				}
			}
			
			@Override
			protected Resources doInBackground(String... params) {
				if(null != params && params.length == 1) {
					String skinPath = params[0];
					File file = new File(skinPath);
					if(null != file && file.exists()) {
						PackageManager packageManager = mContext.getPackageManager();
						PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, 1);
						if(null != packageInfo) {
							mSkinPkgName = packageInfo.packageName;
						}
						return getResources(mContext, skinPath);
					}
				}
				return null;
			}
			@Override
			protected void onPostExecute(Resources result) {
				if(null != result) {
					mResources = result;
					if(null != listener) {
						listener.onSuccess();
					}
				} else {
					if(null != listener) {
						listener.onFailure();
					}
				}
			}
		}.execute(skinPath);
	}
	
	public Resources getResources(Context context, String apkPath) {
		try {
			AssetManager assetManager = AssetManager.class.newInstance();
			Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
			addAssetPath.setAccessible(true);
			addAssetPath.invoke(assetManager, apkPath);
			
			Resources r = context.getResources();
			Resources skinResources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
			return skinResources;
		} catch (Exception e) {
		}
		return null;
	}
	
	public void restoreDefaultSkin() {
		if(null != mResources) {
			mResources = null;
			mSkinPkgName = null;
		}
	}
	
	public int getColor(int id) {
		enableContext(mContext);
		Resources originResources = mContext.getResources();
		int originColor = originResources.getColor(id);
		if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
			return originColor;
		}
		String entryName = mResources.getResourceEntryName(id);
		int resourceId = mResources.getIdentifier(entryName, "color", mSkinPkgName);
		try {
			return mResources.getColor(resourceId);
		} catch (Exception e) {
		}
		return originColor;
	}
	
	public Drawable getDrawable(int id) {
		enableContext(mContext);
		Resources originResources = mContext.getResources();
		Drawable originDrawable = originResources.getDrawable(id);
		if(null == mResources || TextUtils.isEmpty(mSkinPkgName)) {
			return originDrawable;
		}
		String entryName = mResources.getResourceEntryName(id);
		int resourceId = mResources.getIdentifier(entryName, "drawable", mSkinPkgName);
		try {
			return mResources.getDrawable(resourceId);
		} catch (Exception e) {
		}
		return originDrawable;
	}
	
	private void enableContext(Context context) {
		if(null == context) {
			throw new NullPointerException();
		}
	}
}

        SkinManager我们采用了单例模式保证应用中只有一个实例,在使用的时候需要先进行初始化操作否则会抛异常。SkinManager不仅定义了属性mContext和mResources(mContext表示APP的运行上下文环境,mResources代表资源apk的Resources实例对象,如果为空表示使用默认APP主题资源),而且它还对外提供了一系列方法,比如读取资源的getColor()和getDrawable()方法,加载资源apk的方法loadSkin()等。

        现在主题切换的核心逻辑都有了,我们看一下程序包结构图是怎样的,切图如下:

        主题切换的核心逻差不多已经然完成了,接下来就是要练习使用一下看看效果能不能成了,首先修改activity_skin.xml布局文件,修改如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:skin="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/common_bg_color"
    android:orientation="vertical"
    skin:enable="true" >

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="65dp"
        android:background="@color/common_title_bg_color"
        skin:enable="true" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="主题切换标题"
            android:textColor="@color/common_title_text_color"
            android:textSize="18sp"
            skin:enable="true" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right|center_vertical"
            android:onClick="updateSkin"
            android:text="切换主题" />
    </FrameLayout>

</LinearLayout>
        在activity_skin.xml布局文中给需要做主题切换的View节点添加了enable属性并且设置其值为true。接下来就是要做一个主题apk包了,做主题包的简单方式就是新建一个工程,里边不添加Activity等,然后在资源文件夹下创建对应的资源等,需要注意的是资源文件名一定要和APP中的资源名一致。然后编译打包成一个apk文件,这里就不再演示了。打包完apk后我们导入到模拟器根目录下,然后修改MainActivity,添加updateSkin()方法,代码如下:
public void updateSkin(View view) {
	String skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "skin.apk";
	SkinManager.getInstance().loadSkin(skinPath, new ILoadListener() {
		@Override
		public void onSuccess() {
			mFactory.applaySkin();
		}
		
		@Override
		public void onStart() {
		}
		
		@Override
		public void onFailure() {
		}
	});
}
        添加完updateSkin()方法之后,就可以实现切换主题了,为了方便我直接把skin.apk文件直接导入了SD卡根目录下, 需要注意有的手机没有外置存储卡需要做个判断,别忘了在配置文件添加文件的读写权限,然后运行程序,效果如下:

        好了,现在在当前页面进行主题切换看起来是OK的,但是还存在不足,当页面进行跳转比如从A→B→C→D然后在D中进行主题切换,这时候ABC是没有效果的,另外代码的通用性也不强,所以在下篇文章Android 源码系列之<六>从源码的角度深入理解LayoutInflater.Factory之主题切换(下)处理这些问题,敬请期待...





发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/51287391