Android自定义View的正确流程

平时开发中Android已经为我们提供了很多优秀的UI控件,我们可以直接拿来用就能正确的显示我们想要的效果,那么问题来了我们要的效果系统并不能满足这时候我们就要自定去定义一个View了。所以我们从简单画一个圆来说明View的自定义流程。

在开始之前我们肯定都会疑问
1.如何自定义圆的颜色、大小
2.为什么margin有效果而padding却无效呢。
3.为什么我明明设置的是wrap_content却还是显示成match_parent呢。
接下来我们一个一个来解答这些所谓的疑问。

如何定义圆的颜色和大小
我们需要在values的目录下自定义一个xml文件,假设我们命名为attrs.xml然后我们在这个文件里面为我们的View定义一个declare-styleable为我们的View提供自定义的颜色或者圆圈大小

    <!--自定义属性-->
    <declare-styleable name="Custom_CircleView">
        <attr name="custom_circle_color" format="color"/>
        <attr name="custom_circle_stroke" format="dimension"/>
    </declare-styleable>

在attr中我们可以支持很多的format属性,比如说string、color、dimension、int、boolean等值。

    <com.custom.demo.CircleView
      android:background="@android:color/holo_blue_dark"       app:custom_circle_color="@android:color/holo_red_light"
        app:custom_circle_stroke="5dp"
        android:padding="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

这样我们就能在activity布局文件中直接把我们声明的属性拿来使用了如demo中的app:custom_circle_color和app:custom_circle_stroke。这时候我们会发现app这是什么意思呢?

 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"

我们就能明白app和android这个意思一样就是用来声明自定义属性的前缀,当然这个名字不一定规定写成app可以自定任何定义。

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 读取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Custom_CircleView);
        color = ta.getColor(R.styleable.Custom_CircleView_custom_circle_color, Color.RED);
        strokeWidth = (int) ta.getDimension(R.styleable.Custom_CircleView_custom_circle_stroke, 20);
        ta.recycle();
    }

一切就绪之后再我们自定的CircleView中通过TypedArray读取出来即可了。这样我们就能完全的自定义我们想要的颜色和大小了。

为什么margin有效果而padding却无效
首先为什么margin有效呢,因为margin是有父控件也就是我们一般常见的ViewGroup控制的,因此我们不需要做特殊处理就能生效的。而padding确实我们自己View中控制的所以我们得特殊处理。

        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

我们可以这样子把padding值取出来,然后在使用的地方相对应的把padding考虑进去就好。

为什么我明明设置的是wrap_content却还是显示成match_parent
看到这个问题我们一开始初学Android自定义View的时候肯定蒙圈了。可是这是为什么呢?要理解这个问题我们需要正确的理解MeasureSpec这个东西。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

我们自定义一个View需要重写的一个函数,深受其害所以我们很有必要掌握这个东西才能在以后自定义的道路上一路向前。

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
     }

可以明确一点是这是View种的一个静态类,只是一些简单的参数而已不要想得太过于复杂就好。
UNSPECIFIED:这个一个系统内部的一个测量状态,平时开发中我们几乎不会用到
EXACTLY:父容器已经精确的知道View的测量值。比如说:match_parent或者100dp等具体的大小
AT_MOST:父容器指定了一个可用的大小,但是具体的值需要看这个View的具体实现了。比如说wrap_content

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

我们知道我们只能在这里给View指定大小,那么LayoutParams和MeasureSpec的关系是什么呢。系统会在父容器的约束下将LayoutParams转换成MeasureSpec,所以转成成MeasureSpec是受父容器和自身的LayoutParam共同约束的,一旦MeasureSpec确定值后我们就能在onMeasure中直接拿去测量View的大小了。

        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

通过MeasureSpec中上面两个函数获取模式和大小。
所以我们看下View的默认情况是如何测试View的呢?

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

我们可以看出来就是通过getMode、getSize来读取模式和大小,这时候你再注意一下AT_MOST和EXACTLY的情况下都是等于specSize大小。这就是为什么设置了wrap_content却值还是等于match_parent的主要原因了。

这样我们一个完整的View的代码就完成了,附上代码。

public class CircleView extends View {

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 读取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Custom_CircleView);
        color = ta.getColor(R.styleable.Custom_CircleView_custom_circle_color, Color.RED);
        strokeWidth = (int) ta.getDimension(R.styleable.Custom_CircleView_custom_circle_stroke, 20);
        ta.recycle();

        init();
    }

    private int color;
    private int strokeWidth = 20;
    private Paint mPaint;

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(strokeWidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 把padding都考虑进去..
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;// 取该View最短的一边作为半径..
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 读取SpecMode和SpecSize
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 处理wrap_content情况:默认取值为250px
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSpecSize);
        } else if (heightMeasureSpec == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 300);
        } else {
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }
}

正确的自定义View:需要把能自定义的东西封装成属性供别人自定义,能够支持padding,并且能支持wrap_content的特殊情况。只有这么做才是正确的自定义View。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值