资讯专栏INFORMATION COLUMN

Android 自定义View

anRui / 3117人阅读

摘要:它所做的就是对需要的视图进行测量视图大小确定视图的位置与绘制视图。自定义时不需要去管理该方法。自定义时一般都要自定义属性,所以都会在中定义与最后在自定义中通过获取。

前言

Android自定义View的详细步骤是我们每一个Android开发人员都必须掌握的技能,因为在开发中总会遇到自定义View的需求。为了提高自己的技术水平,自己就系统的去研究了一下,在这里写下一点心得,有不足之处希望大家及时指出。

流程

Android中对于布局的请求绘制是在Android framework层开始处理的。绘制是从根节点开始,对布局树进行measuredraw。在RootViewImpl中的performTraversals展开。它所做的就是对需要的视图进行measure(测量视图大小)、layout(确定视图的位置)与draw(绘制视图)。下面的图能很好的展现视图的绘制流程:

当用户调用requestLayout时,只会触发measurelayout,但系统开始调用时还会触发draw

下面来详细介绍这几个流程。

measure

measureView中的final型方法不可以进行重写。它是对视图的大小进行测量计算,但它会回调onMeasure方法,所以我们在自定义View的时候可以重写onMeasure方法来对View进行我们所需要的测量。它有两个参数widthMeasureSpecheightMeasureSpec。其实这两个参数都包含两部分,分别为sizemodesize为测量的大小而mode为视图布局的模式
我们可以通过以下代码分别获取:

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

获取到的mode种类分为以下三种:

MODE EXPLAIN
UNSPECIFiED 父视图不对子视图进行约束,子视图大小可以是任意大小,一般是对ListViewScrollView等进行自定义,一般用不到
EXACTLY 父视图对子视图设定了一个精确的尺寸,子视图不超过该尺寸,一般为精确的值例如200dp或者使用了match_parent
AT_MOST 父视图对子视图指定了一最大的尺寸,确保子视图的所以内容都刚好能在该尺寸中显示出来,一般为wrap_content,这种父视图不能获取子视图的大小,只能由子视图自己去计算尺寸,这也是我们测量要实现的逻辑情况
setMeasuredDimension

通过以上逻辑获取视图的宽高,最后要调用setMeasuredDimension方法将测量好的宽高进行传递出去。其实最终是调用setMeasuredDimensionRaw方法对传过来的值进行属性赋值。调用super.onMeasure()的调用逻辑也是一样的。
下面以自定义一个验证码的View为例,它的onMeasure方法如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY) {
            //直接获取精确的宽度
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //计算出宽度(文本的宽度+padding的大小)
            width = bounds.width() + getPaddingLeft() + getPaddingRight();
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            //直接获取精确的高度
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //计算出高度(文本的高度+padding的大小)
            height = bounds.height() + getPaddingBottom() + getPaddingTop();
        }
        //设置获取的宽高
        setMeasuredDimension(width, height);
    }
可以对自定义Viewlayout_widthlayout_height进行设置不同的属性,达到不同的mode类型,就可以看到不同的效果
measureChildren

如果你是对继承ViewGroup的自定义View那么在进行测量自身的大小时还要测量子视图的大小。一般通过measureChildren(int widthMeasureSpec, int heightMeasureSpec)方法来测量子视图的大小。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

通过上面的源码会发现,它其实是遍历每一个子视图,如果该子视图不是隐藏的就调用measureChild方法,那么来看下measureChild源码:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

会发现它首先调用了getChildMeasureSpec方法来分别获取宽高,最后再调用的就是Viewmeasure方法,而通过前面的分析我们已经知道它做的就是对视图大小的计算。而对于measure中的参数是通过getChildMeasureSpec获取,再来看下其源码:

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);
    }

是不是容易理解了点呢。它做的就是前面所说的根据mode的类型,获取相应的size。根据父视图的mode类型与子视图的LayoutParams类型来决定子视图所属的mode,最后再将获取的sizemode通过MeasureSpec.makeMeasureSpec方法整合返回。最后传递到measure中,这就是前面所说的widthMeasureSpecheightMeasureSpec中包含的两部分的值。整个过程为measureChildren->measureChild->getChildMeasureSpec->measure->onMeasure->setMeasuredDimension,所以通过measureChildren就可以对子视图进行测量计算。

