[Android]关于换肤功能的遐想篇

看到过一些很多app都有换肤场景的功能,多数都是从服务器上下载资源然后再使用的,这就解决了资源可选择使用,减轻apk的资源大小,并能很好的提高用户体验。

在android中如何实现这个功能呢,其实可以利用动态加载实现对资源文件的调用,大概意思就是说利用Dalvikvm 中的classloader来加载我们需要的apk中的“我们需要的某个类”或者某个资源,他和java中反射机制一个道理,在java虚拟机中可以利用classloader 加载class文件,反射出其中的对象和方法。android中可以利用这个逻辑,去尝试使用ClassLoader的子类DexClassloader来调用任何位置的dex或apk.

为此,为了更好的让观者理解,可先阅读代码。先放一下demo效果图:

项目关系图:

1.接口程序,主要是为了统一调用的接口

因为是一个简单的demo,不做太深入的分析,但能快速的理解其用意。如这个接口中,主要是想实现2个功能:弹出来自插件工程中的弹出框和获取插件中的皮肤资源(这里就假设是一个图片了)

/**
 * 创建一个接口:用于更新
 * @author jan
 */
public interface SkinChangeInferface {
	/**
	 * 获取当前皮肤名字,以弹出框的形式
	 * @param context
	 */
	public void showSkinNameInDialog(Context context);
	/**
	 * 获取皮肤参数相关的类
	 * @param context
	 * @return
	 */
	public MySkinBean getMySkin(Context context);
	
}
MySkinBean.java  - 关于皮肤的实体类

public class MySkinBean implements Parcelable {
	
	private long bgImageId;
	private String skinName;

	public MySkinBean() {
	}

	public MySkinBean(Parcel parcel) {
		this.bgImageId = parcel.readInt();
		this.skinName = parcel.readString();
	}

	public long getBgImageId() {
		return bgImageId;
	}

	public void setBgImageId(long bgImageId) {
		this.bgImageId = bgImageId;
	}

	public String getSkinName() {
		return skinName;
	}

	public void setSkinName(String skinName) {
		this.skinName = skinName;
	}

	public static final Parcelable.Creator<MySkinBean> CREATOR = new Creator<MySkinBean>() {

		@Override
		public MySkinBean[] newArray(int size) {
			return new MySkinBean[size];
		}

		@Override
		public MySkinBean createFromParcel(Parcel source) {
			return new MySkinBean(source);
		}
	};

	@Override
	public int describeContents() {
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int arg1) {
		dest.writeLong(bgImageId);
		dest.writeString(skinName);
	}

}
这个接口工程,我们需要它作为一个library为主项目ChangeSkinDemo的库来使用,而皮肤插件项目也需要这个lib,但是相同的class是不能被andoroid重复加载的!所以我们需将这个接口工程打成jar的形式供插件项目使用。




将打好的skin-pligin.jar加入到skinSky插件工程中去,记住,这里的引用方式应该选择addJar或者addExternal Jars,这么做在插件打包的时候不会集成到apk中,避免重复的系统编译,而主项目可以以addLirary去引用,这么做动态加载的时候不会在不同的dex中用同一个加载器 加载同一个class而引发异常。


好,我们去看看主程序怎么做的,以下是主项目demo结构图:


主要看BaseActivity这个父类,我们在其中实现了如何加载外部dex资源到主程序的Resources中,还有通过DexClassloader来调用插件实现的接口方法。

public class BaseActivity extends Activity {
	
	public static String TAG = BaseActivity.class.getSimpleName();
	public static final String SKIN_IMPL_CLASSNAME = "org.jan.skin.impl.SkinImpl";
	protected static List<BaseActivity> activityList = new ArrayList<BaseActivity>();
	protected Context mContext;
	//资源管理类
	protected AssetManager mAssetManager;
	//我们app的资源类
	protected Resources mResources;
	protected Theme mTheme;
	//类加载器,他与父类的PathClassLoader有一个差别,就是DexClassLoader可以加载指定path的dex、jar、apk
	//而PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。
	protected DexClassLoader mDexClassLoader;
	private String relasePath;
	
