自定义View详解
2018-09-17 本文已影响0人
kjy_112233
一、自定义View
(1)View的绘制流程
- onMeasure:测量View的宽高
- onLayout:计算当前View及子View位置
- onDraw:绘制视图
(2)坐标系
- Android坐标系:以屏幕的左上角为原点,原点向右为X轴方向,原点向下为Y轴方向。
- View坐标系:
- 自身坐标系
getTop:获取View自身顶边到父布局顶边的距离
getLeft:获取View自身左边到其父布局左边的距离
getRight:获取View自身右边到其父布局左边的距离
getBottom:获取View自身底部到其父布局的顶边的距离 - MotionEvent获取触摸点坐标
getX:获取点击事件距离View控件左边的距离
getY:获取点击事件距离View控件顶边的距离
getRawX:获取点击事件距离整个屏幕左边的距离
getRawY:获取点击事件距离整个屏幕顶边的距离
- 自身坐标系
- 获取view自身的宽高
width = getRight() - getLeft();
height = getBottom() - getTop();
(3)自定义属性
- 在values/attrs.xml自定义属性名称和取值类型
- 类型:
string:字符串
color:颜色值
demension:尺寸值
integer:整型值
enum:枚举值
reference:参考某一资源ID
float:浮点值
boolean:布尔值
fraction:百分数
flag:位或运算
<declare-styleable name="study_view">
<attr name="text" format="string" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
</declare-styleable>
- 在自定义View的构造方法中通过TypedArray获取自定义属性的值,在onDraw中绘制
public class StudyView extends View {
private Rect rect;
private String text;
private Paint paint;
//在Java代码中new时调用
public StudyView(Context context) {
super(context);
}
//在xml布局文件中使用时自动调用
public StudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
}
private void initAttr(Context context, AttributeSet attrs) {
//获取自定义属性的值
@SuppressLint("CustomViewStyleable")
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.study_view);
text = typedArray.getString(R.styleable.study_view_text);
int textColor = typedArray.getColor(R.styleable.study_view_textColor, Color.BLACK);
float textSize = typedArray.getDimension(R.styleable.study_view_textSize, 100);
typedArray.recycle();//回收typedArray
//设置绘制文字画笔
paint = new Paint();
paint.setColor(textColor);
paint.setTextSize(textSize);
//获得绘制文本的宽和高
rect = new Rect();
assert text != null;
paint.getTextBounds(text, 0, text.length(), rect);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制文字
canvas.drawText(text, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
}
}
(4)通过onMeasure设置View的实际宽高
//MeasureSpec是View的静态内部类。用来描述父控件对子控件尺寸的约束
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//保存测量宽度和测量高度
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
/**
* @return 控件的宽度
*/
private int measureWidth(int widthMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取宽的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
if (widthMode == MeasureSpec.EXACTLY) {//match_parent、具体值
return widthSize;//控件的宽度
} else {//wrap_content
float textWidth = rect.width();//文本的宽度
return (int) (getPaddingLeft() + textWidth + getPaddingRight());//控件的宽度
}
}
/**
* @return 控件的高度
*/
private int measureHeight(int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取高的模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高的尺寸
if (heightMode == MeasureSpec.EXACTLY) {//match_parent、具体值
return heightSize;
} else {//wrap_content
float textHeight = rect.height();//文本的高度
return (int) (getPaddingTop() + textHeight + getPaddingBottom());
}
}
(5)实现View自动换行
- Paint方法
paint.setAntiAlias(true):抗锯齿效果
paint.setColor(paintColor):设置画笔颜色
paint.setStyle(Paint.Style.FILL):设置画笔的风格FILL,FILL_OR_STROKE,STROKE
paint.setTextSize(10):设置画笔字体的大小
paint.setStrokeWidth(20):设置画笔的宽度
paint.setShadowLayer(5, 10, 10, paintColor):设置画笔的阴影效果
paint.setStrikeThruText(true):设置字体是否加删除线
paint.setUnderlineText(true):设置字体是否加下划线
paint.setFakeBoldText(true):设置字体是否加粗
paint.setTextSkewX(10):设置字体倾斜度
paint.setTextScaleX(10):设置字体横向缩放
paint.setLetterSpacing(10):设置字行间距
paint.setShader(linearGradient);//设置画笔的渐变色 - 绘制渐变色
LinearGradient:线型渐变色
RadialGradient:圆形渐变色
BitmapShader:位图型渐变色
ComposeShader:组合渐变色 - Canvas方法
canvas.drawLine:绘制直线
canvas.drawRect:绘制矩形
canvas.drawCircle:绘制圆形
canvas.drawArc:绘制饼状图
canvas.drawText:绘制字符
public class StudyView extends View {
private String text;
private int textSize;
private int textColor;
private Paint paint;
private Rect textBranch;
private int drawTextHeight;
private List<String> textList = new ArrayList<>();
//在Java代码中new时调用
public StudyView(Context context) {
this(context, null);
}
//在xml布局文件中使用时自动调用
public StudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
init();
}
private void initAttr(Context context, AttributeSet attrs) {
//获取自定义属性的值
@SuppressLint("CustomViewStyleable")
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.study_view);
text = typedArray.getString(R.styleable.study_view_text);
textColor = typedArray.getColor(R.styleable.study_view_textColor, Color.BLACK);
textSize = (int) typedArray.getDimension(R.styleable.study_view_textSize, 100);
typedArray.recycle();//回收typedArray
}
private void init() {
//初始化画笔
paint = new Paint();
paint.setColor(textColor);
paint.setTextSize(textSize);
paint.setAntiAlias(true);
paint.setStrokeWidth(1);
//获得绘制文本的宽高
textBranch = new Rect();
paint.getTextBounds(text, 0, text.length(), textBranch);
//计算各线在位置
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//每行文字的绘制高度,建议低于基线的距离 - 建议距基线以上的距离
drawTextHeight = (int) (fontMetrics.descent - fontMetrics.ascent);
}
//MeasureSpec是View的静态内部类。用来描述父控件对子控件尺寸的约束
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置文本自动分行
setBranch(widthMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private void setBranch(int widthMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
if (textList.size() == 0) {
//显示文本最大宽度
float specMaxWidth = widthSize - getPaddingLeft() - getPaddingRight();
//实际显示的行数
int lineNum;
if (textBranch.width() > specMaxWidth) {
//获取带小数的行数
float lineNumF = textBranch.width() * 1.0f / specMaxWidth;
//如果有小数就进1
if ((lineNumF + "").contains(".")) {
lineNum = (int) (lineNumF + 0.5);
} else {
lineNum = (int) lineNumF;
}
//每行展示文字的长度
int lineLength = (int) (text.length() / lineNumF);
for (int i = 0; i < lineNum; i++) {
String lineStr;
//判断是否可以一行显示
if (text.length() < lineLength) {
lineStr = text;
} else {
lineStr = text.substring(0, lineLength);
}
textList.add(lineStr);
//重新赋值text
if (!TextUtils.isEmpty(text)) {
if (text.length() > lineLength) {
text = text.substring(lineLength);
}
} else {
break;
}
}
} else {
textList.add(text);
}
}
}
/**
* @return 控件的宽度
*/
private int measureWidth(int widthMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取宽的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
if (widthMode == MeasureSpec.EXACTLY) {//match_parent、具体值
return widthSize;//控件的宽度
} else {//wrap_content
//获取文本的宽度
float textWidth;
if (textList.size() > 1) {
textWidth = widthSize;
} else {
textWidth = textBranch.width();
}
return (int) (getPaddingLeft() + textWidth + getPaddingRight());//控件的宽度
}
}
/**
* @return 控件的高度
*/
private int measureHeight(int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取高的模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高的尺寸
if (heightMode == MeasureSpec.EXACTLY) {//match_parent、具体值
return heightSize;
} else {//wrap_content
//获取文本的高度
float textHeight = drawTextHeight * textList.size();
return (int) (getPaddingTop() + textHeight + getPaddingBottom());
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < textList.size(); i++) {
paint.getTextBounds(textList.get(i), 0, textList.get(i).length(), textBranch);
canvas.drawText(textList.get(i), getPaddingLeft(), (getPaddingTop() + drawTextHeight * (i + 1)), paint);
}
}
}
(6)实现自定义饼状图
- 实现饼状图并显示百分比
public class SectorView extends View {
private int[] colors = {Color.RED, Color.BLACK, Color.CYAN, Color.MAGENTA, Color.GREEN, Color.BLUE};
private Paint paint;
private RectF rectF;
private float radius;
private float startC;
public SectorView(Context context) {
this(context, null);
}
public SectorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(30);
rectF = new RectF();
}
private float centerX;
private float centerY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取设置总数
//获取圆心坐标
centerX = getPivotX();
centerY = getPivotY();
//获取圆的半径
if (radius == 0)
radius = (float) (Math.min(getWidth(), getHeight()) / 2) / 2;
//设置矩形区域
rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
//绘制每个扇形
for (int color : colors) {
//当前扇形的颜色
paint.setColor(color);
float sweep = 360.0f * (1.0f / colors.length);
//绘制圆弧
canvas.drawArc(rectF, startC, sweep, true, paint);
//计算文字的中心点位置
float textAngle = startC + sweep / 2;
float x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
float y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
paint.setColor(Color.WHITE);
//获取绘制的文字
String text = (int) (sweep / 360.0f * 100) + "%";
//根据角度设置字体显示位置
if (textAngle >= 0 && textAngle < 90)
canvas.drawText(text, x, y + paint.getTextSize(), paint);
else if (textAngle >= 90 && textAngle < 180)
canvas.drawText(text, x - paint.getTextSize(), y + paint.getTextSize(), paint);
else if (textAngle >= 180 && textAngle <= 270)
canvas.drawText(text, x - paint.getTextSize(), y, paint);
else if (textAngle > 270 && textAngle < 360)
canvas.drawText(text, x, y, paint);
//下一个扇形的开始角度
startC += sweep;
}
}
}
- 实现饼状图点击效果
public class SectorView extends View {
private float[] endAngles = new float[6];
private float[] startAngles = new float[6];
private int[] colors = {Color.RED, Color.BLACK, Color.CYAN, Color.MAGENTA, Color.GREEN, Color.BLUE};
private Paint paint;
private RectF rectF;
private int select = -1;
private float radius;
public SectorView(Context context) {
this(context, null);
}
public SectorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(30);
rectF = new RectF();
}
private float centerX;
private float centerY;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//饼状图的起始角度
float startC = 0;
//获取圆心坐标
centerX = getPivotX();
centerY = getPivotY();
//获取圆的半径
if (radius == 0)
radius = (float) (Math.min(getWidth(), getHeight()) / 2) / 2;
//绘制每个扇形
for (int i = 0; i < colors.length; i++) {
//设置矩形区域
rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
//当前扇形的颜色
paint.setColor(colors[i]);
//当前扇形的角度
float sweep = 360.0f * (1.0f / colors.length);
//保存起始角度
startAngles[i] = startC;
//保存结束角度
endAngles[i] = startC + sweep;
//计算文字的中心点位置
float textAngle = startC + sweep / 2;
float x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
float y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
//获取绘制的文字
String text = (int) (sweep / 360.0f * 100) + "%";
//j计算文字的起始点位
if (textAngle >= 0 && textAngle < 90) {
if (select == i) {
int top = (int) (Math.sin(Math.toRadians(textAngle)) * 25);
int left = (int) (Math.cos(Math.toRadians(textAngle)) * 25);
rectF.left += left;
rectF.right += left;
rectF.top += top;
rectF.bottom += top;
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) + left;
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) + top;
} else {
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
}
y += paint.getTextSize();
} else if (textAngle >= 90 && textAngle < 180) {
if (select == i) {
int top = (int) (Math.sin(Math.toRadians(180 - textAngle)) * 25);
int left = (int) (Math.cos(Math.toRadians(180 - textAngle)) * 25);
rectF.left -= left;
rectF.right -= left;
rectF.top += top;
rectF.bottom += top;
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) - left;
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) + top;
} else {
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
}
x -= paint.getTextSize();
y += paint.getTextSize();
} else if (textAngle >= 180 && textAngle <= 270) {
if (select == i) {
int top = (int) (Math.sin(Math.toRadians(270 - textAngle)) * 25);
int left = (int) (Math.cos(Math.toRadians(270 - textAngle)) * 25);
rectF.left -= left;
rectF.right -= left;
rectF.top -= top;
rectF.bottom -= top;
//获取扇形弧度的中心点坐标
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) - left;
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) - top;
} else {
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
}
x -= paint.getTextSize();
} else if (textAngle > 270 && textAngle < 360) {
if (select == i) {
int top = (int) (Math.sin(Math.toRadians(360 - textAngle)) * 25);
int left = (int) (Math.cos(Math.toRadians(360 - textAngle)) * 25);
rectF.left += left;
rectF.right += left;
rectF.top -= top;
rectF.bottom -= top;
//获取扇形弧度的中心点坐标
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) + left;
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) - top;
} else {
x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
}
}
//绘制圆弧
canvas.drawArc(rectF, startC, sweep, true, paint);
paint.setColor(Color.WHITE);
//绘制百分比文字
canvas.drawText(text, x, y, paint);
//下一个扇形的开始角度
startC += sweep;
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//获取点击位置的坐标
float x = event.getX();
float y = event.getY();
//获取点击角度
float angle = 0;
//判断当前点击位置在第几象限
if (x >= centerX && y >= centerY)
angle = (float) (Math.atan((y - centerY) / (x - centerX)) * 180 / Math.PI);
else if (x <= centerX && y >= centerY)
angle = (float) (Math.atan((centerX - x) / (y - centerY)) * 180 / Math.PI + 90);
else if (x <= centerX && y <= centerY)
angle = (float) (Math.atan((centerY - y) / (centerX - x)) * 180 / Math.PI + 180);
else if (x >= centerX && y <= centerY)
angle = (float) (Math.atan((x - centerX) / (centerY - y)) * 180 / Math.PI + 270);
for (int i = 0; i < startAngles.length; i++) {
if (startAngles[i] <= angle && endAngles[i] >= angle) {
select = i;
invalidate();
return true;
}
}
return true;
}
return super.onTouchEvent(event);
}
}