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); } }); } }
- Step1:定义一个attr.xml,在values里面就行,
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); }
- 在canvas中可以绘制的图形方法有:drawCircle(圆形)、drawRect(矩形)、drawLine(画线)、
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(); } } }