Android插件化的思考——仿QQ一键换肤,思考比实现更重要!
今天群友希望写一个关于插件的Blog,思来想去,插件也不是很懂,只是用大致的思路看看能不能模拟一个,思路还是比较重要的,如果你有兴趣的话,也可以加群:555974449,你也可以说出你想看的Blog哦,嘿嘿!好的,不多说,我们进入正题:
关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图
我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致的可以猜想:
首选是下载到本地指定文件夹,然后通过插件加载到我们的apk,最后应用为皮肤,逻辑大致是这样的逻辑了,那我们是不是应该动动手啊动动脑?
首选我们新建一个工程好了——PlugInSample
一.实现思路其实说起来,这个插件的实现思路,确实是比较的麻烦,思来想去,还是一种办法比较靠谱,首先,我们刻意去获取手机上所有的安装的/未安装的程序,过滤掉没用的,留下我们的插件apk,我们的插件apk怎么去辨别呢?我们可用通过设置sharedUserId,然后用实体类把插件名称和包名保存下来,有了包名,就比较好说了,我们可用获取插件的上下文,也就是createPackageContext,然后就可以做点坏事了,我们可以去剖析我们的R文件
因为R文件里面都是静态的原因,我们很容易联想到反射机制,是的,我们可以再一次过滤掉无用的信息,通过我们的PathClassLoader去加载,访问我们的内加载器反射到我们的图片ID,也就是后面的那段数字,然后,嘿嘿,就可以使用了,是不是思路比较清晰了?这里要注意的就是图片命名统一,这样就比较号过来,那具体我们应该怎么做?
二.PlugIn主程序我们写一个Spinner,每次切换就直接换肤怎么样?OK,每次换的时候就从插件APK里加载我们的图片资源,看起来是比较顺畅的逻辑,那我们具体该怎么做呢?
?xml version="1.0" encoding="utf-8"? LinearLayout android:id="@+id/mLinearLayout" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" Spinner android:id="@+id/mSpinner" android:layout_width="wrap_content" android:layout_height="wrap_content"/ /LinearLayout1.初始化
/** * 初始化View private void initView() { //初始化控件 mSpinner = (Spinner) findViewById(R.id.mSpinner); }
当然,我这刚应用就一个View,但是实际开发当中可不止,所以步骤一定要明了
2.获取所有的插件* 获取手机里的插件 * @return private List PlugInBean findPlugIn() { mList = new ArrayList (); //获取相关信息 PackageManager mPackageManager = getPackageManager(); //获取卸载/未安装的安装包信息 List PackageInfo mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); //遍历拿到我们的信息 for (PackageInfo info : mUninstallPackage) { String pkgNmae = info.packageName; //获取shareId,根据id判断是否是我们的ID String shareUserId = info.sharedUserId; if (!TextUtils.isEmpty(shareUserId)) { //如果id相同 if (shareUserId.equals("com.liuguilin.share")) { //且排除自己的包名 if (!pkgNmae.equals(getPackageName())) { //这个就是我们的插件了 String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString(); PlugInBean bean = new PlugInBean(); bean.setLabelNmae(lable); bean.setPackagNmae(pkgNmae); mList.add(bean); return mList; }
这里就是过滤了一下,通过sharedUserId去拿到我们的插件APK了,然后就可以拿到我们的包名和应用名,他返回给我们一个数据集
//所有的插件 List PlugInBean allPlugIn = findPlugIn();3.加载皮肤数据
/** * 加载皮肤 * @param allPlugIn private void LoadSkin(List PlugInBean allPlugIn) { //遍历 for (PlugInBean bean : allPlugIn) { HashMap String, Object mMap = new HashMap (); mMap.put("lable", bean.getLabelNmae()); mMap.put("package", bean.getPackagNmae()); mData.add(mMap); //建立Adapter并且绑定数据源 mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1}); //设置数据 mSpinner.setAdapter(mAdapter); //设置监听事件 mSpinner.setOnItemSelectedListener(this); }
我们通过刚才的数据集便可以把我们拿到的数据给直接显示出来了,这里其实可以判断一下size是否为0,如果为0的话也就没有插件,OK,我们设置adapter和监听,做到这里,其实你可以运行一下,虽然我们现在什么都没有,我们要做的还有很多
4.获取插件Context/** * 选中监听事件 * @param adapterView * @param view * @param i * @param l @Override public void onItemSelected(AdapterView ? adapterView, View view, int i, long l) { PlugInBean bean = mList.get(i); //插件的包名 String packageNmae = bean.getPackagNmae(); Context mContext = null; try { //无视警告 访问代码 mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); //获取图片 getImg(packageNmae, mContext); //通过ID加载插件的图片 getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(mListId.get(i))); @Override public void onNothingSelected(AdapterView ? adapterView) {
这里的代码就比较有意思,一定要仔细看,我们首先拿到选中的item的包名,通过我们的createPackageContext拿到我们的上下文,通过这两个我们可用拿到我们的资源ID,也就是R清单里面的ID,然后直接设置window的背景,这里为了好看才设置window的背景,实际上你要设置的是你根布局的背景,那好,我们来看一下如何通过插件的上下文和包名拿到R清单的资源ID
5.获取插件图片 / 返回图片R文件ID / 反射R文件/** * 获取插件图片 / 返回图片R文件ID / 反射R文件 * @param packageNmae * @param mContext private void getImg(String packageNmae, Context mContext) { //类加载器反射插件 PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader()); //反射 $ 访问类加载器 try { Class ? forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass); //拿到所有图片的id Field[] files = forNmae.getDeclaredFields(); for (Field id : files) { //过滤 / 这里的命名可以注意一下 if (id.getName().startsWith("img")) { int drawId = 0; ////这就是我们图片R下的ID drawId = id.getInt(R.drawable.class); mListId.add(drawId); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }
这里我们做了很多事情,首选是拿到我们的类加载器去反射我们的插件,然后通过Class去拿我们的资源,这里注意packageNmae是我们的文件目录,他下面的R文件,$代表类部类的意思,他下面的drawable子节点,然后再一次过滤,过滤之后我们可用遍历一遍拿到我们的ID用List保存起来,也就有了我们选中的时候的设置,好的,到这里主程序算是编写完成了,不过要注意的是,记住要添加sharedUserId啊,至关重要!!!
android:sharedUserId="com.liuguilin.share"
我们现在运行也是空的,无意义,我们直接来写我们的插件吧!
三PlugInApk插件插件的编写很简单,我们新建一个PlugInApk的工程
工程里要做的事情就三件
1.添加sharedUserIdandroid:sharedUserId="com.liuguilin.share"2.更改name
这就取决于你了,比如我这里是Angelababy的主题,我就把名字改成Angelababy
3.把图片放在drawable文件夹下好的,做完这三部,我们本能的把插件运行一下,运行之后,我们再次启动主程序,你会看到....
其实我们主程序里啥也没有,对吧,但是的却加载进来了,这就说明我们的插件化算是圆满实现了,那我们多来点主题看看最终的效果是什么样子的?
通过这个思路确实可以加载到图片,但是这个逻辑依旧有些不完美,不过最重要的,思考比实现更重要,对吧,后续的也就是一步步的优化了,希望大家和我一起探讨一下!
当上完整的代码
MainActivitypackage com.liuguilin.pluginsample; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.view.View; import android.widget.AdapterView; import android.widget.SimpleAdapter; import android.widget.Spinner; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import dalvik.system.PathClassLoader; public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { //下拉 private Spinner mSpinner; //数据源 private SimpleAdapter mAdapter; //插件数据 private List PlugInBean mList; //加载的皮肤数据 private List Map String, Object mData = new ArrayList (); //资源id private int drawId = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); //所有的插件 List PlugInBean allPlugIn = findPlugIn(); //加载皮肤数据 LoadSkin(allPlugIn); * 加载皮肤 * @param allPlugIn private void LoadSkin(List PlugInBean allPlugIn) { //遍历 for (PlugInBean bean : allPlugIn) { HashMap String, Object mMap = new HashMap (); mMap.put("lable", bean.getLabelNmae()); mMap.put("package", bean.getPackagNmae()); mData.add(mMap); //建立Adapter并且绑定数据源 mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1}); //设置数据 mSpinner.setAdapter(mAdapter); //设置监听事件 mSpinner.setOnItemSelectedListener(this); * 获取手机里的插件 * @return private List PlugInBean findPlugIn() { mList = new ArrayList (); //获取相关信息 PackageManager mPackageManager = getPackageManager(); //获取卸载/未安装的安装包信息 List PackageInfo mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); //遍历拿到我们的信息 for (PackageInfo info : mUninstallPackage) { String pkgNmae = info.packageName; //获取shareId,根据id判断是否是我们的ID String shareUserId = info.sharedUserId; if (!TextUtils.isEmpty(shareUserId)) { //如果id相同 if (shareUserId.equals("com.liuguilin.share")) { //且排除自己的包名 if (!pkgNmae.equals(getPackageName())) { //这个就是我们的插件了 String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString(); PlugInBean bean = new PlugInBean(); bean.setLabelNmae(lable); bean.setPackagNmae(pkgNmae); mList.add(bean); return mList; * 初始化View private void initView() { //初始化控件 mSpinner = (Spinner) findViewById(R.id.mSpinner); * 选中监听事件 * @param adapterView * @param view * @param i * @param l @Override public void onItemSelected(AdapterView ? adapterView, View view, int i, long l) { PlugInBean bean = mList.get(i); //插件的包名 String packageNmae = bean.getPackagNmae(); Context mContext = null; try { //无视警告 访问代码 mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); //获取图片 getImg(packageNmae, mContext); //通过ID加载插件的图片 getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(drawId)); //findViewById(R.id.mLinearLayout).setBackgroundDrawable(mContext.getResources().getDrawable(drawId)); @Override public void onNothingSelected(AdapterView ? adapterView) {
//类加载器反射插件 PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader()); //反射 $ 访问类加载器 try { Class ? forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass); //拿到所有图片的id Field[] files = forNmae.getDeclaredFields(); for (Field id : files) { //过滤 / 这里的命名可以注意一下 if (id.getName().startsWith("img")) { ////这就是我们图片R下的ID drawId = id.getInt(R.drawable.class); //mListId.add(drawId); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace();
这里还有一个实体类哦,具体看Demo:
package com.liuguilin.pluginsample; * 项目名: PlugInSample * 包名: com.liuguilin.pluginsample * 文件名: PlugInBean * 创建者: LGL * 创建时间: 2016/9/17 4:18 * 描述: 插件实体类 public class PlugInBean { //包名 private String packagNmae; //应用名 private String labelNmae; public String getPackagNmae() { return packagNmae; public void setPackagNmae(String packagNmae) { this.packagNmae = packagNmae; public String getLabelNmae() { return labelNmae; public void setLabelNmae(String labelNmae) { this.labelNmae = labelNmae;主程序及插件程序:http://download.csdn.net/detail/qq_26787115/9632026 有兴趣的可以加群:555974449
相关文章
- python爬取对方qq好友_小白工作室QQ
- 74款android开机动画,修改Android系统开机动画
- android 浏览器 开发,Android 浏览器的开发实例分享
- Android 显示刷新机制、VSYNC和三重缓存机制
- android okio使用方法,Android 开源框架 Okio 原理剖析「建议收藏」
- strictmode android,Android中的StrictMode
- delphixe5 android,Delphi XE5 Android手机端转换Ansi字符串
- 测试android sdk是否安装成功,配置Android SDK
- iphone4装android,iPhone4可安装Android实现双系统启动.pdf
- android登录注册跳转的代码_Android开发代码
- android中怎么在View构造的attrs中拿到android给的属性以及attrs属性介绍[通俗易懂]
- Android resource linking failed_android sdk location should not
- eclipse中android开发_Android开发教程
- Android scheme呼起App
- 【Android 应用开发】Activity生命周期 与 Activity 之间的通信
- Ubuntu 上使用 ADB 备份 Android 数据
- [android] 自定义广播事件详解手机开发
- Android资源混淆打包方案详解手机开发
- android虚线边框详解手机开发
- Android Auto亮相:全语音控制
- 乐播科技冯森:我为什么不看好Android电视游戏
- QQ拼音在Linux中的应用(qq拼音 linux)
- android自定义Android菜单背景的代码
- Android中实现多行、水平滚动的分页的Gridview实例源码
- android中WebView和javascript实现数据交互实例