标签:des android style blog http color io os 使用
在开发中UI布局是我们都会遇到的问题,随着UI越来越多,布局的重复性、复杂度也会随之增长。Android官方给了几个优化的方法,但是网络上的资料基本上都是对官方资料的翻译,这些资料都特别的简单,经常会出现问题而不知其所以然。这篇文章就是对这些问题的更详细的说明,如果有什么不对的也希望高人指出。
include
首先用得最多的应该是include,按照官方的意思,include就是为了解决重复定义相同布局的问题。例如你有五个界面,这五个界面的顶部都有布局一模一样的一个返回按钮和一个文本控件,在不使用include的情况下你在每个界面都需要重新在xml里面写同样的返回按钮和文本控件的顶部栏,这样的重复工作会相当的恶心。使用include标签,我们只需要把这个会被多次使用的顶部栏独立成一个xml文件,然后在需要使用的地方通过include标签引入即可。其实就相当于C语言、C++中的include头文件一样,我们把一些常用的、底层的API封装起来,然后复用,需要的时候引入它即可,而不必每次都自己写一遍。示例如下 :
my_title_layout.xml
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:id="@+id/my_title_parent_id"
- android:layout_height="wrap_content" >
-
- <ImageButton
- android:id="@+id/back_btn"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:src="@drawable/ic_launcher" />
-
- <TextView
- android:id="@+id/title_tv"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_marginLeft="20dp"
- android:layout_toRightOf="@+id/back_btn"
- android:gravity="center"
- android:text="我的title"
- android:textSize="18sp" />
-
- </RelativeLayout>
include布局文件:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
-
- <include
- android:id="@+id/my_title_ly"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- layout="@layout/my_title_layout" />
-
- </LinearLayout>
这样我们就可以使用my_title_layout了。注意事项
1、使用include最常见的问题就是findViewById查找不到目标控件,其正确的使用形式如下:
- View titleView = findViewById(R.id.my_title_ly) ;
- TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;
- titleTextView.setText("new Title");
首先找到include的id, 例如这里include设置的id为“my_title_ly”,然后再对获取到的titleView.findViewById来查找目标布局中的子控件,例如title_tv就是my_title_layout.xml中定义的子控件。因此我们如果需要查找控件的话,可以设置include标签的id,通过这个id获取include对应的view以后,再通过对这个view进行findViewById才能正确查找。如果你设置了include标签的id,然后通过被include的布局的root view的id来查找子元素的话,则会报错,如下 : - View titleView = findViewById(R.id.my_title_parent_id) ;
- TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;
- titleTextView.setText("new Title");
这样会报空指针异常,因为titleView没有找到,会报空指针。
那么这是怎么回事呢? 我们来分析它的源码看看吧。对于布局文件的解析,最终都会调用到LayoutInflater的inflate方法,该方法最终又会调用rInflate方法,我们看看这个方法。
-
-
-
-
- void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
- boolean finishInflate) throws XmlPullParserException, IOException {
-
- final int depth = parser.getDepth();
- int type;
-
- while (((type = parser.next()) != XmlPullParser.END_TAG ||
- parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
-
- if (type != XmlPullParser.START_TAG) {
- continue;
- }
-
- final String name = parser.getName();
-
- if (TAG_REQUEST_FOCUS.equals(name)) {
- parseRequestFocus(parser, parent);
- } else if (TAG_INCLUDE.equals(name)) {
- if (parser.getDepth() == 0) {
- throw new InflateException("<include /> cannot be the root element");
- }
- parseInclude(parser, parent, attrs);
- } else if (TAG_MERGE.equals(name)) {
- throw new InflateException("<merge /> must be the root element");
- } else if (TAG_1995.equals(name)) {
- final View view = new BlinkLayout(mContext, attrs);
- final ViewGroup viewGroup = (ViewGroup) parent;
- final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
- rInflate(parser, view, attrs, true);
- viewGroup.addView(view, params);
- } else {
- final View view = createViewFromTag(parent, name, attrs);
- final ViewGroup viewGroup = (ViewGroup) parent;
- final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
- rInflate(parser, view, attrs, true);
- viewGroup.addView(view, params);
- }
- }
-
- if (finishInflate) parent.onFinishInflate();
- }
这个方法其实就是遍历xml中的所有元素,然后挨个进行解析。例如解析到一个<TextView>标签,那么就根据用户设置的一些layout_width、layout_height、id等属性来构造一个TextView对象,然后添加到父控件(ViewGroup类型)中。<include>标签也是一样的,我们看到遇到include标签时,会调用parseInclude函数,这就是对<include>标签的解析,我们看看吧。
- private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
- throws XmlPullParserException, IOException {
-
- int type;
-
- if (parent instanceof ViewGroup) {
- final int layout = attrs.getAttributeResourceValue(null, "layout", 0);
- if (layout == 0) {
- final String value = attrs.getAttributeValue(null, "layout");
- if (value == null) {
- throw new InflateException("You must specifiy a layout in the"
- + " include tag: <include layout=\"@layout/layoutID\" />");
- } else {
- throw new InflateException("You must specifiy a valid layout "
- + "reference. The layout ID " + value + " is not valid.");
- }
- } else {
- final XmlResourceParser childParser =
- getContext().getResources().getLayout(layout);
-
- try {
- final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
-
- while ((type = childParser.next()) != XmlPullParser.START_TAG &&
- type != XmlPullParser.END_DOCUMENT) {
-
- }
-
- if (type != XmlPullParser.START_TAG) {
- throw new InflateException(childParser.getPositionDescription() +
- ": No start tag found!");
- }
-
- final String childName = childParser.getName();
-
- if (TAG_MERGE.equals(childName)) {
-
- rInflate(childParser, parent, childAttrs, false);
- } else {
-
- final View view = createViewFromTag(parent, childName, childAttrs);
- final ViewGroup group = (ViewGroup) parent;
-
- ViewGroup.LayoutParams params = null;
- try {
- params = group.generateLayoutParams(attrs);
- } catch (RuntimeException e) {
- params = group.generateLayoutParams(childAttrs);
- } finally {
- if (params != null) {
- view.setLayoutParams(params);
- }
- }
-
-
- rInflate(childParser, view, childAttrs, true);
-
-
-
- TypedArray a = mContext.obtainStyledAttributes(attrs,
- com.android.internal.R.styleable.View, 0, 0);
- int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
-
- int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
- a.recycle();
-
- if (id != View.NO_ID) {
- view.setId(id);
- }
-
- switch (visibility) {
- case 0:
- view.setVisibility(View.VISIBLE);
- break;
- case 1:
- view.setVisibility(View.INVISIBLE);
- break;
- case 2:
- view.setVisibility(View.GONE);
- break;
- }
-
- group.addView(view);
- }
- } finally {
- childParser.close();
- }
- }
- } else {
- throw new InflateException("<include /> can only be used inside of a ViewGroup");
- }
-
- final int currentDepth = parser.getDepth();
- while (((type = parser.next()) != XmlPullParser.END_TAG ||
- parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
-
- }
- }
整个过程就是根据不同的标签解析不同的元素,首先会解析include元素,然后再解析被include进来的布局的root view元素。在我们的例子中对应的root view就是id为my_title_parent_id的RelativeLayout,然后再解析root view下面的所有元素,这个过程是从上面注释的2~4的过程,然后是设置布局参数。我们注意看注释5处,这里就解释了为什么include标签和被引入的布局的根元素都设置了id的情况下,通过被引入的根元素的id来查找子控件会找不到的情况。我们看到,注释5处的会判断include标签的id如果不是View.NO_ID的话会把该id设置给被引入的布局根元素的id,即此时在我们的例子中被引入的id为my_title_parent_id的根元素RelativeLayout的id被设置成了include标签中的id,即RelativeLayout的id被动态修改成了"my_title_ly"。因此此时我们再通过“my_title_parent_id”这个id来查找根元素就会找不到了!
所以结论就是: 如果include中设置了id,那么就通过include的id来查找被include布局根元素的View;如果include中没有设置Id, 而被include的布局的根元素设置了id,那么通过该根元素的id来查找该view即可。拿到根元素后查找其子控件都是一样的。
ViewStub
我们先看看官方的说明:
ViewStub is a lightweight view with no dimension and doesn’t draw anything or participate in the layout. As such, it‘s cheap to inflate and cheap to leave in a view hierarchy. Each ViewStub simply needs to include the android:layout attribute to specify the layout to inflate.
其实ViewStub就是一个宽高都为0的一个View,它默认是不可见的,只有通过调用setVisibility函数或者Inflate函数才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果,这个要被加载的布局通过android:layout属性来设置。例如我们通过一个ViewStub来惰性加载一个消息流的评论列表,因为一个帖子可能并没有评论,此时我可以不加载这个评论的ListView,只有当有评论时我才把它加载出来,这样就去除了加载ListView带来的资源消耗以及延时,示例如下 :
- <ViewStub
- android:id="@+id/stub_import"
- android:inflatedId="@+id/stub_comm_lv"
- android:layout="@layout/my_comment_layout"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom" /
my_comment_layout.xml如下:
- <?xml version="1.0" encoding="utf-8"?>
- <ListView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:id="@+id/my_comm_lv"
- android:layout_height="match_parent" >
-
- </ListView>
在运行时,我们只需要控制id为stub_import的ViewStub的可见性或者调用inflate()函数来控制是否加载这个评论列表即可。示例如下 :
- public class MainActivity extends Activity {
-
- public void onCreate(Bundle b){
-
- setContentView(R.layout.main);
-
-
- ViewStub listStub = (ViewStub) findViewById(R.id.stub_import);
-
- listStub.setVisibility(View.VISIBLE);
-
- ListView commLv = findViewById(R.id.stub_comm_lv);
- if ( listStub.getVisibility() == View.VISIBLE ) {
-
- }
- }
- }
通过setVisibility(View.VISIBILITY)来加载评论列表,
此时你要获取到评论ListView对象的话,则需要通过findViewById来查找,而这个id并不是就是ViewStub的id。这是为什么呢 ?
我们先看ViewStub的部分代码吧:
-
- @SuppressWarnings({"UnusedDeclaration"})
- public ViewStub(Context context, AttributeSet attrs, int defStyle) {
- TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub,
- defStyle, 0);
-
- mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
- mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
-
- a.recycle();
-
- a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);
- mID = a.getResourceId(R.styleable.View_id, NO_ID);
- a.recycle();
-
- initialize(context);
- }
-
- private void initialize(Context context) {
- mContext = context;
- setVisibility(GONE);
- setWillNotDraw(true);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(0, 0);
- }
-
-
- @Override
- public void setVisibility(int visibility) {
- if (mInflatedViewRef != null) {
- View view = mInflatedViewRef.get();
- if (view != null) {
- view.setVisibility(visibility);
- } else {
- throw new IllegalStateException("setVisibility called on un-referenced view");
- }
- } else {
- super.setVisibility(visibility);
- if (visibility == VISIBLE || visibility == INVISIBLE) {
- inflate();
- }
- }
- }
-
-
-
-
-
-
-
-
- public View inflate() {
- final ViewParent viewParent = getParent();
-
- if (viewParent != null && viewParent instanceof ViewGroup) {
- if (mLayoutResource != 0) {
- final ViewGroup parent = (ViewGroup) viewParent;
- final LayoutInflater factory = LayoutInflater.from(mContext);
- final View view = factory.inflate(mLayoutResource, parent,
- false);
-
- if (mInflatedId != NO_ID) {
- view.setId(mInflatedId);
- }
-
- final int index = parent.indexOfChild(this);
- parent.removeViewInLayout(this);
-
- final ViewGroup.LayoutParams layoutParams = getLayoutParams();
- if (layoutParams != null) {
- parent.addView(view, index, layoutParams);
- } else {
- parent.addView(view, index);
- }
-
- mInflatedViewRef = new WeakReference<View>(view);
-
- if (mInflateListener != null) {
- mInflateListener.onInflate(this, view);
- }
-
- return view;
- } else {
- throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
- }
- } else {
- throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
- }
- }</span>
可以看到,其实最终加载目标布局的还是inflate()函数,在该函数中将加载目标布局,获取到根元素后,如果mInflatedId不为NO_ID则把mInflatedId设置为根元素的id,这也是为什么我们在获取评论ListView时会使用findViewById(R.id.stub_comm_lv)来获取,其中的stub_comm_lv就是ViewStub的inflatedId。当然如果你没有设置inflatedId的话还是可以通过评论列表的id来获取的,例如findViewById(R.id.my_comm_lv)。然后就是ViewStub从parent中移除、把目标布局的根元素添加到parent中。最后会把目标布局的根元素返回,因此我们在调用inflate()函数时可以直接获得根元素,省掉了findViewById的过程。
还有一种方式加载目标布局的就是直接调用ViewStub的inflate()方法,示例如下 :
- public class MainActivity extends Activity {
-
-
- ListView commLv2 = null;
-
- public void onCreate(Bundle b){
-
- setContentView(R.layout.main);
-
-
- ViewStub listStub2 = (ViewStub) findViewById(R.id.stub_import) ;
-
- if ( commLv2 == null ) {
-
- commLv2 = (ListView)listStub2.inflate();
- } else {
-
- }
-
- }
-
- }
注意事项
1、判断是否已经加载过, 如果通过setVisibility来加载,那么通过判断可见性即可;如果通过inflate()来加载是不可以通过判断可见性来处理的,而需要使用方式2来进行判断。
2、findViewById的问题,注意ViewStub中是否设置了inflatedId,如果设置了则需要通过inflatedId来查找目标布局的根元素。
Merge
首先我们看官方的说明:
The <merge /> tag helps eliminate redundant view groups in your view hierarchy when including one layout within another. For example, if your main layout is a vertical LinearLayout in which two consecutive views can be re-used in multiple layouts, then the re-usable layout in which you place the two views requires its own root view. However, using another LinearLayout as the root for the re-usable layout would result in a vertical LinearLayout inside a vertical LinearLayout. The nested LinearLayout serves no real purpose other than to slow down your UI performance.
其实就是减少在include布局文件时的层级。<merge>标签是这几个标签中最让我费解的,大家可能想不到,<merge>标签竟然会是一个Activity,里面有一个LinearLayout对象。-
-
-
- public class Merge extends Activity {
- private LinearLayout mLayout;
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- mLayout = new LinearLayout(this);
- mLayout.setOrientation(LinearLayout.VERTICAL);
- LayoutInflater.from(this).inflate(R.layout.merge_tag, mLayout);
-
- setContentView(mLayout);
- }
-
- public ViewGroup getLayout() {
- return mLayout;
- }
- }
使用merge来组织子元素可以减少布局的层级。例如我们在复用一个含有多个子控件的布局时,肯定需要一个ViewGroup来管理,例如这样 :
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
-
- <ImageView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
-
- android:scaleType="center"
- android:src="@drawable/golden_gate" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="20dip"
- android:layout_gravity="center_horizontal|bottom"
-
- android:padding="12dip"
-
- android:background="#AA000000"
- android:textColor="#ffffffff"
-
- android:text="Golden Gate" />
-
- </FrameLayout>
将该布局通过include引入时就会多引入了一个FrameLayout层级,此时结构如下 :
使用merge标签就会消除上图中蓝色的FrameLayout层级。示例如下 :
- <merge xmlns:android="http://schemas.android.com/apk/res/android">
-
- <ImageView
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
-
- android:scaleType="center"
- android:src="@drawable/golden_gate" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="20dip"
- android:layout_gravity="center_horizontal|bottom"
-
- android:padding="12dip"
-
- android:background="#AA000000"
- android:textColor="#ffffffff"
-
- android:text="Golden Gate" />
-
- </merge>
效果图如下 :
那么它是如何实现的呢,我们还是看源码吧。相关的源码也是在LayoutInflater的inflate()函数中。
- 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 {
-
- int type;
- while ((type = parser.next()) != XmlPullParser.START_TAG &&
- type != XmlPullParser.END_DOCUMENT) {
-
- }
-
- if (type != XmlPullParser.START_TAG) {
- throw new InflateException(parser.getPositionDescription()
- + ": No start tag found!");
- }
-
- final String name = parser.getName();
-
-
- 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 {
-
- }
-
- } catch (XmlPullParserException e) {
-
- }
-
- return result;
- }
- }
-
-
- void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
- boolean finishInflate) throws XmlPullParserException, IOException {
-
- final int depth = parser.getDepth();
- int type;
-
- while (((type = parser.next()) != XmlPullParser.END_TAG ||
- parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
-
- if (type != XmlPullParser.START_TAG) {
- continue;
- }
-
- final String name = parser.getName();
-
- if (TAG_REQUEST_FOCUS.equals(name)) {
- parseRequestFocus(parser, parent);
- } else if (TAG_INCLUDE.equals(name)) {
- if (parser.getDepth() == 0) {
- throw new InflateException("<include /> cannot be the root element");
- }
- parseInclude(parser, parent, attrs);
- } else if (TAG_MERGE.equals(name)) {
- throw new InflateException("<merge /> must be the root element");
- } else if (TAG_1995.equals(name)) {
- final View view = new BlinkLayout(mContext, attrs);
- final ViewGroup viewGroup = (ViewGroup) parent;
- final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
- rInflate(parser, view, attrs, true);
- viewGroup.addView(view, params);
- } else {
- final View view = createViewFromTag(parent, name, attrs);
-
- final ViewGroup viewGroup = (ViewGroup) parent;
-
- final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
-
- rInflate(parser, view, attrs, true);
-
- viewGroup.addView(view, params);
- }
- }
-
- if (finishInflate) parent.onFinishInflate();
- }
其实就是如果是merge标签,那么直接将其中的子元素添加到merge标签parent中,这样就保证了不会引入额外的层级。Android布局优化之ViewStub、include、merge使用与源码分析
标签:des android style blog http color io os 使用
原文地址:http://blog.csdn.net/bboyfeiyu/article/details/39373973