Android自定義View的用法總結
阿新 • • 發佈:2019-02-17
1、通過減少View的使用來增加UI的顯示效率
2、構建SDK中沒有的控制元件
原文總結了4種自定義View,分別是Composite View, Custom Composite View, Flat Custom View和Async Custom Views。示例程式碼在https://github.com/lucasr/android-layout-samples,可以直接執行。該工程依賴兩個工程:Picasso 和Smoothie.Picasso
Picasso是一個非同步圖片載入庫,Smoothie提供了非同步載入ListView和GridView資料項的介面,使列表資料的載入更加順滑。
本文只介紹Composite Vew 和 Custom Composite View的方法,這兩種方式足夠我們使用了,剩餘兩種方法需要自定義一套控制檢視的框架,維護代價高,建議只用在app的核心且穩定的UI中,感興趣的讀者可自行研究。
Composite View
此方法是將多個View結合成一個可重用View的最簡單方法,過程如下:
1、自定義控制元件,繼承相應的控制元件。
2、在建構函式中填充一個merge佈局
3、初始化自定義控制元件中的內部View
4、提供重新整理View的介面
下面介紹了一個用法,該View的佈局如下圖所示:
該類繼承自RelativeLayout,實現了TweetPresenter的介面以更新UI。建構函式中初始化內部的Viewpublic class TweetCompositeView extends RelativeLayout implements TweetPresenter { private final ImageView mProfileImage; private final TextView mAuthorText; private final TextView mMessageText; private final ImageView mPostImage; private final EnumMap<Action, ImageView> mActionIcons; public TweetCompositeView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TweetCompositeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LayoutInflater.from(context).inflate(R.layout.tweet_composite_view, this, true); //初始化內部成員變數 mProfileImage = (ImageView) findViewById(R.id.profile_image); mAuthorText = (TextView) findViewById(R.id.author_text); mMessageText = (TextView) findViewById(R.id.message_text); mPostImage = (ImageView) findViewById(R.id.post_image); mActionIcons = new EnumMap(Action.class); for (Action action : Action.values()) { final ImageView icon; switch (action) { case REPLY: icon = (ImageView) findViewById(R.id.reply_action); break; case RETWEET: icon = (ImageView) findViewById(R.id.retweet_action); break; case FAVOURITE: icon = (ImageView) findViewById(R.id.favourite_action); break; default: throw new IllegalArgumentException("Unrecognized tweet action"); } mActionIcons.put(action, icon); } } @Override public boolean shouldDelayChildPressedState() { return false; } //提供更新UI的介面 @Override public void update(Tweet tweet, EnumSet<UpdateFlags> flags) { mAuthorText.setText(tweet.getAuthorName()); mMessageText.setText(tweet.getMessage()); final Context context = getContext(); ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags); final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl()); mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE); if (hasPostImage) { ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags); } } }
<?xml version="1.0" encoding="utf-8"?> <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:layout_toRightOf="@id/profile_image" android:layout_alignTop="@id/profile_image" 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_below="@id/author_text" android:layout_alignLeft="@id/author_text" 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_below="@id/message_text" android:layout_alignLeft="@id/message_text" android:layout_marginTop="@dimen/tweet_content_margin" android:scaleType="centerCrop"/> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@id/post_image" android:layout_alignLeft="@id/message_text" android:layout_marginTop="@dimen/tweet_content_margin" android:orientation="horizontal"> <ImageView android:id="@+id/reply_action" android:layout_width="0dp" android:layout_height="@dimen/tweet_icon_image_size" android:layout_weight="1" android:src="@drawable/tweet_reply" android:scaleType="fitStart"/> <ImageView android:id="@+id/retweet_action" android:layout_width="0dp" android:layout_height="@dimen/tweet_icon_image_size" android:layout_weight="1" android:src="@drawable/tweet_retweet" android:scaleType="fitStart"/> <ImageView android:id="@+id/favourite_action" android:layout_width="0dp" android:layout_height="@dimen/tweet_icon_image_size" android:layout_weight="1" android:src="@drawable/tweet_favourite" android:scaleType="fitStart"/> </LinearLayout> </merge>
這種方法自定義的View用法簡單,維護也方便。但這種方式自定義的View的UI子View較多,對於複雜的View,將影響遍歷效率。開啟手機設定中的顯示佈局邊界選項,效果圖如下所示:
Android某些控制元件如RelativeLayout,LinearLayout等容器控制元件,需要多次遍歷子View來確定自身的屬性,如LinearLayout的weight屬性。如果能針對自己的App自定義子View的計算和定位邏輯,則可以極大的優化UI的遍歷。這種做法便是接下來介紹的Custom Composite View
Custom Composite View
相比Composite View的方法,一個Custom Composite View繼承自一個ViewGroup,並實現了onMeasure和onLayout方法。下面的TweetLayoutView便是一個Custom Composite View.
public class TweetLayoutView extends ViewGroup implements TweetPresenter {
private final ImageView mProfileImage;
private final TextView mAuthorText;
private final TextView mMessageText;
private final ImageView mPostImage;
private final EnumMap<Action, View> mActionIcons;
public TweetLayoutView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TweetLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.tweet_layout_view, this, true);
mProfileImage = (ImageView) findViewById(R.id.profile_image);
mAuthorText = (TextView) findViewById(R.id.author_text);
mMessageText = (TextView) findViewById(R.id.message_text);
mPostImage = (ImageView) findViewById(R.id.post_image);
mActionIcons = new EnumMap(Action.class);
for (Action action : Action.values()) {
final int viewId;
switch (action) {
case REPLY:
viewId = R.id.reply_action;
break;
case RETWEET:
viewId = R.id.retweet_action;
break;
case FAVOURITE:
viewId = R.id.favourite_action;
break;
default:
throw new IllegalArgumentException("Unrecognized tweet action");
}
mActionIcons.put(action, findViewById(viewId));
}
}
private void layoutView(View view, int left, int top, int width, int height) {
MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams();
final int leftWithMargins = left + margins.leftMargin;
final int topWithMargins = top + margins.topMargin;
view.layout(leftWithMargins, topWithMargins,
leftWithMargins + width, topWithMargins + height);
}
private int getWidthWithMargins(View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getWidth() + lp.leftMargin + lp.rightMargin;
}
private int getHeightWithMargins(View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
private int getMeasuredWidthWithMargins(View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
private int getMeasuredHeightWithMargins(View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthUsed = 0;
int heightUsed = 0;
measureChildWithMargins(mProfileImage,
widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
widthUsed += getMeasuredWidthWithMargins(mProfileImage);
measureChildWithMargins(mAuthorText,
widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
heightUsed += getMeasuredHeightWithMargins(mAuthorText);
measureChildWithMargins(mMessageText,
widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
heightUsed += getMeasuredHeightWithMargins(mMessageText);
if (mPostImage.getVisibility() != View.GONE) {
measureChildWithMargins(mPostImage,
widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
heightUsed += getMeasuredHeightWithMargins(mPostImage);
}
int maxIconHeight = 0;
for (Action action : Action.values()) {
final View iconView = mActionIcons.get(action);
measureChildWithMargins(iconView,
widthMeasureSpec, widthUsed,
heightMeasureSpec, heightUsed);
final int height = getMeasuredHeightWithMargins(iconView);
if (height > maxIconHeight) {
maxIconHeight = height;
}
widthUsed += getMeasuredWidthWithMargins(iconView);
}
heightUsed += maxIconHeight;
int heightSize = heightUsed + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
int currentTop = paddingTop;
layoutView(mProfileImage, paddingLeft, currentTop,
mProfileImage.getMeasuredWidth(),
mProfileImage.getMeasuredHeight());
final int contentLeft = getWidthWithMargins(mProfileImage) + paddingLeft;
final int contentWidth = r - l - contentLeft - getPaddingRight();
layoutView(mAuthorText, contentLeft, currentTop,
contentWidth, mAuthorText.getMeasuredHeight());
currentTop += getHeightWithMargins(mAuthorText);
layoutView(mMessageText, contentLeft, currentTop,
contentWidth, mMessageText.getMeasuredHeight());
currentTop += getHeightWithMargins(mMessageText);
if (mPostImage.getVisibility() != View.GONE) {
layoutView(mPostImage, contentLeft, currentTop,
contentWidth, mPostImage.getMeasuredHeight());
currentTop += getHeightWithMargins(mPostImage);
}
final int iconsWidth = contentWidth / mActionIcons.size();
int iconsLeft = contentLeft;
for (Action action : Action.values()) {
final View icon = mActionIcons.get(action);
layoutView(icon, iconsLeft, currentTop,
iconsWidth, icon.getMeasuredHeight());
iconsLeft += iconsWidth;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public void update(Tweet tweet, EnumSet<UpdateFlags> flags) {
mAuthorText.setText(tweet.getAuthorName());
mMessageText.setText(tweet.getMessage());
final Context context = getContext();
ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags);
final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl());
mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE);
if (hasPostImage) {
ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags);
}
}
}
這個類的布局檔案仍然是tweet_composite_view.xml,建構函式中初始化內部的View,與Composite View的不同之處在於,它通過過載onMeasure和onLayout方法來確定內部View的尺寸和位置。基本思路是過程通過ViewGroup’s 的measureChildWithMargins() 方法和背後的 getChildMeasureSpec() 方法計算出了每個子檢視的 MeasureSpec 。這個自定義View的效果圖的佈局層次如下圖所示,和Composite View的層次一樣,但這個View的遍歷開銷要少於前者。
如果想進一步優化關鍵部分的UI,如ListView和GridView,可以考慮把Custom Composite View合成單一的View統一管理,使得到的View的層次如下圖所示:
要達到這個效果,需要參考Flat Custom View的自定義View方式,剛興趣讀者可參考原始碼。