自定义控件基础总结

前言

自定义控件按照使用方式不同可分为自定义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

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容