	/**
	 * 加载目标apk dex中的资源。
	 * addAssetPath是一个隐藏的方法,我们可以通过他传入一个apk(zip)来调用其中的资源,
	 * 然后这里将获取Resources,与主项目的资源整合在一起。
	 * @param dexPath
	 */
	protected void loadResources(String dexPath) {
		try {
			AssetManager assetManager = AssetManager.class.newInstance();
			Method addAssetPath = assetManager.getClass().getMethod(
					"addAssetPath", String.class);
			addAssetPath.invoke(assetManager, dexPath);
			mAssetManager = assetManager;
		} catch (Exception e) {
			e.printStackTrace();
		}
		Resources superRes = super.getResources();
		superRes.getDisplayMetrics();
		superRes.getConfiguration();
		mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
				superRes.getConfiguration());
		mTheme = mResources.newTheme();
		mTheme.setTo(super.getTheme());
	}	
	
	protected DexClassLoader getDexClassLoader(String dexPath,String optimizedDirectory ) {
		try {			
			mDexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, null, getClassLoader());
		} catch (Exception e) {
			Log.e(TAG, "getDexClassLoader调用出错:", e);
			return null;
		}
		 return mDexClassLoader;
	}
	
	
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		activityList.add(this);
		mContext = this;
	}
	@Override
	protected void onDestroy() {
		activityList.remove(this);
		super.onDestroy();
	}
	/** 以下三个方法请务必实现 */

	@Override
	public Resources getResources() {
		if(mResources!=null){
			return mResources;
		}
		return super.getResources();
	}
	
	@Override
	public Theme getTheme() {
		if(mTheme!=null){
			return mTheme;
		}
		return super.getTheme();
	}

	@Override
	public AssetManager getAssets() {
		if(mAssetManager!=null){
			return mAssetManager;
		}
		return super.getAssets();
	}
	
	/**
	 * 在这里,我们通过dexclassloader来调用皮肤插件apk中的方法,反射其中的背景id
	 * @param apkPath
	 */
	@SuppressLint("NewApi")
	protected void changeSkin(String apkPath){
		File apkFile = new File(apkPath);
		if(!apkFile.exists()){
			Toast.makeText(mContext, "皮肤未下载", Toast.LENGTH_SHORT).show();
			return;
		}
		mDexClassLoader = getDexClassLoader(apkPath, relasePath);
		Class skinChangeImpl;
		try {
			skinChangeImpl = mDexClassLoader.loadClass(SKIN_IMPL_CLASSNAME);
			SkinChangeInferface skinChange = (SkinChangeInferface) skinChangeImpl.newInstance();
			MySkinBean skin = skinChange.getMySkin(mContext);
			for(BaseActivity activity : activityList){
				activity.changeBackground(apkPath,(int)skin.getBgImageId());
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}catch (NullPointerException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 这里使用了比较粗暴简单的方式更好布局的背景。
	 * @param resPath
	 * @param resId
	 */
	@SuppressLint("NewApi")
	private void changeBackground(String resPath ,int resId) {
		loadResources(resPath);
		Log.d(TAG, "changeBack --backId==" + resId);
		//获取当前view下的根布局来修改背景
		View rootView = ((ViewGroup) (getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0);
		rootView.setBackground(getResources().getDrawable(resId));
	}

	public String getRelasePath() {
		return relasePath;
	}

	public void setRelasePath(String relasePath) {
		this.relasePath = relasePath;
	}
}
皮肤选择界面的代码,写的比较简单,就是点击换肤而已。

/**
 * 选择皮肤的列表界面
 * @author jan
 *
 */
public class SkinListActivity extends BaseActivity {
	private DexClassLoader mClassLoader;
	private ListView mListView;
	private String relasePath;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.skin_list);
		mListView = (ListView) findViewById(R.id.skin_listview);
		//资源释放的路径
		relasePath = getDir("dex", MODE_PRIVATE).getAbsolutePath();
		setRelasePath(relasePath);
		fillListData();
	}

	@Override
	protected void onDestroy() {
		super.onDestroy();
	}
	//这里填充一下数据,模拟了apk的下载好位置在当前apk的安装位置/cache目录
	private void fillListData() {
		String[] skinArray = { getString(R.string.sky),
				getString(R.string.starry_sky) };
		ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext,
				android.R.layout.simple_list_item_multiple_choice, skinArray);
		mListView.setAdapter(adapter);
		mListView.setOnItemClickListener(new OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> parent, View view,
					int position, long id) {
				String filesDir = getCacheDir().getAbsolutePath();
				String apkPath;
				if (position == 0) {
					apkPath = filesDir + File.separator + "sky.apk";
					changeSkin(apkPath);
					showDialogFromSkin(apkPath);
				} else if (position == 1) {
					apkPath = filesDir + File.separator + "Starry.apk";
					changeSkin(apkPath);
					showDialogFromSkin(apkPath);
				}
			}
		});
	}

	@SuppressLint("NewApi")
	private void showDialogFromSkin(String dexPath) {
		try {
			Class skinImplCls;
			SkinChangeInferface skinInt;
			mClassLoader = getDexClassLoader(dexPath, relasePath);
			skinImplCls = mClassLoader.loadClass(SKIN_IMPL_CLASSNAME);
			skinInt = (SkinChangeInferface) skinImplCls.newInstance();
			skinInt.showSkinNameInDialog(mContext);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
}
最后,我们就只要简单的实现插件工程中的那个接口方法即可

下面这段是SkinSky插件项目中的实现,主要为了保证接口调用一致性,这里的插件包名都规定统一了。

public class SkinImpl implements SkinChangeInferface {

	@Override
	public void showSkinNameInDialog(Context context) {
		AlertDialog.Builder builder = new Builder(context);
		builder.setMessage("你好,这是来自蓝天皮肤的提示");
		  builder.setTitle(R.string.app_name);
		  builder.setNegativeButton("取消", new Dialog.OnClickListener() {
			   @Override
			   public void onClick(DialogInterface dialog, int which) {
				   dialog.dismiss();
			   }
			  });
		  Dialog dialog = builder.create();
		  dialog.show();
	}

	@Override
	public MySkinBean getMySkin(Context context) {
		MySkinBean skin = new MySkinBean();
		skin.setBgImageId(R.drawable.tk_skin);
		skin.setSkinName("蓝天皮肤");
		return skin;
	}

}

然后打包apk,push到安装好的主项目的cache下


运行一下程序吧,感觉有点意思。但实际情况我们还需要做很多规则的统一和定制,这篇遐想篇只是带个观者一个思路,将其拓展是很有意思的事情。

示例相关代码下载如意门传送链接


发布了70 篇原创文章 · 获赞 36 · 访问量 22万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章