自定義View
(一)、簡介:
1、重寫一個view一般情況下隻需要重寫onDraw()方法。那麼什麼時候需要重寫onMeasure()、onLayout()、onDraw() 方法呢,這個問題隻要把這幾個方法的功能弄清楚就應該知道怎麼做了。
①、如果需要繪制View的圖像,那麼需要重寫onDraw()方法。(這也是最常用的重寫方式。)
②、如果需要改變view的大小,那麼需要重寫onMeasure()方法。
③、如果需要改變View的(在父控件的)位置,那麼需要重寫onLayout()方法。
④、根據上面三種不同的需要你可以組合出多種重寫方案。
2、按類型劃分,自定義View的實作方式可分為三種:自繪控件、組合控件、以及繼承控件。
3、如何讓自定義的View在界面上顯示出來?隻需要像使用普通的控件一樣來使用自定義View就可以了。
(二)、自繪控件:
1、概念
自繪控件的意思就是,這個View上所展現的内容全部都是自己繪制出來的。
2、自繪控件的步驟:
1、繪制View :
-
- 繪制View主要是onDraw()方法中完成。通過參數Canvas來處理,相關的繪制主要有drawRect、drawLine、drawPath等等。
- Canvas繪制的常用方法:
- drawColor() 填充顔色
- drawLine() 繪制線
- drawLines() 繪制線條
- drawOval() 繪制圓
- drawPath() 繪制路徑
- drawPicture() 繪制圖檔
- drawPoint() 繪制點
- drawPoints() 繪制點
- drawRGB() 填充顔色
- drawRect() 繪制矩形
- drawText() 繪制文本
- drawTextOnPath() 在路徑上繪制文本
2、重新整理View :(重新整理view的方法這裡主要有:)
-
- invalidate(int l,int t,int r,int b)
-
-
- 重新整理局部,四個參數分别為左、上、右、下
- invalidate()
- 整個view重新整理。執行invalidate類的方法将會設定view為無效,最終重新調用onDraw()方法。
- invalidate()是用來重新整理View的,必須是在UI線程中進行工作。在修改某個view的顯示時,調用invalidate()才能看到重新繪制的界面。invalidate()的調用是把之前的舊的view從主UI線程隊列中pop掉。
- invalidate(Rect dirty)
- 重新整理一個矩形區域
-
3、案例核心代碼:
//繪制View
@Override
protected void onDraw(Canvas canvas) {
calendar = Calendar.getInstance();
//1.圓心X軸坐标,2.圓心Y軸坐标,3.半徑,4.畫筆
int radius= width / 2 - 10;
//畫表盤
canvas.drawCircle(width / 2, height / 2, radius, circlePaint);
canvas.drawCircle(width / 2, height / 2, 15, dotPaint);
for (int i = 1; i < 13; i++){
//在旋轉之前儲存畫布狀态
canvas.save();
canvas.rotate(i * 30, width / 2, height / 2);
//1.2表示起點坐标,3.4表示終點坐标,5.畫筆
canvas.drawLine(width / 2, height / 2- radius, width / 2, height / 2- radius + 10, circlePaint);
//畫表盤數字1.要繪制的文本,2.文本x軸坐标,3.文本基線,4.文本畫筆
canvas.drawText(i + "", width / 2, height / 2 - radius + 22, numPaint);
//恢複畫布狀态
canvas.restore();
}
//獲得目前小時
int hour =calendar.get(Calendar.HOUR);
canvas.save();
//旋轉螢幕
canvas.rotate(hour * 30, width / 2, height / 2);
//畫時針
canvas.drawLine(width / 2, height / 2+ 20, width / 2, height / 2 - 90, hourPaint);
canvas.restore();
int minute= calendar.get(Calendar.MINUTE);
canvas.save();
canvas.rotate(minute * 6, width / 2, height / 2);
canvas.drawLine(width / 2, height / 2+ 30, width / 2, height / 2 - 110, minutePaint);
canvas.restore();
int second= calendar.get(Calendar.SECOND);
canvas.save();
canvas.rotate(second * 6, width / 2, height / 2);
canvas.drawLine(width / 2, height / 2+ 40, width / 2, height / 2 - 130, secondPaint);
canvas.restore();
//每隔1秒重繪View,重繪會調用onDraw()方法
postInvalidateDelayed(1000);
}
(三)、組合控件:
1、概念:
組合控件的意思就是,不需要自己去繪制視圖上顯示的内容,而隻是用系統原生的控件就好了,但可以将幾個系統原生的控件組合到一起,這樣建立出的控件就被稱為組合控件。
2、案例
标題欄就是個很常見的組合控件,很多界面的頭部都會放置一個标題欄,标題欄上會有個傳回按鈕和标題,點選按鈕後就可以傳回到上一個界面。那麼下面我們就來嘗試去實作這樣一個标題欄控件。
3、案例核心代碼
public CustomToolBar(final Contextcontext, AttributeSet attrs, int defStyleAttr) {
super(context,attrs, defStyleAttr);
titleTextView = new TextView(context);
leftImg = new ImageView(context);
leftImg.setPadding(12, 12, 12, 12);
rightImg = new ImageView(context);
rightImg.setPadding(12, 12, 12, 12);
TypedArray ta =context.obtainStyledAttributes(attrs, R.styleable.CustomToolBar);
String titleText =ta.getString(R.styleable.CustomToolBar_titleText);
//第二個參數表示預設顔色
int titleTextColor= ta.getColor(R.styleable.CustomToolBar_myTitleTextColor, Color.BLACK);
//已經由sp轉為px
float titleTextSize= ta.getDimension(R.styleable.CustomToolBar_titleTextSize, 12);
//讀取圖檔
Drawable leftDrawable = ta.getDrawable(R.styleable.CustomToolBar_leftImageSrc);
Drawable rightDrawable = ta.getDrawable(R.styleable.CustomToolBar_rightImageSrc);
//回收TypedArray
ta.recycle();
leftImg.setImageDrawable(leftDrawable);
rightImg.setImageDrawable(rightDrawable);
titleTextView.setText(titleText);
titleTextView.setTextSize(titleTextSize);
titleTextView.setTextColor(titleTextColor);
//給控件設定LayoutParams時,該控件的父容器是那個,就選那個的LayoutParams
LayoutParams leftParams = new LayoutParams((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()),
(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()));
//表示該控件和父容器的左邊對齊
leftParams.addRule(ALIGN_PARENT_LEFT, TRUE);
this.addView(leftImg, leftParams);
LayoutParams titleParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
titleParams.addRule(CENTER_IN_PARENT, TRUE);
addView(titleTextView, titleParams);
LayoutParams rightParams = new LayoutParams((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()),
(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()));
rightParams.addRule(ALIGN_PARENT_RIGHT, TRUE);
addView(rightImg, rightParams);
//4.點選ImageView時調用接口中的方法
leftImg.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (imgClickListener!=null) {
imgClickListener.leftImgClick();
}
}
});
rightImg.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (imgClickListener!=null) {
imgClickListener.rightImgClick();
}
}
});
}
(四)、繼承控件:
概念:
繼承控件的意思就是,我們并不需要自己重頭去實作一個控件,隻需要去繼承一個現有的控件,然後在這個控件上增加一些新的功能,就可以形成一個自定義的控件了。這種自定義控件的特點就是不僅能夠按照我們的需求加入相應的功能,還可以保留原生控件的所有功能。
(二)案例
1.功能
繼承EditText實作一個記事本。
2.核心代碼:
public class NotePad extends EditText {
private int lineWidth = 1;
private int lineColor = Color.BLACK;
private int spacing_line = 10;
private int padding = 10;
private Paint paint;
public NotePad(Context context) {
this(context, null);
}
public NotePad(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NotePad(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.NotePad);
lineWidth = (int) ta.getDimension(R.styleable.NotePad_lineWidth,lineWidth);
lineColor = ta.getColor(R.styleable.NotePad_lineColor, lineColor);
padding = (int) ta.getDimension(R.styleable.NotePad_np_padding,padding);
ta.recycle();
setFocusableInTouchMode(true);
//設定光标處于左上角
setGravity(Gravity.TOP);
//設定行間距
setLineSpacing(spacing_line, 1);
setPadding(padding, 10, padding, 10);
paint = new Paint();
paint.setColor(lineColor);
paint.setStrokeWidth(lineWidth);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//獲得目前控件的高度
int height = getHeight();
//獲得每一行的高度
int lineHeight = getLineHeight();
//計算出每頁的行數
int pageCount = height / lineHeight;
for (int i = 0; i < pageCount; i++) {
canvas.drawLine(padding, (i + 1) * lineHeight, getWidth() - padding, (i+ 1) * lineHeight, paint);
}
//獲得目前文本總行數
int lineCount = getLineCount();
int extraCount = lineCount - pageCount;
if (extraCount > 0) {
for (int i = pageCount; i < lineCount; i++) {
canvas.drawLine(padding, (i +1) * lineHeight, getWidth() - padding, (i + 1) * lineHeight, paint);
}
}
}
}
二、View的測量
(一)、View的測量模式
分為以下三種:
1、EXACTLY:表示設定了精确的值,一般當childView設定其寬、高為精确值、match_parent時,它的測量模式為EXACTLY;
2、AT_MOST:表示子布局被限制在一個最大值内,一般當childView設定其寬、高為wrap_content時,其測量模式為AT_MOST;
3、UNSPECIFIED:表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見。
(二)、widthMeasureSpec和heightMeasureSpec
widthMeasureSpec和heightMeasureSpec是一個32位int型資料,其中高2為表示測量模式,低30位表示測量值。通過MeasureSpec類中的靜态方法getMode和getSize我們可以擷取一個控件寬高的測量模式和測量值。用法如下:
//測量控件的寬和高,該方法在onDraw之前調用
//widthMeasureSpec表示一個32位int類型的資料,高2位表示測量模式,低30位表示測量值
//測量模式分為3種:
//1.EXACTLY:精确模式,對應設定寬高時給一個具體值或者match_parent
//2.AT_MOST:對應設定寬高時給一個wrap_content
//3.UNSPECIFIED:子控件無限大
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//擷取寬的測量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//擷取寬的測量值
int widthSize =MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
//如果寬為wrap_content,則給定一個預設值
widthSize = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTWIDTH, getResources().getDisplayMetrics());
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
heightSize = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULTHEIGHT,getResources().getDisplayMetrics());
break;
}
widthSize = heightSize = Math.min(widthSize, heightSize);
//設定測量結果
setMeasuredDimension(widthSize, heightSize);
}
三、自定義ViewGroup
(一)、要點
1.ViewGroup的測量
當ViewGroup的寬高設定為wrap_content時,我們需要在onMeasure方法中計算ViewGroup中所有子控件的高度之和來作為ViewGroup的高度。
2.子View的擺放
ViewGroup在它的onLayout方法中對子View進行擺放。核心方法:
//四個參數分别表示View的左上角和右下角,這裡的位置是相對于父容器的位置
childView.layout(leftChild, topChild,rightChild, bottomChild);
(二)、案例(流式布局)
效果圖:

