Android-->将布局文件放在服务器上,动态改变布局。
2015-03-11 16:14
134 查看
目前在做项目时候有这样的需求:布局文件的控件类型大致相同,例如某布局文件由GridView、ScrollView、TextView、Button四个控件组成,但是控件的摆放位置不同。因为摆放的方式很多,不可能把所有摆放方式都写一个布局文件,因为这样不利于迭代开发。这时候就想出能不能把布局文件放在服务器上,当需要某布局的时候,从服务器下载布局文件保存到存储卡上,然后读取存储卡上的布局文件。
思路大致清除了,那么现在开始进行可行性分析。
继续按住Ctrl+鼠标左键,点击return中的inflate(....)进入:
wow,它是一个接口,而且是继承自XmlPullParser,那么XmlPullParser是什么?查看它的源码,这里就不贴出源码的英文介绍,因为巴拉巴拉太长了!这里只要知道,它是解析XML文件要使用到的。到这里,我想大概有思路了,因为Android是支持解析Xml文件的,例如游戏的配置文件可以用XML文件完成。如果我们能够将SD卡上的布局文件转成XmlPullParser的话就应该可以完成这种需求!
最后来看看inflate方法的最终实现代码:
关于从SD卡读取XML文件的网上例子有很多,下面项目中用到的方法,用来得到XmlPullParser。
首先我们要把Android中的LayoutInflate类拷贝一份到自己工程中,接下来的工作就是照葫芦画瓢了!看下图:
从上图,可以看到LayoutInflate中的所有inflate方法,可惜就是没有以String 作为参数的方法,那么这里我们就按照源码中inflate方法进行增加,如下:
最后就可以调到系统原有的以XmlPullPaser作为参数的inflate方法。那么这里就完成了吗?其实没有,这里做的操作仅仅只是读取布局文件操作。里面的控件如何识别,控件的属性怎么操作,都需要继续研究。XML文件里面有布局,也有控件,若SD卡上的XML文件有某控件,那么我们就要重写该控件。因为这里读取布局的逻辑是我们自己写的,那么相应的View的操作也要自己重写。具体原因将在后续的某期博客中进行分析。例如XML文件中有Button控件:
好了,今天的分析就到这里!
..................................................................................................................................
文章中肯定有写的不到位的,欢迎大家给出更好的思路。需要源码的可以留言,目前这部分操作已经打包成jar,可以在项目中使用。
思路大致清除了,那么现在开始进行可行性分析。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }上面的代码太熟悉不过了,创建一个新Android工程,这玩意就已经写好了!通过setContentView()方法,我们可以设置布局文件,但是这里的参数是R文件的布局ID,如果是布局文件的路径就好了,这样就可以直接读取SD卡的XML布局文件了!按住Ctrl+鼠标左键,点击setContentView()进入:
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initActionBar(); }从这里我们可以看到,setContentView()方法调用了Window类里的setContentView()方法,调来调去啊!继续按住Ctrl+鼠标左键,点击setContentView()进入:
/** * Convenience for * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)} * to set the screen content from a layout resource. The resource will be * inflated, adding all top-level views to the screen. * * @param layoutResID Resource ID to be inflated. * @see #setContentView(View, android.view.ViewGroup.LayoutParams) */ public abstract void setContentView(int layoutResID);花擦,这里变成了抽象函数,在哪里实现啊!不要惊慌,通过源码可以得知,Window类是一个抽象类,那么它的实现应该是在子类中。那么Window类有哪些子类?
/** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * * <p>The only existing implementation of this abstract class is * android.policy.PhoneWindow, which you should instantiate when needing a * Window. Eventually that class will be refactored and a factory method * added for creating Window instances without knowing about a particular * implementation. */这是Window类源码的开头说明,从中可以立马看到PhoneWindow,对!没错就是它!在PhoneWindow中终于找到了它的具体实现!
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } <span style="color:#ff0000;"> mLayoutInflater.inflate(layoutResID, mContentParent); </span> final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }从标红的代码,我们可以看到,布局的ID是用在了该段代码,看到了inflate方法,我想大家应该不陌生,在BaseAdpter中经常会用到,用来填充布局的!继续按住Ctrl+鼠标左键,点击inflate(....)进入:
/** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. * * @param resource ID for an XML layout resource to load (e.g., * <code>R.layout.main_page</code>) * @param root Optional view to be the parent of the generated hierarchy. * @return The root View of the inflated hierarchy. If root was supplied, * this is the root View; otherwise it is the root of the inflated * XML file. */ public View inflate(int resource, ViewGroup root) { return inflate(resource, root, root != null); }
继续按住Ctrl+鼠标左键,点击return中的inflate(....)进入:
/** * Inflate a new view hierarchy from the specified xml resource. Throws * {@link InflateException} if there is an error. * * @param resource ID for an XML layout resource to load (e.g., * <code>R.layout.main_page</code>) * @param root Optional view to be the parent of the generated hierarchy (if * <em>attachToRoot</em> is true), or else simply an object that * provides a set of LayoutParams values for root of the returned * hierarchy (if <em>attachToRoot</em> is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to * the root parameter? If false, root is only used to create the * correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ public View inflate(int resource, ViewGroup root, boolean attachToRoot) { if (DEBUG) System.out.println("INFLATING from resource: " + resource); <span style="color:#ff0000;"> XmlResourceParser parser = getContext().getResources().getLayout(resource); </span> try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }重点来了,看到标红代码,我们可以看到布局ID被getLayout方法作为参数使用,最后返回了XmlResourceParser这玩意,那么它是啥?点击它进去看看:
/** * The XML parsing interface returned for an XML resource. This is a standard * XmlPullParser interface, as well as an extended AttributeSet interface and * an additional close() method on this interface for the client to indicate * when it is done reading the resource. */ public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable { /** * Close this interface to the resource. Calls on the interface are no * longer value after this call. */ public void close(); }
wow,它是一个接口,而且是继承自XmlPullParser,那么XmlPullParser是什么?查看它的源码,这里就不贴出源码的英文介绍,因为巴拉巴拉太长了!这里只要知道,它是解析XML文件要使用到的。到这里,我想大概有思路了,因为Android是支持解析Xml文件的,例如游戏的配置文件可以用XML文件完成。如果我们能够将SD卡上的布局文件转成XmlPullParser的话就应该可以完成这种需求!
最后来看看inflate方法的最终实现代码:
/** * Inflate a new view hierarchy from the specified XML node. Throws * {@link InflateException} if there is an error. * <p> * <em><strong>Important</strong></em> For performance * reasons, view inflation relies heavily on pre-processing of XML files * that is done at build time. Therefore, it is not currently possible to * use LayoutInflater with an XmlPullParser over a plain XML file at runtime. * * @param parser XML dom node containing the description of the view * hierarchy. * @param root Optional view to be the parent of the generated hierarchy (if * <em>attachToRoot</em> is true), or else simply an object that * provides a set of LayoutParams values for root of the returned * hierarchy (if <em>attachToRoot</em> is false.) * @param attachToRoot Whether the inflated hierarchy should be attached to * the root parameter? If false, root is only used to create the * correct subclass of LayoutParams for the root view in the XML. * @return The root View of the inflated hierarchy. If root was supplied and * attachToRoot is true, this is root; otherwise it is the root of * the inflated XML file. */ public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); if (DEBUG) { System.out.println("**************************"); System.out.println("Creating root view: " + name); System.out.println("**************************"); } if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml View temp; if (TAG_1995.equals(name)) { temp = new BlinkLayout(mContext, attrs); } else { temp = createViewFromTag(root, name, attrs); } ViewGroup.LayoutParams params = null; if (root != null) { if (DEBUG) { System.out.println("Creating params from root: " + root); } // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } if (DEBUG) { System.out.println("-----> start inflating children"); } // Inflate all children under temp rInflate(parser, temp, attrs, true); if (DEBUG) { System.out.println("-----> done inflating children"); } // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; } } } catch (XmlPullParserException e) { InflateException ex = new InflateException(e.getMessage()); ex.initCause(e); throw ex; } catch (IOException e) { InflateException ex = new InflateException( parser.getPositionDescription() + ": " + e.getMessage()); ex.initCause(e); throw ex; } finally { // Don't retain static reference on context. mConstructorArgs[0] = lastContext; mConstructorArgs[1] = null; } return result; } }最后通过该方法,就可以返回一个View,该View相当于整个布局(因为布局也是View)。通过返回的View,就可以进行findViewById的操作了!
关于从SD卡读取XML文件的网上例子有很多,下面项目中用到的方法,用来得到XmlPullParser。
public XmlPullParser getXmlPullParser(String resource) { XmlPullParser parser = Xml.newPullParser(); try { FileInputStream is = new FileInputStream(resource); parser.setInput(is, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return parser; }这里传入的就是XML在SD卡中的路径,通过相关操作就可以得到XmlPullParser对象。答题思路清晰明了,接下来说说如何做?
首先我们要把Android中的LayoutInflate类拷贝一份到自己工程中,接下来的工作就是照葫芦画瓢了!看下图:
从上图,可以看到LayoutInflate中的所有inflate方法,可惜就是没有以String 作为参数的方法,那么这里我们就按照源码中inflate方法进行增加,如下:
public View inflate(String resource, ViewGroup root) { return inflate(resource, root, root != null); }接下来还有
public View inflate(String resource, ViewGroup root, boolean attachToRoot) { XmlPullParser parser = getXmlPullParser(resource); return inflate(parser, root, attachToRoot); }
最后就可以调到系统原有的以XmlPullPaser作为参数的inflate方法。那么这里就完成了吗?其实没有,这里做的操作仅仅只是读取布局文件操作。里面的控件如何识别,控件的属性怎么操作,都需要继续研究。XML文件里面有布局,也有控件,若SD卡上的XML文件有某控件,那么我们就要重写该控件。因为这里读取布局的逻辑是我们自己写的,那么相应的View的操作也要自己重写。具体原因将在后续的某期博客中进行分析。例如XML文件中有Button控件:
<Button android:id="@+id/btn_back" android:layout_width="@dimen/mine_back_btn_width" android:layout_height="@dimen/mine_back_btn_height" android:layout_gravity="center_vertical" android:background="@drawable/mine_btn_back" />例如id,layout_width,layout_height等属性,如果我们不重写Button,那么这些属性都是无用的。因为我们没有进行相应的操作,大家通过阅读Button源码就可以了解AttributeSet的相关知识,有时间在后续博客我也将进行分析。
public class VAButton extends android.widget.Button { public VAButton(Context context, AttributeSet attrs) { super(context); setAttributeSet(attrs); } @SuppressWarnings("deprecation") public void setAttributeSet(AttributeSet attrs) { HashMap<String, ParamValue> map = YDResource.getInstance().getViewMap(); int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { ParamValue key = map.get(attrs.getAttributeName(i)); if (key == null) { continue; } switch (key) { case id: this.setTag(attrs.getAttributeValue(i)); break; case text: String value = YDResource.getInstance().getString( attrs.getAttributeValue(i)); this.setText(value); break; case ellipsize: if (attrs.getAttributeBooleanValue(i, false)) { this.setFocusable(true); this.setFocusableInTouchMode(true); this.setSingleLine(true); this.setEllipsize(TruncateAt.MARQUEE); this.setMarqueeRepeatLimit(1000); this.setSingleLine(); this.setHorizontallyScrolling(true); this.requestFocus(); } break; case fadingEdge: this.setHorizontalFadingEdgeEnabled(attrs .getAttributeBooleanValue(i, false)); break; case scrollHorizontally: this.setHorizontallyScrolling(attrs.getAttributeBooleanValue(i, false)); break; case textColor: this.setTextColor(YDResource.getInstance().getIntColor( attrs.getAttributeValue(i))); break; case textSize: String val1 = attrs.getAttributeValue(i); if (!TextUtils.isEmpty(val1)) { this.setTextSize(YDResource.getInstance() .calculateRealSize(val1)); } break; case visibility: String val2 = attrs.getAttributeValue(i); if (!TextUtils.isEmpty(val2)) { if (val2.equals("invisible")) { this.setVisibility(View.INVISIBLE); } else if (val2.equalsIgnoreCase("gone")) { this.setVisibility(View.GONE); } } break; case background: String backgroundString = attrs.getAttributeValue(i); if (backgroundString.startsWith("#")) { this.setBackgroundColor(YDResource.getInstance() .getIntColor(attrs.getAttributeValue(i))); } else { if (backgroundString.startsWith("@drawable/")) { backgroundString = backgroundString.substring(10); } String rootpath = getContext().getFilesDir().toString(); StringBuilder sb = new StringBuilder(); sb.append(rootpath).append("/").append(backgroundString) .append(".png"); Bitmap bm = BitmapFactory.decodeFile(sb.toString()); setBackgroundDrawable(new BitmapDrawable(bm)); } break; case textStyle: if ("bold".equalsIgnoreCase(attrs.getAttributeValue(i))) this.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); break; case style: String style = attrs.getAttributeValue(i); style = style.substring(style.indexOf("/") + 1); Log.i("button", "设置属性值"); int id = YDResource.getInstance().getIdentifier( "R.style." + style); this.setTextAppearance(getContext(), id); break; case src: break; case contentDescription: String content = attrs.getAttributeValue(i); this.setContentDescription(content); break; case gravity: this.setGravity(Gravity.CENTER_HORIZONTAL); break; default: break; } } } }通过上面代码,我们可以知道,这里的操作都是对各种属性标签利用代码来进行设置。如果有些复杂的属性不想写了,其实最后还是可以通过代码来完成的。这里就是你的布局需要多少控件属性,就要在代码中完成多少属性的设置。布局控件的代码,以FrameLayout为例:
public class VAFrameLayout extends android.widget.FrameLayout { public VAFrameLayout(Context context, AttributeSet attrs) { super(context); setLayoutParams(generateLayoutParams(attrs)); } @SuppressWarnings({ "unchecked", "deprecation" }) @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { // TODO Auto-generated method stub LayoutParams params = this.generateDefaultLayoutParams(); HashMap<String, ParamValue> map = YDResource.getInstance() .getLayoutMap(); params.width = -2; params.height = -2; int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { String name = attrs.getAttributeName(i); ParamValue key = map.get(name); if (key == null) { continue; } switch (key) { case layout_width: String width = attrs.getAttributeValue(i); if (width.startsWith("f") || width.startsWith("m")) { params.width = LayoutParams.MATCH_PARENT; break; } if (width.startsWith("w")) { params.width = LayoutParams.WRAP_CONTENT; break; } params.width = YDResource.getInstance() .calculateRealSize(width); break; case layout_height: String height = attrs.getAttributeValue(i); if (height.startsWith("f") || height.startsWith("m")) { params.width = -1; break; } if (height.startsWith("w")) { params.width = -2; break; } params.height = YDResource.getInstance().calculateRealSize( height); break; case layout_gravity: params.gravity = YDResource.getInstance().getGravity( attrs.getAttributeValue(i)); break; case layout_marginLeft: params.leftMargin = YDResource.getInstance().calculateRealSize( attrs.getAttributeValue(i)); break; case layout_margin: int tm = YDResource.getInstance().calculateRealSize( attrs.getAttributeValue(i)); params.setMargins(tm, tm, tm, tm); break; default: break; } } return params; } }
好了,今天的分析就到这里!
..................................................................................................................................
文章中肯定有写的不到位的,欢迎大家给出更好的思路。需要源码的可以留言,目前这部分操作已经打包成jar,可以在项目中使用。
相关文章推荐
- Android中View绘制优化二一---- 使用<include />标签复用布局文件
- android面试题目大全<第一部分>,android基本的UI控件和布局文件知识
- Android-->View.setX()和.setY()的使用(移动布局,设置布局位置,动态添加View)
- android This tag and its children can be replaced by one <TextView/> and a compound drawable 布局文件提示
- Android开发之动态创建布局文件及控件
- 修改Android中strings.xml文件, 动态改变数据
- SpringMVC 的<mvc:resources>使用映射路径展示文件服务器上的图片
- Android动态改变布局,比如登陆弹出软键盘,登陆框上移(转载)
- <Android>递归删除文件夹中的文件以及文件夹
- android中对/data/data/<package name>/files下文件的读写操作
- juahya 一个动态解析android layout xml 布局文件的框架
- Error--->android工程导入找不到R文件
- android Activity.this.getLayoutInflater()::动态加载布局文件,实现弹窗效
- Android中View绘制优化二一---- 使用<include />标签复用布局文件
- Android 读取清单文件<meta-data>元素的数据
- Android布局优化之<merge>与<ViewStub>标签使用
- Android实战简易教程<四>(ScrollView和HorizontalScrollView动态添加控件并提供事件监听)
- Android实战简易教程<十一>(树形组件:ExpandableListView显示和动态添加删除)
- Android实战简易教程<十三>(五大布局研究)
- Android实战简易教程<十七>(LayoutAnimation布局动画)