看過《Android 開發(fā)藝術探索》一書的小伙伴都知道,這本書將自定義 View 分成四個類型,分別是:
- 繼承 View 重寫 onDraw 方法
- 繼承 ViewGroup 派生特殊的 Layout
- 繼承已有的 View
- 繼承已有的 ViewGroup
我們本次并不討論具體的類型應該如何實現(xiàn)焚辅,自定義 View 的范圍實在是太寬廣了,只有想不到,沒有做不到。在書中任玉剛大大還提到了自定義 View 應該注意的幾個方面:
- 讓 View 支持 wrap_content
- 讓 View 支持 padding
- 盡量不要在 View 中使用 Handler
- View 中有線程或者動畫豪筝,需要及時停止
- View 有滑動嵌套情形的,需要處理好滑動沖突
這些注意事項都非常有用摘能,即使是一個新手做自定義 View续崖,在本書的指引下,遵循這些標準也能做出可用性較高的自定義 View团搞,比如說我(微笑)严望。不過我在實踐的過程中發(fā)現(xiàn)一個任玉剛大大沒有提到的方面,那就是讓自定義 View 支持 ScrollView莺丑,畢竟 ScrollView 已經(jīng)是個非常常用的布局了著蟹。
首先看一個小例子,我們就拿書中的自定義 View 案例來示范梢莽,也就是單純的畫個圓:
public class CircleView extends View {
private int mColor = getResources().getColor(R.color.colorAccent);
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = array.getColor(R.styleable.CircleView_circle_color,
getResources().getColor(R.color.colorAccent));
array.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
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);
}
}
以上代碼除了顏色我沒有做其他改動,將這個 CircleView 放在縱向的 LinearLayout 中奸披,寬度設置 match_parent
昏名,高度設置 wrap_content
,背景色設置為黑色阵面,為了比較轻局,在其下面放一個 TextView,我們來看看顯示的結果:
還是很正常的样刷,符合我們的預期仑扑。
如果在 Layout 最外層套一個 ScrollView,再來看看:
自定義 View 看不見了置鼻!首先自定義 View 的外層是 LinearLayout镇饮,高度是 match_parent
,從常理來分析箕母,ScrollView 內部的高度無限大的储藐,如果內部的 View 的不做精確設置俱济,可能會導致 View 無限大,所以 ScrollView 內部沒有設置精確高度的 View 都會無法顯示钙勃,除非內部做特殊處理蛛碌。比如下面的 TextView ,設置的高度也是 wrap_content
辖源,但它卻能顯示蔚携,為什么呢?按照我們在 onMeasure 方法中的邏輯克饶,如果自定義 View 是大小不定酝蜒,也就是對應 MeasureSpec.AT_MOST,那么寬高都應該為默認的 200 才對彤路,這樣也不會不顯示秕硝。那么就調試一下看看:
heightMeasureSpec 的值是0,我們知道 MesureSpec 是一個 32 位的 int 值洲尊,高 2 位表示測量模式远豺,低 30 位表示在這種模式下的測量值。顯然這不屬于任何一種 MeasureSpec 已知的模式坞嘀,所以自定義 View 無法獲得測量高度躯护,也就無法顯示了。知道了原因就好辦了丽涩,只需要對 heightMeasureSpec 的值作出識別處理就行了棺滞,比如下面是我的方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//避免在 scrollView 里獲取不到高度
if (heightMeasureSpec == 0) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.AT_MOST);
}
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, widthSpecSize);
}
}
如果無法獲取 heightMeasureSpec,就用 widthSpecSize 重新實例化一個 heightMeasureSpec 出來矢渊,模式設置為 AT_MOST继准,值默認與寬度相同,如果獲取不到高度矮男,就默認設置為與寬度相同移必。因為這里是個圓,那么就有個好處毡鉴,即使寬高設置的都是 match_parent
崔泵,那么真正的高度也只是最大寬大的值,畢竟在 ScrollView 中高度是不會有 match_parent
的效果的猪瞬。當然根據(jù)自己的 View 的用途最好設置適合的默認值憎瘸。
看看效果:
再把高度設置為 match_parent
:
一樣的效果,這種在 ScrollView 中就算是一種比較合理的方式陈瘦,并且完全不會影響自定義 View 在非 ScrollView 布局中的表現(xiàn)幌甘。所以除了任玉剛大大提到的 5 點注意事項,我還想再加一條,那就是 讓自定義 View 支持 ScrollView含潘。
本文最早發(fā)布于 alphagao.com 饲做。