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();
        }
    }
}