layout

layout也是一样的内部会回调onLayout方法,该方法是用来确定子视图的绘制位置,但这个方法在ViewGroup中是个抽象方法,所以如果要自定义的View是继承ViewGroup的话就必须实现该方法。但如果是继承View的话就不需要了,View中有一个空实现。而对子视图位置的设置是通过Viewlayout方法通过传递计算出来的lefttoprightbottom值,而这些值一般都要借助View的宽高来计算,视图的宽高则可以通过getMeasureWidthgetMeasureHeight方法获取,这两个方法获取的值就是上面onMeasuresetMeasuredDimension传递的值,即子视图测量的宽高。

getWidthgetHeightgetMeasureWidthgetMeasureHeight是不同的,前者是在onLayout之后才能获取到的值,分别为left-righttop-bottom;而后者是在onMeasure之后才能获取到的值。只不过这两种获取的值一般都是相同的,所以要注意调用的时机

下面以定义一个把子视图放置于父视图的四个角的View为例:

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        MarginLayoutParams params;
        
        int cl;
        int ct;
        int cr;
        int cb;
            
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            params = (MarginLayoutParams) child.getLayoutParams();
                
            if (i == 0) {
                //左上角
                cl = params.leftMargin;
                ct = params.topMargin;
            } else if (i == 1) {
                //右上角
                cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth();
                ct = params.topMargin;
            } else if (i == 2) {
                //左下角
                cl = params.leftMargin;
                ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight()
                 - params.topMargin;
            } else {
                //右下角
                cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth();
                ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight()
                 - params.topMargin;
            }
            cr = cl + child.getMeasuredWidth();
            cb = ct + child.getMeasuredHeight();
            //确定子视图在父视图中放置的位置
            child.layout(cl, ct, cr, cb);
        }
    }
至于onMeasure的实现源码我后面会给链接,如果要看效果图的话,我后面也会贴出来,前面的那个验证码的也是一样
draw

draw是由dispatchDraw发动的,dispatchDrawViewGroup中的方法,在View是空实现。自定义View时不需要去管理该方法。而draw方法只在View中存在,ViewGoup做的只是在dispatchDraw中调用drawChild方法,而drawChild中调用的就是Viewdraw方法。那么我们来看下draw的源码:

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
         
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas" layers to prepare for fading
         *      3. Draw view"s content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
          
        // Step 1, draw the background, if needed
        int saveCount;
 
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
         
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
             
            // Step 4, draw the children
            dispatchDraw(canvas);
             
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                            mOverlay.getOverlayView().dispatchDraw(canvas);
            }
                         
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
                       
            // we"re done...
            return;
        }
        //省略2&5的情况
        ....
}        

源码已经非常清晰了draw总共分为6步;

绘制背景

如果需要的话,保存layers

绘制自身文本

绘制子视图

如果需要的话,绘制fading edges

绘制scrollbars

其中 第2步与第5步不是必须的。在第3步调用了onDraw方法来绘制自身的内容,在View中是空实现,这就是我们为什么在自定义View时必须要重写该方法。而第4步调用了dispatchDraw对子视图进行绘制。还是以验证码为例:

@Override
    protected void onDraw(Canvas canvas) {
        //绘制背景
        mPaint.setColor(getResources().getColor(R.color.autoCodeBg));
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

        mPaint.getTextBounds(autoText, 0, autoText.length(), bounds);
        //绘制文本
        for (int i = 0; i < autoText.length(); i++) {
             mPaint.setColor(getResources().getColor(colorRes[random.nextInt(6)]));
            canvas.drawText(autoText, i, i + 1, getWidth() / 2 - bounds.width() / 2 + i * bounds.width() / autoNum
                    , bounds.height() + random.nextInt(getHeight() - bounds.height())
                    , mPaint);
        }
 
        //绘制干扰点
        for (int j = 0; j < 250; j++) {
             canvas.drawPoint(random.nextInt(getWidth()), random.nextInt(getHeight()), pointPaint);
        }
 
        //绘制干扰线
        for (int k = 0; k < 20; k++) {
            int startX = random.nextInt(getWidth());
            int startY = random.nextInt(getHeight());
            int stopX = startX + random.nextInt(getWidth() - startX);
            int stopY = startY + random.nextInt(getHeight() - startY);
             linePaint.setColor(getResources().getColor(colorRes[random.nextInt(6)]));
            canvas.drawLine(startX, startY, stopX, stopY, linePaint);
        }
    }
