您的位置:首页 > 移动开发 > Android开发

Android-->将布局文件放在服务器上,动态改变布局。

2015-03-11 16:14 134 查看
目前在做项目时候有这样的需求:布局文件的控件类型大致相同,例如某布局文件由GridView、ScrollView、TextView、Button四个控件组成,但是控件的摆放位置不同。因为摆放的方式很多,不可能把所有摆放方式都写一个布局文件,因为这样不利于迭代开发。这时候就想出能不能把布局文件放在服务器上,当需要某布局的时候,从服务器下载布局文件保存到存储卡上,然后读取存储卡上的布局文件。

思路大致清除了,那么现在开始进行可行性分析。

@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,可以在项目中使用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