核心代碼:
public class FlowLayout extends ViewGroup {
//存放容器中所有的View
private List<List<View>> mAllViews = newArrayList<List<View>>();
//存放每一行最高View的高度
private List<Integer> mPerLineMaxHeight = new ArrayList<>();
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
}
//測量控件的寬和高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲得寬高的測量模式和測量值
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//獲得容器中子View的個數
int childCount = getChildCount();
//記錄每一行View的總寬度
int totalLineWidth = 0;
//記錄每一行最高View的高度
int perLineMaxHeight = 0;
//記錄目前ViewGroup的總高度
int totalHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//對子View進行測量
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//獲得子View的測量寬度
int childWidth = childView.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
//獲得子View的測量高度
int childHeight = childView.getMeasuredHeight() + lp.topMargin +lp.bottomMargin;
if (totalLineWidth + childWidth > widthSize) {
//統計總高度
totalHeight +=perLineMaxHeight;
//開啟新的一行
totalLineWidth = childWidth;
perLineMaxHeight = childHeight;
} else {
//記錄每一行的總寬度
totalLineWidth += childWidth;
//比較每一行最高的View
perLineMaxHeight =Math.max(perLineMaxHeight, childHeight);
}
//當該View已是最後一個View時,将該行最大高度添加到totalHeight中
if (i == childCount - 1) {
totalHeight +=perLineMaxHeight;
}
}
//如果高度的測量模式是EXACTLY,則高度用測量值,否則用計算出來的總高度(這時高度的設定為wrap_content)
heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
setMeasuredDimension(widthSize, heightSize);
}
//擺放控件
//1.表示該ViewGroup的大小或者位置是否發生變化
//2.3.4.5.控件的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mPerLineMaxHeight.clear();
//存放每一行的子View
List<View> lineViews = new ArrayList<>();
//記錄每一行已存放View的總寬度
int totalLineWidth = 0;
//記錄每一行最高View的高度
int lineMaxHeight = 0;
//獲得子View的總個數
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin +lp.bottomMargin;
if (totalLineWidth + childWidth > getWidth()) {
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
//開啟新的一行
totalLineWidth = 0;
lineMaxHeight = 0;
lineViews = newArrayList<>();
}
totalLineWidth += childWidth;
lineViews.add(childView);
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
//單獨處理最後一行
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
//表示一個View和父容器左邊的距離
int mLeft = 0;
//表示View和父容器頂部的距離
int mTop = 0;
for (int i = 0; i < mAllViews.size(); i++) {
//獲得每一行的所有View
lineViews = mAllViews.get(i);
lineMaxHeight = mPerLineMaxHeight.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View childView =lineViews.get(j);
MarginLayoutParams lp =(MarginLayoutParams) childView.getLayoutParams();
int leftChild = mLeft +lp.leftMargin;
int topChild = mTop +lp.topMargin;
int rightChild = leftChild+ childView.getMeasuredWidth();
int bottomChild = topChild +childView.getMeasuredHeight();
//四個參數分别表示View的左上角和右下角
childView.layout(leftChild,topChild, rightChild, bottomChild);
mLeft += lp.leftMargin +childView.getMeasuredWidth() + lp.rightMargin;
}
mLeft = 0;
mTop += lineMaxHeight;
}
}
}