自定义View

一、自定义View的分类

1.继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态德显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并padding也需要自己处理。

2.继承ViewGroup派生特殊的Layout

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

3.继承特定的View(比如TextView)

这种方法比较常见,一般用于扩展某种已有的View的功能。这种方法不需要自己支持wrap_content和padding。

4.继承特定的ViewGroup(比如LinearLayout)

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

二、自定义View的实现

接下来只实现继承View和ViewGroup这两种情况

1.继承View重写onDraw方法

这种方式下要注意的是处理wrap_content和padding

public class CircleView extends View {
    private int mColor = Color.RED;
    private Paint mPaint;

    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);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        a.recycle();
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(mColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //指定默认的宽高值
        int mWidth = 200;
        int mHeight = 200;
        
        //处理宽、高或宽和高为wrap_content的情况
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if (widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightMode);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,mHeight);
        }
    }

    @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;
        canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
    }
}

除此之外,我们经常提供自定义的属性,接下来介绍一下自定义属性的使用过程。

第一步,在values目录下创建自定义属性的XML,比如attrs.xml,也可以选择类似于attrs_aaa_bbb.xml这样以attrs_开头的文件名称。当然这个名称不受限制,也可以随便取,但为了阅读性好,还是按一定规则取文件名。文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

在这个XML文件中声明了一个CircleView的自定义属性集合,在这个集合里可以有很多属性,这里只定义了一个格式为color的属性circle_color。除了color还有其他格式,比如reference是指资源id,dimension是指尺寸。

第二步,在View的构造方法中解析自定义属性的值,并做相应处理。

public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //加载自定义属性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        //解析CircleView属性集合中的circle_color这个属性,它的id是R.styleable.CircleView_circle_color,并设置默认值为红色
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        //通过recycle方法来实现资源
        a.recycle();
        init();
    }

第三步,在布局文件中使用自定义属性。要注意的是在使用时要添加schemas声明:

xmlns:app="http://schemas.android.com/apk/res-auto"。使用时用app作为自定义属性的前缀。

2.继承ViewGroup派生特殊的Layout

这种方式一般用于实现一个自定义的布局,除了要处理ViewGroup的测量和布局过程以及子View的测量和布局过程,如果有滑动情况还要处理滑动冲突。这里我们重点了解一下measure和layout的过程,其他不作考虑。

public class HorizontalScrollViewEx extends ViewGroup {
    public HorizontalScrollViewEx(Context context) {
        this(context,null);
    }

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

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

    private void init() {
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        int childCount = getChildCount();
        // ViewGroup要先测量孩子
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        //在测量和计算自己
        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);

        if (childCount == 0){
            //如果没有孩子
            setMeasuredDimension(0,0);
        }else if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
            //这里假设所有的子View的宽都是一致的
            View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth()*childCount;
            measureHeight = childView.getMeasuredHeight();
        }else if (widthMeasureSpec == MeasureSpec.AT_MOST){
            View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth()*childCount;
            measureHeight = heightSpaceSize;
        }else if (heightMeasureSpec == MeasureSpec.AT_MOST){
            View childView = getChildAt(0);
            measureHeight = childView.getMeasuredHeight();
            measureWidth = widthSpaceSize;
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;//以左侧为0为起点
        int childCount = getChildCount();

        for (int i = 0 ;i< childCount;i++){
            View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE){
                int measuredWidth = childView.getMeasuredWidth();
                childView.layout(childLeft,0,childLeft+measuredWidth,childView.getMeasuredHeight());
                childLeft += measuredWidth;
            }
        }
    }
}

上述代码主要是做一个示例,并没有着重处理细节。在onMeasure方法中,我们要先处理子View的测量,然后测量自身,同时还要考虑是否是wrap_content的情况。但是这里并没有处理padding和margin的问题。在onLayout中,主要是遍历子View,通过调用layout方法来给每一个View定位。

 

 

参考《Android开发艺术探索》

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值