View背後不為人知的勾當(一)--自定義控制元件和測量過程
本節首先講講自定義控制元件套路,自定義屬性,測量過程的問題,為了後面的幾節能專注於實現特效,而不受本文這些慣用套路的影響
特效系列的目錄
- 關於Draw
- 關於動畫
- 關於滑動
- 關於Layout
要實現介面特效,首先得掌握:
- View的簡單原理
- 自定義屬性
- measure過程
- 這算是UI特效的周邊技術,任何特效都得考慮這幾個問題
安卓特效的實現,需要藉助以下四個技術點:
- draw
- 動畫
- 滑動
- layout
1 View的原理
- 原理
- 一個Activity包含一個Window物件,也就是PhoneWindow
- 在onResume()方法裡,系統才會把DecoreView新增到PhoneWindow中
- DecoreView將內容顯示在PhoneWindow上,所有View的監聽都通過WM接收,並通過Activity回撥響應的onClickListener
- DecoreView包含一個TitleView,一個ContentView,ContentView的id是android.R.id.content,所以如果你的自定義佈局如SwipeBack需要將ToolBar包含在內,需要考慮把你的佈局新增到DecoreView下,但國內一般不用ToolBar,而是用自己定義的TitleBar,看情況
- ContentView下才是setContentView設定的內容
- requestWindowFeature()必須在setContentView之前呼叫
2 自定義屬性
宣告和使用自定義屬性:
定義attr:在values目錄下,attrs.xml
<declare-styleable name="TopBar">
<attr name="title" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="titleBg" format="reference|color" />
</declare-styleable>
使用attr:在任意佈局檔案裡
<xx.xx.xx.TopBar
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/topbar"
custom:title="title"
custom:titleTextSize="15sp"
custom:titleTextColor="#aaaaaa"
custom:titleBg="@drawable/ic_launcher"
/>
處理自定義屬性
取出xml中設定的屬性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mTitle = ta.getString(R.styleable.TopBar_title);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitleBg = ta.getDrawable(R.styleable.TopBar_titleBg);
ta.recycle();
3 measure過程
需要知道的
- View的測量過程是最複雜的,對於view來說,得考慮內容寬高的計算,對於ViewGroup來說,得考慮具體佈局,padding,margin,weight等
- 可能有時需要重複measure
- MeasureSpec類:32位的int值,高2位是測量模式(最多表示4個模式),低30位是測量的大小,即寬或高的值
- 3種測量模式
- EXACTLY: 精確值
- match_parent和dp,px都相當於指定具體數值
- 特別注意一下match_parent,如果子View是match_parent, 父View是wrap_content,這裡就有衝突了
- 子會被降級為AT_MOST
- AT_MOST
- wrap_content表示控制元件尺寸隨控制元件內容,但不能超過父控制元件給的最大尺寸
- View預設是支援wrap_content的,很合理,一個空View是沒有內容的
- 一般各種具體View的wrap_content會根據以下因素決定:
- 背景drawable的寬高,也就是getInstinctWidth和getInstinctHeight
- padding
- 控制元件內容,如TextView的內容是text,ImageView的內容是src
- minWidth和minHeight值(如果計算出的寬高小於min,則取min)
- UNSPECIFIED
- 不指定測量模式,View想多大就多大
- 這個模式也比較合理,但我也不知道能用在哪兒
- 能用在滾動控制元件?如果控制元件可以橫向滾動,則傳給子View的measureSpec就是不控制你寬高?
- 可能意思就是:還是wrap_content,但不限制你最大值,所以和AT_MOST有區別,這是我猜的
- EXACTLY: 精確值
如何確定測量模式:
- 首先注意:padding屬性影響測量過程,margin屬性影響佈局過程,也影響ViewGroup的測量過程
3.1 View的預設measure行為
View預設情況下,測量過程如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//取minWidth和背景drawable的getMinimumWidth的最大值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size; //minWidth屬性或者背景寬度:這個就可以認為是空白View的內容
break;
case MeasureSpec.AT_MOST: //wrap_content,此時specSize是父控制元件給留的最大值
case MeasureSpec.EXACTLY: //match_parent或者具體值
result = specSize;
break;
}
return result;
}
可以看出,唯一沒處理的就是wrap_content的情況
3.2 一個View的測量模板
一般具體帶內容的View,按下面的套路測量
* 思路也很簡單
* EXACTLY:就指定我多大,那我就多大,一般這時子控制元件就是match_parent,或者具體數值
* AT_MOST:specSize是我的最大值,我本身也帶個內容寬高,二者比較,取小的就是了,我儘量給父控制元件省地方
* 除非父控制元件放不下我了,那我就得按父控制元件尺寸來
* 至於calculateContentWidth和calculateContentHeight,可能會被padding,背景,內容等因素影響
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
mearureWidth(widthMeasureSpec),
mearureHeight(heightMeasureSpec));
}
private int mearureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentWidth();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int mearureHeight(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentHeight();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
///你自己測量寬度:寬度wrap_content時,會走這
private int calculateContentWidth(){
return 200;
}
///你自己測量高度:寬度wrap_content時,會走這
private int calculateContentHeight(){
return 200;
}
自己處理wrap_content時應注意的問題
- 內容如果是圖片,文字等可測量的,則內容本身才有個尺寸,其他的如你自己畫的一個圈,可能就需要你給個固定值或者自定義屬性什麼的了
- padding:內容寬度 + 左右padding就是測量寬度
- max和min相關屬性:也會影響你最終的測量值
padding影響還挺大
- 你draw的時候,寬高是已知的,但是在這裡padding也是要考慮的
3.3 ViewGroup的測量模板
其實ViewGroup的match_parent和固定值也好說,就是EXACTLY,主要還是wrap_content的問題
ViewGroup的onMeasure直接繼承自View,所以沒有實現對子控制元件的處理,
但是測量子控制元件的方法已經提供了,下面給出一個ViewGroup測量的模板
在給出模板之前,需要說明的是,關於這一節的模板和下一節的原理,不要太糾結,
一般情況下,你通過RelativeLayout或者FrameLayout的margin就可以實現任何形式的佈局,
測量過程本身是很複雜的,如果不夠複雜,可能你考慮的情況不夠,參考LinearLayout的測量過程的程式碼,你就能知道為什麼自己一般不要過多的干擾佈局過程了
ViewGroup處理子控制元件的measure先不說
主要是ViewGroup在自己wrap_content時,寬高是隨著子控制元件來的,並且和具體的佈局方式還有關係,所以測量過程中計算ViewGroup自己本身寬高時,可能需要把佈局演算法先過一遍
模板1:不考慮子控制元件margin,也不考慮ViewGroup本身的wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int count = getChildCount();
if (count > 0) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
模板2:考慮margin,和考慮wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
// 臨時ViewGroup大小值
int viewGroupWidth = 0;
int viewGroupHeight = 0;
if (count > 0) {
// 遍歷childView
for (int i = 0; i < count; i++) {
// childView
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//測量childView包含外邊距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 計算父容器的期望值,下面程式碼我們注掉,因為這裡的程式碼根據具體佈局來
//viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
//根據子控制元件計算ViewGroup寬高,取決於具體佈局
viewGroupWidth = calculateParentWidthBasedOnChilren();
viewGroupHeight = calculateParentHeightBasedOnChilren();
// ViewGroup內邊距
viewGroupWidth += getPaddingLeft() + getPaddingRight();
viewGroupHeight += getPaddingTop() + getPaddingBottom();
//和建議最小值進行比較
viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
}
setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
}
///你自己來實現,根據具體佈局和子控制元件們的資訊,算出ViewGroup的wrap_content寬度
private int calculateParentWidthBasedOnChilren(){
int viewGroupWidth = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++){
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//lp.leftMargin
//lp.rightMargin
//child.getMeausredWidth()
...看你怎麼算了
}
return viewGroupWidth;
}
///你自己來實現,根據具體佈局和子控制元件們的資訊,算出ViewGroup的wrap_content高度
private int calculateParentHeightBasedOnChilren(){
int viewGroupHeight = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++){
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//lp.topMargin
//lp.bottomMargin
//child.getMeausredHeight()
...看你怎麼算了
}
return viewGroupHeight;
}
以下程式碼是ViewGroup本身提供的
///遍歷測量所有子控制元件
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//原始碼中是measureChild,注意還有個measureChildWithMargins
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
3.4 測量過程相關原始碼和流程簡陋分析
參考開發藝術
再看ViewGroup的測量過程
思路:
- match_parent和具體值好說
- 如果ViewGroup本身是wrap_content,那就需要根據所有子View來確定自己的寬高
- 如果一個豎直方向的LinearLayout高度是wrap_content,讓你測量,你怎麼測量?
- 對於每一個View,取其高度和margin
- 如果子View是wrap_content或具體數值,還好說
- 如果子View是match_parent,而咱這個LinearLayout又是個wrap_content
- 那就把子View當wrap_content來處理
- 對於每一個View,取其高度和margin
注意:
- ViewGroup提供了measureChildren, measureChildWithMargins, getChildMeasureSpec方法
- 但沒有提供具體的onMeasure實現,因為也沒法提供
- 再注意LinearLayout的vertical模式下,測量過程需要遍歷子控制元件,see how tall everyone is, also remember max width
對於頂級View,即DecoreView,measure過程和普通View有點不同
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text. First try doing the layout at a smaller
// size to see if it will fit.
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
// Didn't fit in that size... try expanding a bit.
baseSize = (baseSize+desiredWindowWidth)/2;
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
+ baseSize);
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(TAG, "Good!");
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
看這段
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
desire就是螢幕寬高
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
上面產生的就是DecoreView的measureSpec
注意一個問題,這個也曾經出現在阿里面試題裡:
父是AT_MOST時,高度其實是根據子View來,
但如果此時子是match_parent,所以子也只能是AT_MOST了
Child wants to be our size, but our size is not fixed. Constrain child to not be bigger than us.
3.5 直接從RelativeLayout或者FrameLayout寫自己的佈局
實戰:BlockLayout
現在需要一個BlockLayout,其子控制元件可以是任何控制元件,但不論其寬度指定成什麼,
每一行都必須只能放3個子控制元件,而高度不論指定成什麼,最後顯示出來的都是個正方形
並且,每一行的中間那個控制元件,必須距左右各10dp的margin
行與行之間,也是10dp的margin
這估計是個最簡單的Layout了
CubeSdk裡的BlockLayout是繼承RelativeLayout,實現比較簡單比較巧妙,
利用了Relativelayout子控制元件的margin來控制,規避了自己measuue和layout,
我們應該學習
package in.srain.cube.views.block;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
public class BlockListView extends RelativeLayout {
public interface OnItemClickListener {
void onItemClick(View v, int position);
}
private static final int INDEX_TAG = 0x04 << 24;
private BlockListAdapter<?> mBlockListAdapter;
private LayoutInflater mLayoutInflater;
private OnItemClickListener mOnItemClickListener;
public BlockListView(Context context) {
this(context, null, 0);
}
public BlockListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BlockListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutInflater = LayoutInflater.from(context);
}
public void setAdapter(BlockListAdapter<?> adapter) {
if (adapter == null) {
throw new IllegalArgumentException("adapter should not be null");
}
mBlockListAdapter = adapter;
adapter.registerView(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (null != mBlockListAdapter) {
mBlockListAdapter.registerView(null);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (null != mBlockListAdapter) {
mBlockListAdapter.registerView(this);
}
}
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
OnClickListener mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
int index = (Integer) v.getTag(INDEX_TAG);
if (null != mOnItemClickListener) {
mOnItemClickListener.onItemClick(v, index);
}
}
};
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void onDataListChange() {
removeAllViews();
int len = mBlockListAdapter.getCount();
int w = mBlockListAdapter.getBlockWidth();
int h = mBlockListAdapter.getBlockHeight();
int columnNum = mBlockListAdapter.getCloumnNum();
int horizontalSpacing = mBlockListAdapter.getHorizontalSpacing();
int verticalSpacing = mBlockListAdapter.getVerticalSpacing();
boolean blockDescendant = getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS;
for (int i = 0; i < len; i++) {
RelativeLayout.LayoutParams lyp = new RelativeLayout.LayoutParams(w, h);
int row = i / columnNum;
int clo = i % columnNum;
int left = 0;
int top = 0;
if (clo > 0) {
left = (horizontalSpacing + w) * clo;
}
if (row > 0) {
top = (verticalSpacing + h) * row;
}
lyp.setMargins(left, top, 0, 0);
View view = mBlockListAdapter.getView(mLayoutInflater, i);
if (!blockDescendant) {
view.setOnClickListener(mOnClickListener);
}
view.setTag(INDEX_TAG, i);
addView(view, lyp);
}
requestLayout();
}
}
4 自定義控制元件的套路
套路:
- 繼承View,通過onDraw實現效果,需要考慮支援wrap_content和padding
- 預設是不支援的wrap_content的(設為wrap,實際還是使用match_parent效果)
- 繼承ViewGroup,需要合理處理measure和layout過程,得考慮padding和margin,否則這倆屬性就失效了,同時處理子元素的測量和佈局過程
- 這種方法要想得到一個規範的Layout是很複雜的,看LinearLayout原始碼就知道
- 對現有控制元件進行擴充套件,比較簡單,不需要考慮wrap_cotent和padding
- 建立複合控制元件,比較簡單,也是最常見的
- 繼承View,通過onDraw實現效果,需要考慮支援wrap_content和padding
幾個重要的回撥和注意點
- onFinishInflate() 從xml載入元件完成
- onSizeChanged() 元件大小改變
- onAttachedToWindow
- onDetachedFromWindow 動畫,handler,執行緒之類的,應該在這裡停止
- View不可見時,也需要停止執行緒和動畫,否則可能造成記憶體洩漏
- 滑動衝突需要考慮
- onMeasure
- onLayout
- onTouchEvent()
其他:
- 知道怎麼自定義attr
- 自定義Drawable也是個路子
5 自定義控制元件入門
在這裡先給出入門的例子,為後面幾節做準備
5.1 CircleView:版本1
效果就是畫個圓,不考慮wrap_content和padding
public class CircleView extends View{
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private void init(){
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width/2, height/2, radius, mPaint);
}
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
>
<org.ayo.ui.sample.view_learn.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000" />
</FrameLayout>
- 效果:
- 預設支援了margin,因為margin由父控制元件處理了
- 但不支援padding,因為沒有在onDraw裡考慮padding
- 也不支援wrap_content,而是當做match_parent處理
- 要解決這個問題,這個例子裡,需要指定個預設寬高,例如都是200px
5.2 CircleView:版本2
新增padding支援
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
}
<org.ayo.ui.sample.view_learn.CircleView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000" />
5.3 CircleView:版本3
新增wrap_content支援
//===========================================
//為了讓控制元件支援wrap_content時,內容尺寸取200px,需要我們重寫measure過程
//===========================================
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
mearureWidth(widthMeasureSpec),
mearureHeight(heightMeasureSpec));
}
private int mearureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentWidth();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int mearureHeight(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentHeight();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int calculateContentWidth(){
return 200;
}
private int calculateContentHeight(){
return 200;
}
<org.ayo.ui.sample.view_learn.CircleView2
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000" />
其實上面這段java程式碼,可以簡化一下,參考開發藝術
///不考慮contentSize和specSize的大小關係,不考慮minWidth和minHeight
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, 200);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, 200);
}
}