大家在寫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。有興趣的同學可以
去www.2cto.com這個地方看一下這個圖片庫。
然後我們來看第一個自定義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 }
復制代碼
然後看看他的布局文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?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
這一般在低端手機上 確實會出現卡幀的現象。其他地方看情況修改。
你4