自定义View & 自定义属性

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,自动寻找
      • 
        
        
            
            
                
                
            
        
        
        
        
            
        
        

         

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

 

 

 

 

 

 

你可能感兴趣的:(Android开发)