其实很简单,就是一些绘制的业务逻辑。好了基本就到这里了,下面上传一张示例的效果图,与源码链接
示例图

对了还有自定义属性,这里简单说一下。自定义View时一般都要自定义属性,所以都会在res/values/attr.xml中定义attrdeclare-styleable,最后在自定义View中通过TypedArray获取。

示例源码地址:https://github.com/idisfkj/Cu...

个人博客:https://idisfkj.github.io/arc...

关注

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/13336.html

相关文章

  • Android-定义View

    摘要:自定义简单实现凹凸优惠券效果自定义属性的简单使用,继承重写方法使用来绘制,简单实现凹凸优惠券效果图文并茂自定义之切换标签自定义实现一个简单好用的切换标签自定义滑动确认控件自定义控件,用来进行滑动确认等操作。 Android 之自定义 View 的死亡三部曲之 Measure 我还不知道你的三围呢(你要占多少屏幕),我怎么能轻易让你出场呢? Android 自定义 View,ViewGr...

    UnixAgain 评论0 收藏0
  • Android 定义 View - 收藏集 - 掘金

    摘要:在本篇文安卓自定义进阶分类和流程掘金自定义分类与流程经历过前面三篇啰啰嗦嗦的基础篇之后,终于到了进阶篇,正式进入解析自定义的阶段。 Android 从 0 开始自定义控件之 View 的 draw 过程 (九) - Android - 掘金转载请标明出处: http://blog.csdn.net/airsaid/... 本文出自:周游的博客 ... Andriod 从 0 开始自定义...

    AndroidTraveler 评论0 收藏0
  • 定义view控件效果实现及实践

    摘要:项目需求讨论自定义实现步骤及封装根据实际项目需求出发。自定义控件之带下载进度的下载按钮最近要用到一个带下载进度的按钮,各种搜索了一波,很抱歉,实在没有发现自己想要的效果,没办法只能自己尝试实现了一个了。 项目需求讨论 - Android 自定义 Dialog 实现步骤及封装 根据实际项目需求出发。因为项目中的对话框要配合整个项目的 UI 风格,所以进行自定义 Dialog 的实现步骤,...

    shiweifu 评论0 收藏0
  • Android 定义View - 收藏集 - 掘金

    摘要:在本篇文安卓自定义进阶分类和流程掘金自定义分类与流程经历过前面三篇啰啰嗦嗦的基础篇之后,终于到了进阶篇,正式进入解析自定义的阶段。 这交互炸了(二):爱范儿是如何让详情页缩小为横向列表的 - 掘金本文同步自wing的地方酒馆 写在前面:写这段话的时候,已经是夜里3点了。别问我为什么这么拼,一切为了与你分享干货!!!! 不要太感动,擦擦眼泪继续往下看。 本开源库链接 Expandable...

    yanbingyun1990 评论0 收藏0
  • Android 面试题总结之Android 进阶(一)

    摘要:事实上,你可以只的几个构造函数代码直接一个实例的时候会调用第一个构造函数在创建但是没有指定的时候被调用多了一个类型的参数,自定义属性,在通过布局文件创建一个时,会把内的参数通过带入到内。 在前几篇文章中都是讲的基础,大家应该会觉得非常熟悉,但往往我们可能对于基础某些细节认识不够彻底或贯穿不全,我一直认为基础都是比较难的,那么本章节终于到进阶啦,主要讲的是View 的相关知识,在前面[...

    zhiwei 评论0 收藏0
  • Android 定义View的各种姿势1

    摘要:前言这一篇我们来看自定义的各种姿势。在进行自定义之前我们先来看一下的坐标系。需要注意的是直接继承自的控件需要对支持和做处理。以及在方法中加入了自身的处理。很明显,我们需要自定义一个控件符合上述要求。 该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。 前言 这一篇我们来看自定义View的各种姿势。前面几篇...

    jcc 评论0 收藏0

发表评论

0条评论

anRui

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<