自定义View开篇,必须跨过的一道坎儿

简介: 自定义View开篇,必须跨过的一道坎儿

 前言

对于许多Android开发者而言,无论工作的方向是什么,自定义View是不得不跨过去的一道坎儿,相信很多伙伴有这样的感受,关于自定义View的知识都看的明白,甚至滚瓜烂熟,但是遇到自定义View实战的时候,还是感觉无从下手,其实想学好自定义View,只有一个字,'练",从今天开始我会持续开始写自定义View系列的文章,一方面巩固自己的基础,另一方面温故而知新。

自定义view的方式及异同

    • 继承自View
    • 继承自ViewGroup
    • 继承自特定现有的View 如TextView
    • 继承自特定现有的ViewGroup 如LinerLayout

    我们先说继承自View和ViewGroup,这两种方式和后面两种相比 更接近于底层,可实现的view更加多样化,但是这两种方式需要处理wrap_content的情况以及padding,否则会出现显示问题。

    继承自ViewGroup的控件比较复杂,需要自行处理onMeasure以及onLayout方法,margin是由父控件决定的,所以继承自View的自定义控件只需要处理padding,但是继承自ViewGroup的控件需要处理自身的padding以及子控件的margin

    image.gif

    如何处理wrap_content问题

    首先我们举个例子 绘制一个简单的圆形。新建CircleView类继承自View,生成三个构造方法,至于每个构造方法有什么区别我在Android自定义View之绘制圆形头像 提到过,初始化画笔等操作都是基础操作,这里就不再赘述了,在onDraw中绘制一个圆,半径为宽高中短边的一半。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height) / 2;
        canvas.drawCircle(width / 2,height/2,radius,paint);
    }

    image.gif

    在布局中引用这个view,设置宽高为300dp,为了方便看效果 我们给这个view设置蓝色背景。运行效果图如下

    image.gif

    接下里我们,设置宽高为wrap_content,运行效果图如下,我们可以看到效果和设置match_parent是一样的

    image.gif

    所以针对wrap_content问题 ,最简单的方式是设置一个默认值,至于默认值设置多少,其实是随意的,那么你可能会问,那为什么TextView设置wrap_content 大小就是正好的呢,我们看一下TextView的源码:

    if (widthMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;
    } else {
        if (mLayout != null && mEllipsize == null) {
            des = desired(mLayout);
        }
        if (des < 0) {
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if (boring != null) {
                mBoring = boring;
            }
        } else {
            fromexisting = true;
        }
        if (boring == null || boring == UNKNOWN_BORING) {
            if (des < 0) {
                des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                        mTransformed.length(), mTextPaint, mTextDir, widthLimit));
            }
            width = des;
        } else {
            width = boring.width;
        }
        final Drawables dr = mDrawables;
        if (dr != null) {
            width = Math.max(width, dr.mDrawableWidthTop);
            width = Math.max(width, dr.mDrawableWidthBottom);
        }
        if (mHint != null) {
            int hintDes = -1;
            int hintWidth;
            if (mHintLayout != null && mEllipsize == null) {
                hintDes = desired(mHintLayout);
            }
            if (hintDes < 0) {
                hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                if (hintBoring != null) {
                    mHintBoring = hintBoring;
                }
            }
            if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                if (hintDes < 0) {
                    hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
                            mHint.length(), mTextPaint, mTextDir, widthLimit));
                }
                hintWidth = hintDes;
            } else {
                hintWidth = hintBoring.width;
            }
            if (hintWidth > width) {
                width = hintWidth;
            }
        }
        width += getCompoundPaddingLeft() + getCompoundPaddingRight();
        if (mMaxWidthMode == EMS) {
            width = Math.min(width, mMaxWidth * getLineHeight());
        } else {
            width = Math.min(width, mMaxWidth);
        }
        if (mMinWidthMode == EMS) {
            width = Math.max(width, mMinWidth * getLineHeight());
        } else {
            width = Math.max(width, mMinWidth);
        }
        // Check against our minimum width
        width = Math.max(width, getSuggestedMinimumWidth());
        if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);
        }
    }

    image.gif

    我们可以看到 ,当设置的大小是具体值的时候 宽度直接等于MeasureSpec获取的,如果测量模式是AT_MOST (设置wrap_content),就通过测量字体所占的宽度,最终取和系统测量的最小值。

    对于上面的例子,我们直接设置为默认值为200即可,代码如下所示:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(200,200);
        }else if (widthMode == MeasureSpec.AT_MOST){
          setMeasuredDimension(200,height);
        }else if (heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(width,200);
        }
    }

    image.gif

    运行结果如下图所示,这样我们自定义的View就完成了对wrap_content的处理。

    image.gif

    如何支持padding

    首先我们将布局文件设置为match_parent,设置上下左右边距为50dp

    <com.support.hlq.layout.CircleView
        android:padding="50dp"
        android:background="@color/colorPrimaryDark"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    image.gif

    不做特殊处理时,运行结果如下:

    image.gif

    我们可以看到设置的padding并没有生效,我们需要在onDraw中绘制的时候考虑边距即可,代码如下

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth() - getPaddingLeft() - getPaddingRight();
        int height = getHeight() - getPaddingTop() - getPaddingBottom();
        int radius = Math.min(width,height) / 2;
        canvas.drawCircle(width / 2 + getPaddingLeft() ,height/2 + getPaddingTop(),radius,paint);
    }

    image.gif

    运行效果如下图所示

    image.gif

    从上图中可以看出padding生效了,对于自定义View继承Viewgroup时,上述两个问题的处理方式是一样的,只不过因为margin是由父控件决定的,所以测量的时候还要考虑子控件的margin,详细问题会在后面实例讲解。考虑到上面几个问题之后,一个自定义View就比较合格了,不过还缺点什么,比如 如何通过xml设置属性呢?

    如何通过xml文件给自定义设置属性

    上面例子中,绘制了一个红色的圆形,我们来通过xml属性配置绘制图形的颜色 以及 画笔的style吧

    首先新建cycle_attr.xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="cycleView">
            <attr name="viewColor" format="reference" />
            <attr name="paintStyle" >
                <enum name="fill" value="0" />
                <enum name="stroke" value="1" />
            </attr>
        </declare-styleable>
    </resources>

    image.gif

    使用declare-styleable标签声明名字为cycleView的属性列,viewColor 表示view的颜色,format所对应的是属性值的类型reference表示资源文件 ,比如颜色值、图片等,paintStyle表示画笔的属性,这里使用枚举类,也就是说自定义属性只能是这两个值。0表示填充类型,1表示描边类型。

    在自定义View中 通过TypedArray 获取自定义的属性

    private void init(Context context, AttributeSet attributeSet) {
        TypedArray typedArray = context.obtainStyledAttributes(attributeSet,R.styleable.cycleView);
        //默认为填充类型
        int style = typedArray.getInt(R.styleable.cycleView_paintStyle,0);
        if (style == 1){
            paint.setStyle(Paint.Style.STROKE);
        }else {
            paint.setStyle(Paint.Style.FILL);
        }
        //默认为红色
        int color = typedArray.getColor(R.styleable.cycleView_viewColor,Color.RED);
        paint.setColor(color);
        typedArray.recycle();
    }
    默认设置为红色,类型为填充类型,接下来我们xml中设置自定义的属性,把view颜色改为粉红色,填充类型改为描边

    image.gif

    <com.support.hlq.layout.CircleView
        app:viewColor = "@color/colorAccent"
        app:paintStyle = "stroke"
        android:padding="50dp"
        android:background="@color/colorPrimaryDark"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    image.gif

    运行结果如下图所示

    image.gif

    如此一来,一个比较规范的自定义view就完成了。

    下一篇:自定义View二篇,如何自定义一个规范的ViewGroup


    目录
    相关文章
    |
    6月前
    |
    前端开发 JavaScript API
    如何实现两栏布局?这篇文章告诉你所有的细节!
    欢迎来到前端入门之旅!这个专栏是为那些对Web开发感兴趣、刚刚开始学习前端的读者们打造的。无论你是初学者还是有一些基础的开发者,我们都会在这里为你提供一个系统而又亲切的学习平台。我们以问答形式更新,为大家呈现精选的前端知识点和最佳实践。通过深入浅出的解释概念,并提供实际案例和练习,让你逐步建立起一个扎实的基础。无论是HTML、CSS、JavaScript还是最新的前端框架和工具,我们都将为你提供丰富的内容和实用技巧,帮助你更好地理解并运用前端开发中的各种技术。
    |
    容器
    Pyside6-第十三篇-布局(最后一章废话-理论篇)
    Pyside6-第十三篇-布局(最后一章废话-理论篇)
    511 0
    |
    11天前
    |
    Android开发
    Android面试高频知识点(1) 图解Android事件分发机制
    Android面试高频知识点(1) 图解Android事件分发机制
    |
    20天前
    |
    存储 缓存 网络协议
    5个Android性能优化相关的深度面试题
    本文涵盖五个Android面试题及其解答,包括优化应用启动速度、内存泄漏的检测与解决、UI渲染性能优化、减少内存抖动和内存溢出、优化网络请求性能。每个问题都提供了详细的解答和示例代码。
    20 2
    |
    6月前
    |
    算法
    二分查找算法的细节刨析 --适合有基础的朋友阅读
    二分查找算法的细节刨析 --适合有基础的朋友阅读
    |
    小程序 数据可视化 前端开发
    吐血整理!一文吃透小程序必备Flex布局
    吐血整理!一文吃透小程序必备Flex布局
    269 0
    吐血整理!一文吃透小程序必备Flex布局
    |
    程序员 Android开发
    牛逼!终于有人能把Android事件分发机制讲明白了
    在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
    牛逼!终于有人能把Android事件分发机制讲明白了
    老大爷都能看懂的RecyclerView动画原理之二
    老大爷都能看懂的RecyclerView动画原理之二
    老大爷都能看懂的RecyclerView动画原理之二
    |
    Android开发 容器
    深度好文|Android事件分发机制分析(已重新排版)
    深度好文|Android事件分发机制分析(已重新排版)
    深度好文|Android事件分发机制分析(已重新排版)
    |
    Java 调度 开发工具
    关于Android性能优化的几点建议,2年以上经验必看
    关于Android性能优化的几点建议,2年以上经验必看