背景
在android中,一成不变的UI布局可能会使用户厌烦(现在基本上都是ViewPager+ListView的方式),那么有没有什么方式实现动态更新UI布局提高用户的体验呢?答案是肯定的,本文就是介绍一种方式实现动态更新UI布局的方式。
技术途径
在动态实现类补丁这篇文章中,我实现了动态加载类,它可以实现dalvik动态更新类(art原生支持文章提到方式),结合这篇文章我们可以很清楚明白,在实现动态更新类的时候,同时替换布局xml文件也是可以得。这个时候我们需要将dex文件,layout等资源文件一起打包生成APK。具体实现是在Activity setContentView():
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext = this; String apkPath = HookManager.getInstance().getPatchDir(mContext).getAbsolutePath() + File.separator + "DexTest.apk"; PatchResource patchResource = ResourceManager.getInstance().getPatchResource(mContext, apkPath); int resId = patchResource.getResApkLayoutId("activity_main"); if (resId <= 0) { setContentView(R.layout.activity_main); } else { setContentView(resId); }
....
}而PatchResource类主要是对patch中的资源文件进行提取:具体实现:
/** * 获取apk里面的资源文件 * Created by Jarlene on 2015/11/23. */ public class PatchResource { public static final String TAG = PatchResource.class.getSimpleName(); private Resources res;// 获取的资源apk里面的res private String apkPackageName;// 资源apk里面的包名 private PatchContext mPatchContext; public PatchResource(Context context, String apkPatch) { mPatchContext = new PatchContext(context, apkPatch); res = mPatchContext.getResources(); apkPackageName = ApkUtils.getPackageInfo(context, apkPatch).packageName; } public PatchResource(Resources res, String apkPackageName) { this.res = res; this.apkPackageName = apkPackageName; } /** * 获取layout文件中的id号 * * @param layoutName * layout名 */ public int getResApkLayoutId(String layoutName) { Log.d(TAG, "getResApkLayoutId"); return res.getIdentifier(layoutName, "layout", apkPackageName); } /** * 获取布局layout文件 * * @param context * 上下文 * @params layoutName * @return view */ public View getResApkLayoutView(Context context, String layoutName) { Log.d(TAG,"getResApkLayoutView"); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(res.getLayout(getResApkLayoutId(layoutName)), null); } /** * 获取控件view的id号 * * @param widgetName * 控件名 */ public int getResApkWidgetViewID(String widgetName) { Log.d(TAG,"getResApkWidgetViewID"); return res.getIdentifier(widgetName, "id", apkPackageName); } /** * 获取布局文件中的控件 * * @params layout,资源apk中的布局(view) * @params widgetName 控件名称 * @return widgetView */ public View getResApkWidgetView(View layout, String widgetName) { Log.d(TAG,"getResApkWidgetView"); return layout.findViewById(getResApkWidgetViewID(widgetName)); } /** * 获取drawable文件的id * * @param imgName * 图片名字 */ public int getDrawableId(String imgName) { Log.d(TAG,"getDrawableId"); return res.getIdentifier(imgName, "drawable", apkPackageName); } /** * 获取图片资源 * * @param imgName * @return drawable */ public Drawable getResApkDrawable(String imgName) { Log.d(TAG,"getResApkDrawable"); return res.getDrawable(getDrawableId(imgName)); } /** * 获取string文件中的id号 * * @param stringName * 字符串在String文件中的名字 */ public int getResApkStringId(String stringName) { Log.d(TAG,"getResApkStringId"); return res.getIdentifier(stringName, "string", apkPackageName); } /** * 获取String字符串 * * @param stringName * @return string */ public String getResApkString(String stringName) { Log.d(TAG,"getResApkString"); return res.getString(getResApkStringId(stringName)); } /** * 获取anim文件中的id号 * * @param animationName */ public int getResApkAnimId(String animationName) { Log.d(TAG,"getResApkAnimId"); return res.getIdentifier(animationName, "anim", apkPackageName); } /** * 获取anim文件 XmlPullParser * * @param animationName * @return XmlPullParser */ public XmlPullParser getResApkAnimXml(String animationName) { Log.d(TAG,"getResApkAnimXml"); return res.getAnimation(getResApkAnimId(animationName)); } /** * 获取动画anim * * @params animationName * @param context */ public Animation getResApkAnim(Context context, String animationName) { Log.d(TAG,"getResApkAnim"); Animation animation = null; XmlPullParser parser = getResApkAnimXml(animationName); AttributeSet attrs = Xml.asAttributeSet(parser); try { animation = createAnimationFromXml(context, parser, null, attrs); } catch (XmlPullParserException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return animation; } /** * 获取anim动画 */ private Animation createAnimationFromXml(Context c, XmlPullParser parser, AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException { Log.d(TAG,"createAnimationFromXml"); Animation anim = null; int type; int depth = parser.getDepth(); while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals("set")) { anim = new AnimationSet(c, attrs); createAnimationFromXml(c, parser, (AnimationSet) anim, attrs); } else if (name.equals("alpha")) { anim = new AlphaAnimation(c, attrs); } else if (name.equals("scale")) { anim = new ScaleAnimation(c, attrs); } else if (name.equals("rotate")) { anim = new RotateAnimation(c, attrs); } else if (name.equals("translate")) { anim = new TranslateAnimation(c, attrs); } else { throw new RuntimeException("Unknown animation name: "+ parser.getName()); } if (parent != null) { parent.addAnimation(anim); } } return anim; } /** * 获取 color文件中的id号 * * @param colorName */ public int getResApkColorId(String colorName) { Log.d(TAG,"getResApkColorId"); return res.getIdentifier(colorName, "color", apkPackageName); } /** * 获取color 值 * * @param colorName * @return int */ public int getResApkColor(String colorName) { Log.d(TAG,"getResApkColor"); return res.getColor(getResApkColorId(colorName)); } /** * 获取 dimens文件中的id号 * * @param dimenName */ public int getResApkDimensId(String dimenName) { Log.d(TAG,"getResApkDimensId"); return res.getIdentifier(dimenName, "dimen", apkPackageName); } /** * 获取dimens文件中值 * * @param dimenName * @return float */ public float getResApkDimens(String dimenName) { Log.d(TAG,"getResApkDimens"); return res.getDimension(getResApkDimensId(dimenName)); } }里面的PatchContext主要是代理实现Context,具体如下:
/** * 主要为patch apk实现资源提取(伪Context) * Created by Jarlene on 2015/12/1. */ public class PatchContext extends ContextThemeWrapper { private AssetManager mAssetManager; private Resources mResources; private Resources mProxyResource; private Context mContext; private String mPatchPath; public PatchContext(Context base, String apkPath) { super(base, 0); this.mContext = base; this.mProxyResource = base.getResources(); this.mPatchPath = apkPath; } @Override public Resources getResources() { if (mResources == null) { mResources = new Resources(getAssets(), mProxyResource.getDisplayMetrics(), mProxyResource.getConfiguration()); } return mResources; } @Override public AssetManager getAssets() { if (mAssetManager == null) { mAssetManager = (AssetManager) newInstanceObject(AssetManager.class); invokeMethod(mAssetManager, "addAssetPath", new Class[]{String.class}, new Object[]{mPatchPath}); } return mAssetManager; } private Object invokeMethod(Object obj, String methodName, Class[] valueType, Object[] values) { try { Class<?> clazz = obj.getClass(); Method method = clazz.getDeclaredMethod(methodName, valueType); method.setAccessible(true); return method.invoke(obj, values); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } return null; } private Object newInstanceObject(Class<?> clazz){ try { return clazz.getConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } return null; } }到此为止就将patch中的资源提取出来了,同时伴随着Activity类一起加载。实现UI动态更新。
至于怎么生成APK,网上有很多教程,这里不再详细叙述。