大家在写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 这个项目大家没事可以多看看,
有很多值得学习的地方。