美文网首页
自定义控件基础总结

自定义控件基础总结

作者: apeku | 来源:发表于2019-03-06 15:14 被阅读0次

前言

自定义控件按照使用方式不同可分为自定义View和自定义ViewGroup,自定义View一般用在没有子控件的控件上;自定义Viewgroup用在需要容纳控件的容器

1.自定义View

自定义View一般需要完成两个任务
1.计算出自身宽高并告知系统
2.绘制自身显示图形
由此需要进行的两个步骤
1.继承View,复写onMeasure方法,在onMeasure方法里测量自身宽高并通过setMeasuredDimension方法设置宽高
2.复写onDraw方法,绘制图形
注意:很多情况下我们需要让自定义的控件支持自定义属性,这时我们需要在这两个步骤之前自定义属性,关于自定义属性可以参考这篇文章 自定义属性基础

1.1 onMeasure步骤解读

由于在使用该自定义控件时设置的宽高可以是具体的值,也可以是match_parent和wrap_content,而在通过setMeasuredDimension设置宽高必须是具体的值,所以我们需要在设置之前需要根据自己的需求进行转化和处理,那么怎么转化?

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

在onMeasure的参数中widthMeasureSpec和heightMeasureSpec里面都包含了约束信息,包括3种测量模式和宽高值等,google把此整数32位的前两位用来存放测量模式的信息,后30位用来存放宽高信息,想要从widthMeasureSpec或者heightMeasureSpec取出这些信息也很简单只需要调用下面两个方法就可以了

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);

测量模式有3种,分别是UNSPECIFIED和AT_MOST以及EXACTLY
UNSPECIFIED:父控件没有限制子控件的大小,子控件可以取任何值,adapterView的item的heightmode和ScrollView的子控件的heightmode就是取的这个模式(此模式在开发中很少用到)
AT_MOST:约束值为子控件能取的最大值
EXACTLY:表示子控件应该直接取约束值
一般来说,子View的测量模式由自身宽高设置值和父控件的测量模式共同决定,具体可看ViewGroup的源码。不过通常我们可将3种测量模式和宽高取值match_parent、wrap_content、具体数值这样对应:
具体数值---EXACTLY
match_parent---EXACTLY
wrap_content---AT_MOST
一般来说EXACTLY模式不需要我们做太多处理,直接将宽高设置成约束值即可,我们需要处理的是测量模式为AT_MOST的情况,根据此模式的含义我们也可以猜到,在不超过约束值的情况下,应该刚好能包裹内容就可以了,需要注意的是应该考虑padding对宽高的影响。

1.2 onDraw方法解读

在这个方法里通过canvas.drawXXX等方法画出各种图形,需要注意的是画图的边界问题,要自己处理不能超过padding区域。关于canvas的文章可以看看这篇:
Canvas类的最全面详解

1.3 一个简单的例子

/**
 * Created by apeku on 2019/1/31.
 * 该自定义View主要是根据xml设置的正方形的边长画一个正方形和一个内接正方形的圆
 */

public class MSelfDefineView extends View {

    private int mSquareSideLength;
    public MSelfDefineView(Context context) {
        this(context,null);
    }

    public MSelfDefineView(Context context, AttributeSet attrs) {
        this(context,attrs,0);
    }

    public MSelfDefineView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获得自定义属性,正方形的边长
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MSelfDefineView);
        mSquareSideLength = typedArray.getDimensionPixelSize(R.styleable.MSelfDefineView_lengthofside, 40);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        int width=0;
        int height=0;
        switch (widthMode){

            case MeasureSpec.EXACTLY:
                width=widthSize;
                break;
            case MeasureSpec.AT_MOST:
                //将padding考虑在内
                int paddingLeft=getPaddingLeft();
                int paddingRight=getPaddingRight();
                width=Math.min(widthSize,paddingLeft+paddingRight+mSquareSideLength);
                break;
            default:
                break;
        }
        switch (heightMode){
            case MeasureSpec.EXACTLY:
                height=heightSize;
                break;
            case MeasureSpec.AT_MOST:
                height=Math.min(heightSize,getPaddingTop()+getPaddingBottom()+mSquareSideLength);
                break;
            default:
                break;

        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint mPaint=new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(3);
        mPaint.setColor(Color.parseColor("#000000"));

        int width=getWidth();
        int height=getHeight();
        //考虑padding对正方形中心点的影响
        float centerX=getPaddingLeft()+((float)(width-getPaddingLeft()-getPaddingRight()))/2;
        float centerY=getPaddingTop()+((float)(height-getPaddingBottom()-getPaddingTop()))/2;
        //手动控制画的正方形不能超过padding的区域,在此的逻辑是如果超过除开padding后剩余区域的大小,则把正方形大小设置为剩余区域宽高较小值
        mSquareSideLength=Math.min(mSquareSideLength,Math.min(width-getPaddingLeft()-getPaddingRight(),height-getPaddingBottom()-getPaddingTop()));
        canvas.drawRect(centerX-mSquareSideLength/2,centerY-mSquareSideLength/2,centerX+mSquareSideLength/2,centerY+mSquareSideLength/2,mPaint);
        canvas.drawCircle(centerX,centerY,mSquareSideLength/2,mPaint);
    }
}

