自定义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 voidonDraw(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 (inti = 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、案例核心代码
publicCustomToolBar(finalContextcontext, AttributeSet attrs, int defStyleAttr) {
super(context,attrs, defStyleAttr);
titleTextView= newTextView(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 = newLayoutParams((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 = newLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
titleParams.addRule(CENTER_IN_PARENT,TRUE);
addView(titleTextView, titleParams);
LayoutParams rightParams = newLayoutParams((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(newOnClickListener() {
@Override
public void onClick(View v) {
if (imgClickListener!=null) {
imgClickListener.leftImgClick();
}
}
});
rightImg.setOnClickListener(newOnClickListener() {
@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,将View添加到List<List<View>>集合中***************************************/
//获得子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并显示出来******************************************/
//表示一个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;
}
}
}