03 自定义View目录
- 三大类 3.5类:
- 1、继承自原有控件
- 2、组合View
- 2.1 自定义VIew的自定义属性.
- 3、继承View的自绘控件
- 3.1 View
- 3.2 ViewGroup
- 自定义方法中最重要的三个方法:
- onDraw 、 onLayout、 onMeasure
- 绘图、排版子布局、测量自定义View的宽高
- 需要注意的点:{
- 1. inflate(context, layout, this);
- 2. ObtainStyle.
- 3. 外层 内层}
java.util.concurrent.TimeoutException: Cannot get spooler! 【异常】:不能再主线程中使用invalidate方法更新UI,也就是重新绘图。
3.2 组合View的自定义属性
- 代码方面:
- Step1:定义一个attr.xml,在values里面就行,
- declare-styleable是代表一组属性,name的命名就是View类名_Style即可,
- 里面的attr是每一个属性,name就是在xml布局中引用的名字,format是属性类型,有10种属性类型。reference是引用,就是图片引用
- Step1.2 :在布局文件中引用的时候,首先定义一个nameSpace,app,从res后面就是res-auto,自动寻找
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 定义一组属性 -->
<declare-styleable name="AddDecreaseView_Style">
<attr name="middle_text_color" format="color"></attr>
<attr name="left_image_src" format="reference"></attr>
</declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.bwie.juan_mao.selfview02.view.AddDecreaseView
android:id="@+id/adv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:left_image_src="@mipmap/ic_launcher"
app:middle_text_color="#ff0000" />
</LinearLayout>
- Step2:在initView的时候使用context.obtainStyledAttributes,引入一个自定义属性组,返回一个TypedArray,类型数组
- a.getColor(Styleable, int);传入对应的值即可.
- 当然在设置完的时候要释放资源.
- 并且添加设置给View的对应属性.
public class AddDecreaseView extends RelativeLayout {
private ImageView btnDecrease;
private ImageView btnAdd;
private TextView txtNum;
// 1.提供一个接口
public interface OnAdvClickListener {
void add(int num);
void decrease(int num);
}
// 2.提供一个接口对象
private OnAdvClickListener listener;
public void setOnAdvClickListener(OnAdvClickListener listener) {
this.listener = listener;
}
public AddDecreaseView(Context context) {
this(context, null);
}
public AddDecreaseView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AddDecreaseView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 通过obtainStyledAttributes方法返回了一个类型的数组 // 返回值是一个类型的数组
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AddDecreaseView_Style); // 自定义的declare-styleable的名字
// 父层的name+下划线+子层的name
int color = a.getColor(R.styleable.AddDecreaseView_Style_middle_text_color, Color.BLACK); // 设置一个颜色,getColor
int leftImage = a.getResourceId(R.styleable.AddDecreaseView_Style_left_image_src, R.drawable.img_decrease); // 设置图片resourceId,
// 使用结束之后释放资源 // 使用完之后释放重用数据
a.recycle();
// 引入资源文件的时候最后一个参数是this
View.inflate(context, R.layout.item_add_decrease, this);
btnDecrease = findViewById(R.id.btn_decrease);
btnAdd = findViewById(R.id.btn_add);
txtNum = findViewById(R.id.txt_num);
txtNum.setTextColor(color); // 记得把设置好的Color啥的设置给控件
btnDecrease.setImageResource(leftImage);
btnAdd.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
String s = txtNum.getText().toString();
int num = Integer.parseInt(s);
num++;
txtNum.setText(num + "");
// 回调加号的方法
listener.add(num);
}
});
btnDecrease.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
String s = txtNum.getText().toString();
int num = Integer.parseInt(s);
if (num > 0) {
num--;
}
txtNum.setText(num + "");
listener.decrease(num);
}
});
}
}
3.3 画一个View
- 画东西用到的是笔(paint)和画布canvas
- 要重写onDraw方法
- 在canvas中可以绘制的图形方法有:drawCircle(圆形)、drawRect(矩形)、drawLine(画线)、
- drawOval(画椭圆)、drawAct(扇形或弧形)、drawPath(画路径)、drawText(画文本)、drawBitmap(画图片)
- drawColor(画布背景色)
// 绘制图形要重写的方法
// Canvas 画布
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// new 出来一个paint画笔
Paint paint = new Paint();
paint.setColor(Color.RED);
// 抗锯齿 ,true的画 边角是比较圆滑的,不会毛毛糙糙
paint.setAntiAlias(true);
// 设置绘制样式
// 代码之中的单位全部是px像素
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.FILL); // paint中style的三种属性,代表画线/ 还是填充/ 还是填充+加线
paint.setStyle(Paint.Style.FILL_AND_STROKE);
// 画圆形,圆心的x、y,半径,画笔
canvas.drawCircle(100, 100, 100, paint);
// 画矩形,左、上、右、下
canvas.drawRect(0, 200, 200, 400, paint);
// 画线,起点的x、y,终点的x、y,画笔
canvas.drawLine(0, 0, 200, 200, paint);
// 画椭圆,左、上、右、下,画笔
canvas.drawOval(0, 0, 400, 200, paint);
// 重置画笔,重置之后原来设置的属性均不生效,就等于重新new了一个画笔
paint.reset();
paint.setColor(Color.GREEN);
// 画扇形或弧形,左上右下是圆的范围,startAngle开始角度,以圆的右边距为起点,sweepAngle是扫描过的角度,顺时针方向
// useCenter为true是使用圆内部的空间,false的时候去掉了圆心和半径夹角的三角形
canvas.drawArc(0, 0, 200, 200, 180,
180, false, paint);
paint.setColor(Color.GRAY);
canvas.drawRect(0, 100, 200, 300, paint);
canvas.drawRect(0, 0, 200, 200, paint);
paint.setColor(Color.GREEN);
// 画路径
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 100);
path.lineTo(200, 100);
path.addArc(0, 0, 200, 200, 0, 90);
path.lineTo(100, 200);
path.lineTo(0, 200);
path.lineTo(0, 0);
canvas.drawPath(path, paint);
canvas.drawRect(0, 0, 100, 100, paint);
paint.setTextSize(30);
String text = "hello world";
canvas.drawText(text, 0, 100, paint);
// 画文字,第一个参数是要绘制的文字,start的开始的索引,end是结束的索引,包含start,不包含end,x、y轴是开始得=的坐标
// 以文字的左下角为开始的坐标
canvas.drawText("hello world", 0, 3, 0, 100, paint);
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds); // 画一个text的边框,设置画笔的textBounds为Rect
canvas.drawRect(bounds, paint); // 画出来的就是一个文字的边框
bounds.width(); // 能够获取文字的宽 和 高
bounds.height();
canvas.drawColor(Color.RED); // 画布的背景色
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher), 0, 0, paint); // 画一个图
canvas.drawRect(0, 0, 290, 290, paint);
}
3.4 测量自定义View的宽高(搞明白他的测量模式)
- 重写onMeasure方法,
- 代码方面:
- 当布局中宽或者高是match_parent或者固定值的时候,他的Mode模式是EXACTLY,代表精确的.
- 而wrap_content,Mode模式是AT_MOST,代表是控件最大值.
/**
* 测量自定义View的宽高
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@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);
// 设置最终测量的宽高 99%的情况下是这样设置宽高
setMeasuredDimension(widthSize / 2, heightSize / 2);
switch (widthMode) {
case MeasureSpec.EXACTLY:
Log.i(TAG, "onMeasure: " + "当前测量模式是精确值");
break;
case MeasureSpec.AT_MOST:
Log.i(TAG, "onMeasure: " + "当前测量模式是最大值");
break;
// 一般用不到
case MeasureSpec.UNSPECIFIED:
Log.i(TAG, "onMeasure: " + "当前没有什么特殊的");
break;
}
Log.i(TAG, "onMeasure: 测量的大小是" + widthSize);
// Log.i(TAG, "onMeasure: " + MeasureSpec.EXACTLY);
// Log.i(TAG, "onMeasure: " + MeasureSpec.AT_MOST);
// Log.i(TAG, "onMeasure: " + MeasureSpec.UNSPECIFIED);
// EXACTLY: 1073741824--- 精确值
// AT_MOST: -2147483648--- 最大值
// UNSPECIFIED: 0
// match_parent
// widthMode: 1073741824---------EXACTLY
// widthSize: 720
// wrap_content
// widthMode: -2147483648----------AT_MOST
// SelfView: widthSize: 720
// 200dp
// widthMode: 1073741824-----------EXACTLY
// widthSize: 300
Log.d(TAG, "widthMode: " + widthMode);
Log.d(TAG, "widthSize: " + widthSize);
// setMeasuredDimension(200, 1920);
}
3.5 自定义一个定时器怎么做
- 定义一个MyTextView,继承TextView
- 代码方面:
public class MyTextView extends TextView {
public int num = 1;
private Paint paint;
private boolean isStart = true;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
paint.setTextSize(100);
}
private Canvas canvas;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
canvas.drawText(num + "", 300, 300, paint);
}
public void add() {
num++;
// draw(canvas);
// 每次调用invalidate会重新调用onDraw方法,也就是重新绘制
invalidate(); // 运行在主线程UI线程中
// 内部又创建了一个Handler,效率会变低
postInvalidate();
}
public void start() {
isStart = true;
new Thread(new Runnable() {
@Override
public void run() {
while (isStart) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
// 在子线程不能使用invalidate来更新UI
// invalidate();
// 子线程重新调用onDraw方法的时候需要使用postInvalidate
postInvalidate();
}
}
}).start();
}
public void stop() {
isStart = false;
}
}
3.6 重写onLayout,继承ViewGroup
3.6 梯形布局怎么写
- 继承ViewGroup,重写onMeasure、onLayout方法
- 代码方面:
public class LadderView extends ViewGroup {
private static final String TAG = "LadderView";
public LadderView(Context context) {
this(context, null);
}
public LadderView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LadderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// ViewGroup中是可以调用onDraw方法,一般在ViewGroup中不用onDraw
/*@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setAntiAlias(true);
canvas.drawCircle(100, 100, 100, paint);
}*/
/**
* 在继承自ViewGroup时可以调用到onMeasure,并且非常重要
* 测量的是ViewGroup的宽高
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
}
// 继承自ViewGroup必须要重写的方法
// 布局的方法
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
// 获取子控件的数据
int count = getChildCount();
Log.i(TAG, "count: " + count);
measureChildren(0, 0);
/**
* 纵向
*/
/*int sumHeight = 0;
// 循环取出每一个子控件
for (int j = 0; j < count; j++) {
View view = getChildAt(j);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Log.i(TAG, "第" + i + "个view的宽是: " + width);
Log.i(TAG, "第" + i + "个view的高是: " + height);
// 左上右下
view.layout(0, sumHeight, view.getMeasuredWidth(), sumHeight + view.getMeasuredHeight());
sumHeight += view.getMeasuredHeight();
}*/
/**
* 横向
*/
/*int sumWidth = 0;
for (int j = 0; j < count; j++) {
View view = getChildAt(j);
view.layout(sumWidth, 0, sumWidth + getMeasuredWidth(), view.getMeasuredWidth());
sumWidth = sumWidth + view.getMeasuredWidth();
}*/
/**
* 梯形布局
*/
int sumWidth = 0;
int sumHeight = 0;
for (int j = 0; j < count; j++) {
View view = getChildAt(j);
view.layout(sumWidth, sumHeight, sumWidth + view.getMeasuredWidth(), sumHeight + view.getMeasuredHeight());
sumWidth += view.getMeasuredWidth();
sumHeight += view.getMeasuredHeight();
}
}
}