xml布局文件如下:

    <com.apeku.defindviewapp1.MSelfDefineView
        android:layout_width="80dp"
        android:layout_height="80dp"

        android:paddingLeft="10dp"
        android:paddingTop="20dp"
        android:paddingRight="30dp"
        android:paddingBottom="20dp"
        app:lengthofside="50dp"
        android:layout_centerInParent="true"
        android:layout_marginLeft="40dp"
        android:background="@color/colorPrimaryDark"/>

运行结果:


Screenshot_2019-02-21-22-10-19.png

2.自定义ViewGroup

自定义ViewGroup需完成如下任务

  1. 测量子View的大小
  2. 测量自己并告知系统自身大小
  3. 排列子View

自定义ViewGroup的步骤

  1. 继承ViewGroup,复写onMeasure方法,测量子View的大小。由于子View完成了测量自身的方法,我们只需告诉子View去测量自己即可
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

参数childMeasureSpec需要我们自己传入,不过ViewGroup已经有了常规化的处理,即前面提到的子View的测量模式由父View的测量模式和自身宽高的设置值有关,相关代码如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

当然Viewgroup也提供了更为方便的测量子View的方法,例如测量单个子View的方法

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec)
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed)

以及测量所有子View的方法

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)

我们可根据情况选用即可。

  1. 测量自身大小,这一步和自定义View差不多,需要注意的点是当测量模式为AT_MOST时,自定义的ViewGroup需要包裹所有的子View,这里面包括了子View的大小,子View的margin值,以及自己的padding值。
  2. 排列子View。这一步需要我们自行计算各个子View的相对于父空间的位置,然后调用子View的layout方法确定子View的位置。
child.layout(left,top,right,bottom)

请注意,这里left,right,top,bottom的数值全都是相对于父控件的;同样的,我们也需要考虑父view的padding和子view自身的margin值对left,top,right,bottom的影响

特别注意:

  • 有时我们需要自己复写generateLayoutParams函数,来为自定义的ViewGroup的子View确定LayoutParams的类型,LayoutParams将决定子View支持哪些layout属性,比如我们想在ViewGroup的子View支持margin属性我们应该这么做
@Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
  • 当我们需要在ViewGroup额外绘制一些图形时,我们一般需要复写dispatchDraw方法而不是onDraw方法,这是因为当ViewGroup无背景时,调用是dispatchDraw方法而不会调用onDraw方法

一个流式布局的例子

public class FlowLayout extends ViewGroup {


    private List<Integer> lineHeights=new ArrayList<Integer>();

    private List<List<View>> lineViews=new ArrayList<List<View>>();

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

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context,attrs,0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int wrapWidth=0;
        int wrapHeight=0;
        int childCount=getChildCount();
        int lineWidth=0;
        int lineHeight=0;
        lineHeights.clear();
        lineViews.clear();
        List<View> lineView=new ArrayList<View>();
        for(int i=0;i<childCount;i++){

            View child=getChildAt(i);
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            MarginLayoutParams lp= (MarginLayoutParams) child.getLayoutParams();
            if(child.getVisibility()==View.GONE){

                if(i==childCount-1){
                    wrapHeight+=lineHeight;
                    wrapWidth=Math.max(wrapWidth,lineWidth);
                    lineHeights.add(lineHeight);
                    lineViews.add(lineView);
                }
                continue;
            }
            if(lineWidth+child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin>widthSize-getPaddingLeft()-getPaddingRight()){

                lineHeights.add(lineHeight);
                wrapHeight+=lineHeight;
                lineViews.add(lineView);
                wrapWidth=Math.max(wrapWidth,lineWidth);
                lineWidth=0;
                lineHeight=0;
                lineView=new ArrayList<View>();
            }
            //宽度和高度考虑了子View的margin值
            lineWidth+=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
            lineHeight=Math.max(lineHeight,child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
            lineView.add(child);
            if(i==childCount-1){
                wrapHeight+=lineHeight;
                wrapWidth=Math.max(wrapWidth,lineWidth);
                lineHeights.add(lineHeight);
                lineViews.add(lineView);
            }
        }
        //wrap_content时宽高加上padding值
        setMeasuredDimension(widthMode==MeasureSpec.EXACTLY?widthSize:wrapWidth+getPaddingLeft()+getPaddingRight(),
                            heightMode==MeasureSpec.EXACTLY?heightSize:wrapHeight+getPaddingTop()+getPaddingBottom());
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int startTop=getPaddingTop();
        int startLeft=getPaddingLeft();
        int childLeft=0;
        int childTop=0;
        int childRight=0;
        int childBottom=0;
        for(int i=0;i<lineViews.size();i++){

            List<View> lineView=lineViews.get(i);
            for(int j=0;j<lineView.size();j++){
                View child=lineView.get(j);
                MarginLayoutParams lp= (MarginLayoutParams) child.getLayoutParams();
                //排列子View时考虑了margin值
                childTop=startTop+lp.topMargin;
                childLeft=startLeft+lp.leftMargin;
                childRight=childLeft+child.getMeasuredWidth();
                childBottom=childTop+child.getMeasuredHeight();
                //调用child.layout方法确定子View的位置
                child.layout(childLeft,childTop,childRight,childBottom);
                startLeft+=child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
            }
            startLeft=getPaddingLeft();
            startTop+=lineHeights.get(i);

        }
    }
    //支持margin
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
}

运行结果如下


Screenshot_2019-03-05-22-03-43.png

参考文章

相关文章

网友评论

      本文标题:自定义控件基础总结

      本文链接:https://www.haomeiwen.com/subject/jzqbjqtx.html