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

转自:https://blog.csdn.net/chan1116/article/details/44200405

目前在做项目时候有这样的需求:布局文件的控件类型大致相同,例如某布局文件由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();
            }
           mLayoutInflater.inflate(layoutResID, mContentParent);
           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);
                XmlResourceParser parser = getContext().getResources().getLayout(resource);
            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;
        }
     
    }


好了,今天的分析就到这里!

..................................................................................................................................

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值