Android 自定义布局 性能问题 初探

大家在写android 代码的时候,基本上都使用过如下几种布局 RelativeLayout,LinearLayout, FrameLayout

但是很多时候 这几种布局 也无法满足我们的使用。于是我们会考虑用自定义布局,使用自定义布局会有几个优点

比如可以减少view的使用啊,让ui显示的更加有效率啊,以及实现一些原生控件无法实现的效果。

 

我们首先去github上 下载一个开源项目 https://github.com/lucasr/android-layout-samples

注意这个项目是基于android studio 结构的。你如果用Eclipse来导入是导入不成功的。

最近github上很多开源项目都开始支持android studio了。所以还是建议大家拥抱下谷歌的新ide。

 

然后这个项目的作者是http://lucasr.org/about/ 就是国外一个很牛逼的android 工程师,我们就以他

的开源项目以及博客 来感受一下 自定义布局的性能。

 

这个项目运行起来以后实际上就是仿照的twitter的一些效果。图片库用的是picasso。有兴趣的同学可以

去http://square.github.io/picasso/  这个地方看一下这个图片库。

然后我们来看第一个自定义ui  TweetCompositeView

 1 /*
 2  * Copyright (C) 2014 Lucas Rocha
 3  *
 4  * Licensed under the Apache License, Version 2.0 (the "License");
 5  * you may not use this file except in compliance with the License.
 6  * You may obtain a copy of the License at
 7  *
 8  *     http://www.apache.org/licenses/LICENSE-2.0
 9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.lucasr.layoutsamples.widget;
18 
19 import android.content.Context;
20 import android.text.TextUtils;
21 import android.util.AttributeSet;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.widget.ImageView;
25 import android.widget.RelativeLayout;
26 import android.widget.TextView;
27 
28 import org.lucasr.layoutsamples.adapter.Tweet;
29 import org.lucasr.layoutsamples.adapter.TweetPresenter;
30 import org.lucasr.layoutsamples.app.R;
31 import org.lucasr.layoutsamples.util.ImageUtils;
32 
33 import java.util.EnumMap;
34 import java.util.EnumSet;
35 
36 public class TweetCompositeView extends RelativeLayout implements TweetPresenter {
37     private final ImageView mProfileImage;
38     private final TextView mAuthorText;
39     private final TextView mMessageText;
40     private final ImageView mPostImage;
41     private final EnumMap<Action, ImageView> mActionIcons;
42 
43     public TweetCompositeView(Context context, AttributeSet attrs) {
44         this(context, attrs, 0);
45     }
46 
47     public TweetCompositeView(Context context, AttributeSet attrs, int defStyleAttr) {
48         super(context, attrs, defStyleAttr);
49 
50         LayoutInflater.from(context).inflate(R.layout.tweet_composite_view, this, true);
51         mProfileImage = (ImageView) findViewById(R.id.profile_image);
52         mAuthorText = (TextView) findViewById(R.id.author_text);
53         mMessageText = (TextView) findViewById(R.id.message_text);
54         mPostImage = (ImageView) findViewById(R.id.post_image);
55 
56         mActionIcons = new EnumMap(Action.class);
57         for (Action action : Action.values()) {
58             final ImageView icon;
59             switch (action) {
60                 case REPLY:
61                     icon = (ImageView) findViewById(R.id.reply_action);
62                     break;
63 
64                 case RETWEET:
65                     icon = (ImageView) findViewById(R.id.retweet_action);
66                     break;
67 
68                 case FAVOURITE:
69                     icon = (ImageView) findViewById(R.id.favourite_action);
70                     break;
71 
72                 default:
73                     throw new IllegalArgumentException("Unrecognized tweet action");
74             }
75 
76             mActionIcons.put(action, icon);
77         }
78     }
79 
80     @Override
81     public boolean shouldDelayChildPressedState() {
82         return false;
83     }
84 
85     @Override
86     public void update(Tweet tweet, EnumSet<UpdateFlags> flags) {
87         mAuthorText.setText(tweet.getAuthorName());
88         mMessageText.setText(tweet.getMessage());
89 
90         final Context context = getContext();
91         ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags);
92 
93         final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl());
94         mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE);
95         if (hasPostImage) {
96             ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags);
97         }
98     }
99 }

 

我们可以看一下这个自定义ui。实际上这个自定义ui非常简单,我们工作中也经常这样使用自定义ui。

他一般就是这么使用的:

1 继承一个layout。当然这个layout可以是相对布局 也可以是流布局 

2 在构造函数里 inflate 我们的布局文件 同时初始化我们的自定义布局的子元素

3 增加一些对应的方法 来更新我们的元素 比如说 update 这个方法 就是来做这个工作的。

 

然后我们来看一下这个布局对应的布局文件

 

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <!--
 3   ~ Copyright (C) 2014 Lucas Rocha
 4   ~
 5   ~ Licensed under the Apache License, Version 2.0 (the "License");
 6   ~ you may not use this file except in compliance with the License.
 7   ~ You may obtain a copy of the License at
 8   ~
 9   ~     http://www.apache.org/licenses/LICENSE-2.0
10   ~
11   ~ Unless required by applicable law or agreed to in writing, software
12   ~ distributed under the License is distributed on an "AS IS" BASIS,
13   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   ~ See the License for the specific language governing permissions and
15   ~ limitations under the License.
16   -->
17 
18 <merge xmlns:android="http://schemas.android.com/apk/res/android"
19     android:layout_width="match_parent"
20     android:layout_height="match_parent">
21 
22     <ImageView
23         android:id="@+id/profile_image"
24         android:layout_width="@dimen/tweet_profile_image_size"
25         android:layout_height="@dimen/tweet_profile_image_size"
26         android:layout_marginRight="@dimen/tweet_content_margin"
27         android:scaleType="centerCrop"/>
28 
29     <TextView
30         android:id="@+id/author_text"
31         android:layout_width="fill_parent"
32         android:layout_height="wrap_content"
33         android:layout_toRightOf="@id/profile_image"
34         android:layout_alignTop="@id/profile_image"
35         android:textColor="@color/tweet_author_text_color"
36         android:textSize="@dimen/tweet_author_text_size"
37         android:singleLine="true"/>
38 
39     <TextView
40         android:id="@+id/message_text"
41         android:layout_width="fill_parent"
42         android:layout_height="wrap_content"
43         android:layout_below="@id/author_text"
44         android:layout_alignLeft="@id/author_text"
45         android:textColor="@color/tweet_message_text_color"
46         android:textSize="@dimen/tweet_message_text_size"/>
47 
48     <ImageView
49         android:id="@+id/post_image"
50         android:layout_width="fill_parent"
51         android:layout_height="@dimen/tweet_post_image_height"
52         android:layout_below="@id/message_text"
53         android:layout_alignLeft="@id/message_text"
54         android:layout_marginTop="@dimen/tweet_content_margin"
55         android:scaleType="centerCrop"/>
56 
57     <LinearLayout android:layout_width="fill_parent"
58         android:layout_height="wrap_content"
59         android:layout_below="@id/post_image"
60         android:layout_alignLeft="@id/message_text"
61         android:layout_marginTop="@dimen/tweet_content_margin"
62         android:orientation="horizontal">
63 
64         <ImageView
65             android:id="@+id/reply_action"
66             android:layout_width="0dp"
67             android:layout_height="@dimen/tweet_icon_image_size"
68             android:layout_weight="1"
69             android:src="@drawable/tweet_reply"
70             android:scaleType="fitStart"/>
71 
72         <ImageView
73             android:id="@+id/retweet_action"
74             android:layout_width="0dp"
75             android:layout_height="@dimen/tweet_icon_image_size"
76             android:layout_weight="1"
77             android:src="@drawable/tweet_retweet"
78             android:scaleType="fitStart"/>
79 
80         <ImageView
81             android:id="@+id/favourite_action"
82             android:layout_width="0dp"
83             android:layout_height="@dimen/tweet_icon_image_size"
84             android:layout_weight="1"
85             android:src="@drawable/tweet_favourite"
86             android:scaleType="fitStart"/>
87 
88     </LinearLayout>
89 
90 </merge>

 

我们可以来看一下这个布局 其中包含了 LinearLayout 这个布局。 我们知道在android里面 linearlayout和relativelayout 是非常复杂的ui

这种viewgroup 会不断的检测子view的大小和布局位置。 所以实际上效率是有损失的。所以我们如果想更近一步的 优化我们的ui效率

我们要尽量避免使用这种高级的viewgroup 

比如我们可以来看看这个view

  1 /*
  2  * Copyright (C) 2014 Lucas Rocha
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *     http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 package org.lucasr.layoutsamples.widget;
 18 
 19 import android.content.Context;
 20 import android.text.TextUtils;
 21 import android.util.AttributeSet;
 22 import android.view.LayoutInflater;
 23 import android.view.View;
 24 import android.view.ViewGroup;
 25 import android.widget.ImageView;
 26 import android.widget.TextView;
 27 
 28 import org.lucasr.layoutsamples.adapter.Tweet;
 29 import org.lucasr.layoutsamples.adapter.TweetPresenter;
 30 import org.lucasr.layoutsamples.app.R;
 31 import org.lucasr.layoutsamples.util.ImageUtils;
 32 
 33 import java.util.EnumMap;
 34 import java.util.EnumSet;
 35 
 36 public class TweetLayoutView extends ViewGroup implements TweetPresenter {
 37     private final ImageView mProfileImage;
 38     private final TextView mAuthorText;
 39     private final TextView mMessageText;
 40     private final ImageView mPostImage;
 41     private final EnumMap<Action, View> mActionIcons;
 42 
 43     public TweetLayoutView(Context context, AttributeSet attrs) {
 44         this(context, attrs, 0);
 45     }
 46 
 47     public TweetLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
 48         super(context, attrs, defStyleAttr);
 49 
 50         LayoutInflater.from(context).inflate(R.layout.tweet_layout_view, this, true);
 51         mProfileImage = (ImageView) findViewById(R.id.profile_image);
 52         mAuthorText = (TextView) findViewById(R.id.author_text);
 53         mMessageText = (TextView) findViewById(R.id.message_text);
 54         mPostImage = (ImageView) findViewById(R.id.post_image);
 55 
 56         mActionIcons = new EnumMap(Action.class);
 57         for (Action action : Action.values()) {
 58             final int viewId;
 59             switch (action) {
 60                 case REPLY:
 61                     viewId = R.id.reply_action;
 62                     break;
 63 
 64                 case RETWEET:
 65                     viewId = R.id.retweet_action;
 66                     break;
 67 
 68                 case FAVOURITE:
 69                     viewId = R.id.favourite_action;
 70                     break;
 71 
 72                 default:
 73                     throw new IllegalArgumentException("Unrecognized tweet action");
 74             }
 75 
 76             mActionIcons.put(action, findViewById(viewId));
 77         }
 78     }
 79 
 80     private void layoutView(View view, int left, int top, int width, int height) {
 81         MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams();
 82         final int leftWithMargins = left + margins.leftMargin;
 83         final int topWithMargins = top + margins.topMargin;
 84 
 85         view.layout(leftWithMargins, topWithMargins,
 86                     leftWithMargins + width, topWithMargins + height);
 87     }
 88 
 89     private int getWidthWithMargins(View child) {
 90         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
 91         return child.getWidth() + lp.leftMargin + lp.rightMargin;
 92     }
 93 
 94     private int getHeightWithMargins(View child) {
 95         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
 96         return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
 97     }
 98 
 99     private int getMeasuredWidthWithMargins(View child) {
100         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
101         return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
102     }
103 
104     private int getMeasuredHeightWithMargins(View child) {
105         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
106         return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
107     }
108 
109     @Override
110     public boolean shouldDelayChildPressedState() {
111         return false;
112     }
113 
114     @Override
115     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
116         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
117 
118         int widthUsed = 0;
119         int heightUsed = 0;
120 
121         measureChildWithMargins(mProfileImage,
122                                 widthMeasureSpec, widthUsed,
123                                 heightMeasureSpec, heightUsed);
124         widthUsed += getMeasuredWidthWithMargins(mProfileImage);
125 
126         measureChildWithMargins(mAuthorText,
127                                 widthMeasureSpec, widthUsed,
128                                 heightMeasureSpec, heightUsed);
129         heightUsed += getMeasuredHeightWithMargins(mAuthorText);
130 
131         measureChildWithMargins(mMessageText,
132                                 widthMeasureSpec, widthUsed,
133                                 heightMeasureSpec, heightUsed);
134         heightUsed += getMeasuredHeightWithMargins(mMessageText);
135 
136         if (mPostImage.getVisibility() != View.GONE) {
137             measureChildWithMargins(mPostImage,
138                                     widthMeasureSpec, widthUsed,
139                                     heightMeasureSpec, heightUsed);
140             heightUsed += getMeasuredHeightWithMargins(mPostImage);
141         }
142 
143         int maxIconHeight = 0;
144         for (Action action : Action.values()) {
145             final View iconView = mActionIcons.get(action);
146             measureChildWithMargins(iconView,
147                                     widthMeasureSpec, widthUsed,
148                                     heightMeasureSpec, heightUsed);
149 
150             final int height = getMeasuredHeightWithMargins(iconView);
151             if (height > maxIconHeight) {
152                 maxIconHeight = height;
153             }
154 
155             widthUsed += getMeasuredWidthWithMargins(iconView);
156         }
157         heightUsed += maxIconHeight;
158 
159         int heightSize = heightUsed + getPaddingTop() + getPaddingBottom();
160         setMeasuredDimension(widthSize, heightSize);
161     }
162 
163     @Override
164     protected void onLayout(boolean changed, int l, int t, int r, int b) {
165         final int paddingLeft = getPaddingLeft();
166         final int paddingTop = getPaddingTop();
167 
168         int currentTop = paddingTop;
169 
170         layoutView(mProfileImage, paddingLeft, currentTop,
171                    mProfileImage.getMeasuredWidth(),
172                    mProfileImage.getMeasuredHeight());
173 
174         final int contentLeft = getWidthWithMargins(mProfileImage) + paddingLeft;
175         final int contentWidth = r - l - contentLeft - getPaddingRight();
176 
177         layoutView(mAuthorText, contentLeft, currentTop,
178                    contentWidth, mAuthorText.getMeasuredHeight());
179         currentTop += getHeightWithMargins(mAuthorText);
180 
181         layoutView(mMessageText, contentLeft, currentTop,
182                 contentWidth, mMessageText.getMeasuredHeight());
183         currentTop += getHeightWithMargins(mMessageText);
184 
185         if (mPostImage.getVisibility() != View.GONE) {
186             layoutView(mPostImage, contentLeft, currentTop,
187                        contentWidth, mPostImage.getMeasuredHeight());
188 
189             currentTop += getHeightWithMargins(mPostImage);
190         }
191 
192         final int iconsWidth = contentWidth / mActionIcons.size();
193         int iconsLeft = contentLeft;
194 
195         for (Action action : Action.values()) {
196             final View icon = mActionIcons.get(action);
197 
198             layoutView(icon, iconsLeft, currentTop,
199                        iconsWidth, icon.getMeasuredHeight());
200             iconsLeft += iconsWidth;
201         }
202     }
203 
204     @Override
205     public LayoutParams generateLayoutParams(AttributeSet attrs) {
206         return new MarginLayoutParams(getContext(), attrs);
207     }
208 
209     @Override
210     protected LayoutParams generateDefaultLayoutParams() {
211         return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
212     }
213 
214     @Override
215     public void update(Tweet tweet, EnumSet<UpdateFlags> flags) {
216         mAuthorText.setText(tweet.getAuthorName());
217         mMessageText.setText(tweet.getMessage());
218 
219         final Context context = getContext();
220         ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags);
221 
222         final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl());
223         mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE);
224         if (hasPostImage) {
225             ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags);
226         }
227     }
228 }

然后看看他的布局文件 

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2014 Lucas Rocha
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/profile_image"
        android:layout_width="@dimen/tweet_profile_image_size"
        android:layout_height="@dimen/tweet_profile_image_size"
        android:layout_marginRight="@dimen/tweet_content_margin"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/author_text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/tweet_author_text_color"
        android:textSize="@dimen/tweet_author_text_size"
        android:singleLine="true"/>

    <TextView
        android:id="@+id/message_text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/tweet_content_margin"
        android:textColor="@color/tweet_message_text_color"
        android:textSize="@dimen/tweet_message_text_size"/>

    <ImageView
        android:id="@+id/post_image"
        android:layout_width="fill_parent"
        android:layout_height="@dimen/tweet_post_image_height"
        android:layout_marginBottom="@dimen/tweet_content_margin"
        android:scaleType="centerCrop"/>

    <ImageView
        android:id="@+id/reply_action"
        android:layout_width="@dimen/tweet_icon_image_size"
        android:layout_height="@dimen/tweet_icon_image_size"
        android:src="@drawable/tweet_reply"
        android:scaleType="fitStart"/>

    <ImageView
        android:id="@+id/retweet_action"
        android:layout_width="@dimen/tweet_icon_image_size"
        android:layout_height="@dimen/tweet_icon_image_size"
        android:src="@drawable/tweet_retweet"
        android:scaleType="fitStart"/>

    <ImageView
        android:id="@+id/favourite_action"
        android:layout_width="@dimen/tweet_icon_image_size"
        android:layout_height="@dimen/tweet_icon_image_size"
        android:src="@drawable/tweet_favourite"
        android:scaleType="fitStart"/>

</merge>

  

看一下我们就会发现,TweetLayoutView 是通过 onMeasure onlayout 自己来决定子布局的大小和位置的

完全跟linearlayout和relativelayout 没有任何关系。这样性能上就有极大的提高。

 

当然我们不可能自己实现 所有的layout对吧,不然的话 我们就都去谷歌了。。。。哈哈。

但是可以有选择的把你app里 ui最复杂的地方 选择性的优化他。提高 ui渲染的效率。

 

最后我们看一下前面这个TweetLayoutView  这个布局实际上还不是最优解。

因为里面有很多系统自带的imageview 和textview。

 

我们可以打开一下设置--开发者选项-显示布局边界 这个功能

这个功能可以把你当前app的 布局边界全部标示出来

我们可以打开android 版的gmail 随便点击个列表。 

可以看一下他们listview里的每个item 布局边界都是在外面。里面没有任何布局边界。

所以可以得知gmail的listview里的 item 是自己重写的一整个view 里面没有使用

任何系统自带的textview 或者是imageview 之类的。

这样就是ui终极进化了。。。。。。

 

当然这么做 工作量很多,而且很多地方需要考虑。比如你自己画文本是简单了,效率是提高了

但是textview 的文本截断呢?你能做么?imageview里的图片缩放呢?你能做么?

 

所以我们在自定义布局的时候 除了考虑ui实现的效率,我们还需要着重考虑实现的难度,和技术上的风险。

个人感觉只需要修改你app最卡顿的地方的布局 即可。尤其是listview viewpager里面的item

这一般在低端手机上 确实会出现卡帧的现象。其他地方看情况修改。

 

最后 https://github.com/lucasr/android-layout-samples 这个项目大家没事可以多看看,

有很多值得学习的地方。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。