之前我們從源碼的角度對View的工作流程進行了分析仑荐,有了這些理論的支撐,我們才能讓自定義View更好的服務于我們的工作挑宠,接下來我們聊聊自定義View中的那些“套路”均驶。如果還不了解View的工作流程,可以先閱讀這篇文章:Android 淺談自定義View(1)竿屹。
根據(jù)自定義View的使用場景和自定義View的繼承關系报强,我們可以將自定義View分四類:
- 1、繼承系統(tǒng)的View類
- 2拱燃、繼承特定的View類(例如TextView秉溉、ProgressBar等)
- 3、繼承系統(tǒng)的ViewGroup類
- 4、繼承特定的ViewGroup類(例如LinearLayout坚嗜、RelativeLayout等)
四種類型的自定義View有什么不同夯膀、各自的特點是什么呢,以及如何選擇選擇一種合適的方式來實現(xiàn)自定義View苍蔬,這些應該是我們關心的點诱建。接下來,我們結合具體的場景逐一的分析下四種類型的自定義View碟绑。
一俺猿、繼承系統(tǒng)的View類
這種類型的自定義View多用來實現(xiàn)一些不規(guī)則的效果,同時不需要包含子View格仲,而且我們無法通過擴展已有的控件來實現(xiàn)押袍,因為是直接繼承系統(tǒng)的View類,所以我們應在onMeasure()方法中對View的尺寸進行重新的測量來支持wrap_content屬性凯肋,否則View使用wrap_content屬性將和使用match_parent屬性是一個效果谊惭,當然這并不是我們愿意看到的,原因在上一篇文章中已經分析過了侮东,同時這種情況下圈盔,如果View使用了padding屬性,我們依然無法看到效果悄雅,所以需要在onDraw()方法中對padding屬性進行支持驱敲,考慮到了這些因素,我們的自定義View才能更加的健壯宽闲。一般情況下众眨,這種類型的自定義View需要在onDraw()方法中通過canvas繪制的方式來實現(xiàn)具體的效果。
來看一個例子容诬,我們在簡單的在onDraw()方法中設置View背景為灰色娩梨,并繪制了一個圓:
public class CircleView extends View {
private Paint mPaint;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width / 2, height / 2);
canvas.drawColor(Color.GRAY);//設置灰色背景
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);//繪制圓形
}
}
在布局文件中這樣使用:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.viewdemo.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp" />
</RelativeLayout>
看下最終的效果:
和我們分析的一樣,由于沒有支持wrap_content和padding屬性览徒,我們的自定義View和match_parent的效果一樣狈定,而且設置的padding屬性無效。接下來繼續(xù)完善:
public class CircleView extends View {
.......省略若干代碼........
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500, 500);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 500);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min((width - getPaddingLeft() - getPaddingRight()) / 2,
(height - getPaddingTop() - getPaddingBottom()) / 2);
canvas.drawColor(Color.GRAY);
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
}
在onMesure()方法中吱殉,如果寬/高的測量模式為MeasureSpec.AT_MOST掸冤,我們通過setMeasuredDimension()重新測量View的尺寸,這樣就解決了使用wrap_content屬相帶來的問題友雳,同時在onDraw()方法中計算半徑時考慮padding屬性稿湿。再看下最終的效果:
此時View的寬/高為500px,同時padding屬性也生效了押赊。其它情況大家可以自行測試哦饺藤。
二包斑、繼承特定的View類
這種類型的自定義View相對第一種要簡單一些,因為我們直接繼承特定的View類涕俗,例如TextView罗丰、ImageView等,這些系統(tǒng)已經對這些View類進行了很好的實現(xiàn)再姑,所以一般情況下我們不需要對wrap_content萌抵、padding屬相進行特別的支持。如果我們要實現(xiàn)的自定義View和系統(tǒng)已有的某個View類似元镀,可以考慮這種方式绍填,我們只需要對其進行擴展即可。和第一種類型類似栖疑,這種自定義View一般也需要在onDraw()方法中通過canvas繪制的方式來實現(xiàn)具體的效果讨永。例如我們要實現(xiàn)一個圓角的TextView就可以采用這種方式:
public class RoundTextView extends TextView {
private Paint mPaint;
public RoundTextView(Context context) {
super(context);
init();
}
public RoundTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
}
//重寫setBackgroundColor()來設置畫筆顏色
@Override
public void setBackgroundColor(int color) {
mPaint.setColor(color);
}
@Override
protected void onDraw(Canvas canvas) {
RectF rect = new RectF(0, 0, getWidth(), getHeight());
canvas.drawRoundRect(rect, 10, 10, mPaint);//繪制圓角矩形作為TextView背景
super.onDraw(canvas);
}
}
在布局文件中的使用方法和系統(tǒng)的TextView一樣,有一點需要注意遇革,如果要設置背景色卿闹,則要通過java代碼:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RoundTextView roundTextView = (RoundTextView) findViewById(R.id.round_tv);
roundTextView.setBackgroundColor(Color.RED);//設置背景為紅色
}
}
最后看一下效果:
簡單的擴展就實現(xiàn)了圓角的效果,不需要額外的drawable背景或者圖片萝快。
三锻霎、繼承系統(tǒng)的ViewGroup類
我們知道系統(tǒng)已經提供了LinearLayout、RelativeLayout這樣的ViewGroup實現(xiàn)類杠巡,但畢竟這些布局控件的都有其特定的使用場景量窘,如果我們需要若干個View按照某種規(guī)則組合在一起雇寇,而系統(tǒng)的布局控件無法實現(xiàn)類似的場景氢拥,我們可以考慮采用這種方式來定義一種新的布局控件。但需要注意的是锨侯,在內容區(qū)域未超過屏幕尺寸的情況下嫩海,我們一般需要在onMeasure()中重新測量ViewGroup尺寸來對wrap_content屬性進行支持,如果內容區(qū)域的大小超過屏幕尺寸囚痴,我們就必須在onMeasure()中重新測量ViewGroup的尺寸叁怪,否則ViewGroup的最大尺寸為屏幕尺寸,導致ViewGroup中的內容顯示不全深滚。同時根據(jù)需要還可以考慮自身的padding屬性以及子View的margin屬性奕谭,這些都會影響我們自定義View最終的測量結果,通常需要在onLayout()方法中確定子View的具體位置痴荐。解析來看一個具體的例子:
public class TestViewGroup extends ViewGroup {
//使ViewGroup支持margin屬性
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
width += childView.getMeasuredWidth() + params.rightMargin + params.leftMargin;
if (i == 0) {
height += childView.getMeasuredHeight() + params.topMargin + params.bottomMargin;
}
}
if (width > getScreenWidth()) {
setMeasuredDimension(width, height);
} else {
setMeasuredDimension((widthSpecMode == MeasureSpec.AT_MOST) ? width : widthSpecSize,
(heightSpecMode == MeasureSpec.AT_MOST) ? height : heightSpecSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
View lastChildView = null;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
left += params.leftMargin;
if (lastChildView != null) {
left += lastChildView.getMeasuredWidth() + ((MarginLayoutParams) lastChildView.getLayoutParams()).rightMargin;
}
int right = left + childView.getMeasuredWidth();
int top = params.topMargin;
int bottom = childView.getMeasuredHeight() + top;
childView.layout(left, top, right, bottom);
lastChildView = childView;
}
}
省略了一些非核心代碼血柳,首先通過重寫generateLayoutParams()方法使ViewGroup支持margin屬性,在onMeasure()中生兆,如果計算出子View的總寬度大于屏幕寬度难捌,則根據(jù)子View尺寸直接重新測量ViewGroup尺寸,否則使用系統(tǒng)默認的測量值,只在ViewGroup布局參數(shù)為wrap_content時使用子View的計算尺寸重新測量ViewGroup尺寸根吁。由于我們實現(xiàn)了一個類似水平滾動的ViewGroup员淫,所以在onLayout()中按照水平從左到右的方式確定View的位置。同時我們考慮了margin屬性击敌,所以子View可以使用margin屬性介返。看一下效果:
四沃斤、繼承特定的ViewGroup類
如果我們的自定View是若干個View組合在一起的效果映皆,同時在系統(tǒng)已有的布局控件中可以找到類似的效果,則可以考慮繼承特定的ViewGroup類轰枝,例如LinearLayout捅彻、RelativeLayout等,比如我們在界面中通常需要頂部title鞍陨,就可以考慮直接繼承LinearLayout來進行封裝步淹,來方便復用。當然通過直接繼承ViewGroup類也可以實現(xiàn)诚撵,但是難度會增加很多缭裆,得不償失。舉個例子吧寿烟,當LinearLayout為垂直方向澈驼,且其中的內容超過屏幕的顯示范圍,則因為LinearLayout的內容區(qū)域無法滾動筛武,我們無法預覽整個LinearLayout內容缝其,有一只解決辦法是通過和ScrollView嵌套。那能不能擴展LinearLayout來實現(xiàn)呢徘六,繼續(xù)往下看:
public class ScrollLinearLayout extends LinearLayout {
private int mLastY;
private Context mContext;
//計算ScrollLinearLayout在屏幕的最大顯示高度
private int showHeight;
public ScrollLinearLayout(Context context) {
this(context, null);
}
public ScrollLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
setClickable(true);//使onTouchEvent()方法可以消費事件
showHeight = getScreenHeight() - getStatusBarHeight() - getActionBarHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//計算ScrollLinearLayout子View高度
int height = 0;
for (int i = 0 ; i < getChildCount(); i++){
height += getChildAt(i).getMeasuredHeight();
}
if (height > showHeight){
setMeasuredDimension(widthMeasureSpec, height);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(y - mLastY) > mTouchSlop) {
intercepted = true;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastY = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if (getHeight() < showHeight){
return true;
}
int scrollY = getScrollY();
int dy = mLastY - y;
if (scrollY + dy <= 0) {
scrollTo(0, 0);
return true;
} else if (scrollY + dy >= getHeight() - showHeight) {
scrollTo(0, getHeight() - showHeight);
return true;
}
scrollBy(0, dy);
break;
case MotionEvent.ACTION_UP:
break;
}
mLastY = y;
return super.onTouchEvent(event);
}
...........省略若干行代碼...........
}
核心代碼很簡單内边,在onMeasure()方法中計算ScrollLinearLayout 的高度,如果子View高度總和大于其在屏幕的最大顯示高度待锈,則重新測量其尺寸漠其。在onTouchEvent()中使ScrollLinearLayout的內容跟隨手指移動,同時進行邊界檢測竿音,防止超出屏幕范圍和屎。最后看下效果:
到這里常見的自定義View類型就介紹完畢了,難免有疏忽的地方春瞬,還請指正柴信,自定義View大致流程上有一定的規(guī)律可循,但更多的方法經驗還需要在實踐中總結快鱼。
有興趣的話颠印,可以下載源碼看看:點我下載哦...