Android自定义View

自定义View的原因

当Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。

自定义View的分类

继承View

这种方法主要用于实现一些不规则的效果,采用这种方法需要自己支持wrap_content,并且padding也需要自己处理。这种方法通常需要重写两个方法:onMeasure()、onDraw()。

onMeasure()方法

onMeasure()方法负责对当前View的尺寸进行测量。onMeasure()方法函数原型如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

方法中的两个参数widthMeasureSpec和heightMeasureSpec是两个32位的int型数据,其中前两位表示测量模式,后30位表示具体数值,其具体的值可通过MeasureSpec类分别获取:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

测量模式包含以下三种

测量模式表达含义
UNSPECIFIED父容器不对View有任何限制,要多大给多大
EXACTLY父容器已检测出View所需的精确大小,View的最终大小为SpecSize所指定的值
AT_MOST父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值

EXACTLY模式即对应我们日常开发中的具体大小尺寸与match_parent尺寸,而AT_MOST尺寸对应的是wrap_content。
在重写onMeasure()方法时,要注意对wrap_content进行支持,即为View提供默认值大小,具体方法可如下编写:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMeasureSize(widthMeasureSpec, 200);
        int height = getMeasureSize(heightMeasureSpec, 200);
        setMeasuredDimension(width, height);
    }

    private int getMeasureSize(int measureSpec, int defaultSize) {
        int mSize = 0;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        switch (mode) {
            case MeasureSpec.AT_MOST:
                mSize = Math.min(defaultSize, size);
                break;
            case MeasureSpec.EXACTLY:
                mSize = size;
                break;
            default:
                mSize = defaultSize;
                break;
        }
        return mSize;
    }
onDraw()方法

在这个方法中,我们需要把我们想要的View效果绘制出来,如下绘制一个简单的圆,在这个方法中记得对View的padding值进行处理,否则View的padding值不会生效:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //处理View的padding值
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int r = Math.min(width, height) / 2;
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, r, mPaint);
    }
自定义View属性

当我们需要对我们自定义的View定义属性时,我们需要在资源文件里面添加自定义属性集合,如下:

<declare-styleable name="CircleView">
        <!--声明自定义属性-->
        <attr name="circle_color" format="color"/>
    </declare-styleable>

然后,在自定义View的的构造方法中将属性值取出,代码如下所示:

public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        color = a.getColor(R.styleable.CircleView_circle_color, color);
        a.recycle();
        init();
    }

完成这些工作后,我们就可以在Xml文件的自定义View中定义这些自定义属性了,代码如下:

 <com.example.study.CircleView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp"
        app:circle_color="@color/colorAccent"/>

继承特定的View(如TextView)

这种方法一般是用于扩展某种已有的View的功能,这种方法比较容易实现,不需要自己支持wrap_content和padding等,具体操作应根据业务需求而进行。

自定义ViewGroup派生特殊的Layout

这种方法主要用于实现自定义的布局,即除了常用布局之外,我们重新定义一种新布局,当某种效果看起来像几种View组合在一起的时候,可以采用这种方法。采用这种方法稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,同时处理子元素的测量与布局过程。

onMeasure()方法

这种方法需要重写onMeasure()方法测量子View的大小以及确定ViewGroup本身的大小,示例代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (childCount == 0) {
            //如果没有子View,则设置宽高为0
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            //宽高都为包裹内容,则宽设置为所有子View的宽度之和,高设置为子View中的最大高度
            setMeasuredDimension(getTotalWidth(), getMaxHeight());
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //只有宽度为包裹内容,则设置宽度为所有子View宽度之和,高度为测量高度
            setMeasuredDimension(getTotalWidth(), heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //只有高度为包裹内容,则设置宽度为测量宽度,高度设置为子View的最大高度
            setMeasuredDimension(widthSize, getMaxHeight());
        }
    }

    private int getTotalWidth() {
        int mChildrenWidth = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View v = getChildAt(i);
            mChildrenWidth += v.getMeasuredWidth();
        }
        return mChildrenWidth;
    }

    private int getMaxHeight() {
        int maxHeight = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View v = getChildAt(i);
            if (v.getMeasuredHeight() > maxHeight) {
                maxHeight = v.getMeasuredHeight();
            }
        }
        return maxHeight;
    }

在上述方法中应该进一步的考虑自定义ViewGroup自身的padding值以及子View的margin值。

onLayout()方法

继承ViewGroup类后必须重写onLayout()抽象方法,在这个方法里面对ViewGroup内的子View按照自己的意愿进行摆放,下列代码例子是一个简单的横向排列摆放逻辑:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int childLeft = 0;
        for (int i = 0; i < childCount; i++) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                //如果子view的显示状态不为GONE,则通过layout将其放置在合适的位置
                int childWidth = v.getMeasuredWidth();
                v.layout(childLeft, 0, childLeft + childWidth, v.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

onLayout()方法中也应该考虑到ViewGroup本身的padding值以及子View的margin值。
至此,自定义ViewGroup的基本逻辑就实现完善了,其余需要注意的内容,如滑动冲突、ViewGroup内部内容的滑动可自行完善。

继承特定的ViewGroup(比如LinearLayout)

这种方法也比较常见,当某种效果看起来像几种View组合在一起的时候,可以采用这种方式,采用这种方式不需要自己处理ViewGroup的测量和布局这两个过程。

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值