Android自定义View(二)

上一篇:

Android自定义View基础(一)

自定义View: 一般继承自View,SurfaceView或其他的View,不包含子View。

自定义ViewGroup: 一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。

自定义View绘制流程函数调用链(简化版)

1、构造函数

//一般用于new一个View时使用,
    public CircleView(Context context) {
        this(context, null);
    }

    //一般用于xml中定义的view,xml定义的属性会通过attrs传递过来
    public CircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) //API21才出,一般可以不用,直接删除
    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

2、onMeasure()

前面我们在View绘制流程中说了,最终是为了确定mMeasuredWidth、mMeasuredHeight,用于onLayout中的mRight和mBottom。

  • 如果我们把对view的宽高进行改变,一定要手写setMeasuredDimension()。

  • 另外onMeasure(widthMeasureSpec, heightMeasureSpec)中一定要setMeasuredDimension(),否则会报错。

    onMeasure() did not set the measured dimension by calling setMeasuredDimension()
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);     //可以屏蔽,但是一定要setMeasuredDimension()
        int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出宽度的确切数值
        int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式

        int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度的确切数值
        int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
        setMeasuredDimension(widthsize, heightsize);
    }

2.1、MeasureSpec

MeasureSpec是View中的一个子类,代表的是测量规格:32位的int类型,高两位是mode,低30位是size

Mode有三种状态:

  • UNSPECIFIED :父控件没有给子view任何限制,子View可以设置为任意大小。linearLayout/scrollView (随心所欲)
  • EXACTLY:父View有确切的大小,子View (match_parent、dp/px)必须是这个范围内 (固定大小)
  • AT_MOST:父View为子view指定了一个范围,在这个范围内,子View可以尽可能的大,wrap_content(受限制的)
public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT; // 11 0000000000000(后接30个0)

        public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 00 000000000000000(后接30个0)
        public static final int EXACTLY     = 1 << MODE_SHIFT; // 01 000000000000000(后接30个0)
        public static final int AT_MOST     = 2 << MODE_SHIFT; // 10 000000000000000(后接30个0)


        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode; //二进制的加法
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        //得到测量模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        //得到测量尺寸
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }

3、onSizeChanged()

  • 在View.java中有5个地方会调用onSizeChanged()

    public final void setTop(int top)
      public final void setBottom(int bottom)
      public final void setLeft(int left)
      public final void setRight(int right)
      protected boolean setFrame(int left, int top, int right, int bottom) //是在view#layout(l,t,r,b)中回调的

View#onSizeChanged()

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }

4、onLayout()

自定义View中不用到,因为view没有子View。

在自定义ViewGroup中,onLayout()一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。

child.layout(l, t, r, b);

l, t, r, b是相对父View的距离。

5、onDraw()

onDraw是实际绘制的部分,使用的是Canvas绘图。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 1; i <= 5; i++) {
            rect.set(margin + (i - 1) * (width + margin), margin,
                    margin + (i - 1) * (width + margin) + width, margin + width);
            canvas.drawRect(rect, paint);
            canvas.drawPoint(margin + width / 2 + (i - 1) * (width + margin), margin + width / 2, paint);
        }
    }

这画的是个什么东西呢?先看下效果图:5个框框,每个框框中间画个点。

鬼知道我什么要画个这个,还命名CircleView,也许一开始我是想画个圆的。

自行在xml中引用一下:(根据自己的包名进行修改)

<com.example.administrator.myapplication.draw.CircleView //根据自己的包名进行修改
        android:layout_width="match_parent"
        android:layout_height="100dp" />

把完整代码贴一下:

public class CircleView extends View {

    private Paint paint = new Paint();
    private int width = 100;
    private int margin = 40;
    private Rect rect;


    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.STROKE);
        rect = new Rect();
        ContextCompat.getColor(context, R.color.colorAccent);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 1; i <= 5; i++) {
            rect.set(margin + (i - 1) * (width + margin), margin,
                    margin + (i - 1) * (width + margin) + width, margin + width);
            canvas.drawRect(rect, paint);

            canvas.drawPoint(margin + width / 2 + (i - 1) * (width + margin), margin + width / 2, paint);
        }
    }
}