首页 Java java教程 Android自定义View

Android自定义View

Nov 17, 2016 am 09:07 AM

前言

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

流程

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

wKiom1gsXnnQJXgSAAB-0Q6z7sI602.png-wh_651x-s_719424164.png

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

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

measure

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

我们可以通过以下代码分别获取:

int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
登录后复制

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

QQ图片20161117090526.png

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); 
    }
登录后复制

可以对自定义View的layout_width与layout_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方法来分别获取宽高,最后再调用的就是View的measure方法,而通过前面的分析我们已经知道它做的就是对视图大小的计算。而对于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&#39;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&#39;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,最后再将获取的size与mode通过MeasureSpec.makeMeasureSpec方法整合返回。最后传递到measure中,这就是前面所说的widthMeasureSpec与heightMeasureSpec中包含的两部分的值。整个过程为measureChildren->measureChild->getChildMeasureSpec->measure->onMeasure->setMeasuredDimension,所以通过measureChildren就可以对子视图进行测量计算。

layout

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

getWidth、getHeight与getMeasureWidth、getMeasureHeight是不同的,前者是在onLayout之后才能获取到的值,分别为left-right与top-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发动的,dispatchDraw是ViewGroup中的方法,在View是空实现。自定义View时不需要去管理该方法。而draw方法只在View中存在,ViewGoup做的只是在dispatchDraw中调用drawChild方法,而drawChild中调用的就是View的draw方法。那么我们来看下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&#39; layers to prepare for fading 
         *      3. Draw view&#39;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&#39;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); 
        } 
    }
登录后复制

图,与源码链接

示例图

wKioL1gsX2fCEmy0AAFhWwhl5k8793.gif

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1672
14
CakePHP 教程
1428
52
Laravel 教程
1333
25
PHP教程
1277
29
C# 教程
1257
24
公司安全软件导致应用无法运行?如何排查和解决? 公司安全软件导致应用无法运行?如何排查和解决? Apr 19, 2025 pm 04:51 PM

公司安全软件导致部分应用无法正常运行的排查与解决方法许多公司为了保障内部网络安全,会部署安全软件。...

如何将姓名转换为数字以实现排序并保持群组中的一致性? 如何将姓名转换为数字以实现排序并保持群组中的一致性? Apr 19, 2025 pm 11:30 PM

将姓名转换为数字以实现排序的解决方案在许多应用场景中,用户可能需要在群组中进行排序,尤其是在一个用...

如何使用MapStruct简化系统对接中的字段映射问题? 如何使用MapStruct简化系统对接中的字段映射问题? Apr 19, 2025 pm 06:21 PM

系统对接中的字段映射处理在进行系统对接时,常常会遇到一个棘手的问题:如何将A系统的接口字段有效地映�...

IntelliJ IDEA是如何在不输出日志的情况下识别Spring Boot项目的端口号的? IntelliJ IDEA是如何在不输出日志的情况下识别Spring Boot项目的端口号的? Apr 19, 2025 pm 11:45 PM

在使用IntelliJIDEAUltimate版本启动Spring...

如何优雅地获取实体类变量名构建数据库查询条件? 如何优雅地获取实体类变量名构建数据库查询条件? Apr 19, 2025 pm 11:42 PM

在使用MyBatis-Plus或其他ORM框架进行数据库操作时,经常需要根据实体类的属性名构造查询条件。如果每次都手动...

Java对象如何安全地转换为数组? Java对象如何安全地转换为数组? Apr 19, 2025 pm 11:33 PM

Java对象与数组的转换:深入探讨强制类型转换的风险与正确方法很多Java初学者会遇到将一个对象转换成数组的�...

电商平台SKU和SPU数据库设计:如何兼顾用户自定义属性和无属性商品? 电商平台SKU和SPU数据库设计:如何兼顾用户自定义属性和无属性商品? Apr 19, 2025 pm 11:27 PM

电商平台SKU和SPU表设计详解本文将探讨电商平台中SKU和SPU的数据库设计问题,特别是如何处理用户自定义销售属...

如何利用Redis缓存方案高效实现产品排行榜列表的需求? 如何利用Redis缓存方案高效实现产品排行榜列表的需求? Apr 19, 2025 pm 11:36 PM

Redis缓存方案如何实现产品排行榜列表的需求?在开发过程中,我们常常需要处理排行榜的需求,例如展示一个�...

See all articles