Android自定义View

简介:

前言

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

流程

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

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

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

measure

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

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

 
 
  1. int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
  2. int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
  3. int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
  4. 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方法如下:

 
 
  1. @Override 
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  3.         int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
  4.         int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
  5.         int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
  6.         int heightMode = MeasureSpec.getMode(heightMeasureSpec); 
  7.         if (widthMode == MeasureSpec.EXACTLY) { 
  8.             //直接获取精确的宽度 
  9.             width = widthSize; 
  10.         } else if (widthMode == MeasureSpec.AT_MOST) { 
  11.             //计算出宽度(文本的宽度+padding的大小) 
  12.             width = bounds.width() + getPaddingLeft() + getPaddingRight(); 
  13.         } 
  14.         if (heightMode == MeasureSpec.EXACTLY) { 
  15.             //直接获取精确的高度 
  16.             height = heightSize; 
  17.         } else if (heightMode == MeasureSpec.AT_MOST) { 
  18.             //计算出高度(文本的高度+padding的大小) 
  19.             height = bounds.height() + getPaddingBottom() + getPaddingTop(); 
  20.         } 
  21.         //设置获取的宽高 
  22.         setMeasuredDimension(width, height); 
  23.     }  

可以对自定义View的layout_width与layout_height进行设置不同的属性,达到不同的mode类型,就可以看到不同的效果

measureChildren

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

 
 
  1. protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { 
  2.         final int size = mChildrenCount; 
  3.         final View[] children = mChildren; 
  4.         for (int i = 0; i < size; ++i) { 
  5.             final View child = children[i]; 
  6.             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { 
  7.                 measureChild(child, widthMeasureSpec, heightMeasureSpec); 
  8.             } 
  9.         } 
  10.     }  

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

 
 
  1. protected void measureChild(View child, int parentWidthMeasureSpec, 
  2.             int parentHeightMeasureSpec) { 
  3.         final LayoutParams lp = child.getLayoutParams(); 
  4.         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
  5.                 mPaddingLeft + mPaddingRight, lp.width); 
  6.         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
  7.                 mPaddingTop + mPaddingBottom, lp.height); 
  8.         child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  9.     }  

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

 
 
  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
  2.         int specMode = MeasureSpec.getMode(spec); 
  3.         int specSize = MeasureSpec.getSize(spec); 
  4.   
  5.         int size = Math.max(0, specSize - padding); 
  6.   
  7.         int resultSize = 0; 
  8.         int resultMode = 0; 
  9.   
  10.         switch (specMode) { 
  11.         // Parent has imposed an exact size on us 
  12.         case MeasureSpec.EXACTLY: 
  13.             if (childDimension >= 0) { 
  14.                 resultSize = childDimension; 
  15.                 resultMode = MeasureSpec.EXACTLY; 
  16.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  17.                 // Child wants to be our size. So be it. 
  18.                 resultSize = size
  19.                 resultMode = MeasureSpec.EXACTLY; 
  20.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  21.                 // Child wants to determine its own size. It can't be 
  22.                 // bigger than us. 
  23.                 resultSize = size
  24.                 resultMode = MeasureSpec.AT_MOST; 
  25.             } 
  26.             break; 
  27.   
  28.         // Parent has imposed a maximum size on us 
  29.         case MeasureSpec.AT_MOST: 
  30.             if (childDimension >= 0) { 
  31.                 // Child wants a specific size... so be it 
  32.                 resultSize = childDimension; 
  33.                 resultMode = MeasureSpec.EXACTLY; 
  34.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  35.                 // Child wants to be our size, but our size is not fixed. 
  36.                 // Constrain child to not be bigger than us. 
  37.                 resultSize = size
  38.                 resultMode = MeasureSpec.AT_MOST; 
  39.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  40.                 // Child wants to determine its own size. It can't be 
  41.                 // bigger than us. 
  42.                 resultSize = size
  43.                 resultMode = MeasureSpec.AT_MOST; 
  44.             } 
  45.             break; 
  46.   
  47.         // Parent asked to see how big we want to be 
  48.         case MeasureSpec.UNSPECIFIED: 
  49.             if (childDimension >= 0) { 
  50.                 // Child wants a specific size... let him have it 
  51.                 resultSize = childDimension; 
  52.                 resultMode = MeasureSpec.EXACTLY; 
  53.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  54.                 // Child wants to be our size... find out how big it should 
  55.                 // be 
  56.                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  57.                 resultMode = MeasureSpec.UNSPECIFIED; 
  58.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  59.                 // Child wants to determine its own size.... find out how 
  60.                 // big it should be 
  61.                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  62.                 resultMode = MeasureSpec.UNSPECIFIED; 
  63.             } 
  64.             break; 
  65.         } 
  66.         //noinspection ResourceType 
  67.         return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
  68.     }  

是不是容易理解了点呢。它做的就是前面所说的根据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为例:

 
 
  1. @Override 
  2.     protected void onLayout(boolean changed, int l, int t, int r, int b) { 
  3.         int count = getChildCount(); 
  4.         MarginLayoutParams params; 
  5.          
  6.         int cl; 
  7.         int ct; 
  8.         int cr; 
  9.         int cb; 
  10.              
  11.         for (int i = 0; i < count; i++) { 
  12.             View child = getChildAt(i); 
  13.             params = (MarginLayoutParams) child.getLayoutParams(); 
  14.                  
  15.             if (i == 0) { 
  16.                 //左上角 
  17.                 cl = params.leftMargin; 
  18.                 ct = params.topMargin; 
  19.             } else if (i == 1) { 
  20.                 //右上角 
  21.                 cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); 
  22.                 ct = params.topMargin; 
  23.             } else if (i == 2) { 
  24.                 //左下角 
  25.                 cl = params.leftMargin; 
  26.                 ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() 
  27.                  - params.topMargin; 
  28.             } else { 
  29.                 //右下角 
  30.                 cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); 
  31.                 ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() 
  32.                  - params.topMargin; 
  33.             } 
  34.             cr = cl + child.getMeasuredWidth(); 
  35.             cb = ct + child.getMeasuredHeight(); 
  36.             //确定子视图在父视图中放置的位置 
  37.             child.layout(cl, ct, cr, cb); 
  38.         } 
  39.     }  

至于onMeasure的实现源码我后面会给链接,如果要看效果图的话,我后面也会贴出来,前面的那个验证码的也是一样

draw

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

 
 
  1. public void draw(Canvas canvas) { 
  2.         final int privateFlags = mPrivateFlags; 
  3.         final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 
  4.                 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); 
  5.         mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; 
  6.           
  7.         /* 
  8.          * Draw traversal performs several drawing steps which must be executed 
  9.          * in the appropriate order
  10.          * 
  11.          *      1. Draw the background 
  12.          *      2. If necessary, save the canvas' layers to prepare for fading 
  13.          *      3. Draw view's content 
  14.          *      4. Draw children 
  15.          *      5. If necessary, draw the fading edges and restore layers 
  16.          *      6. Draw decorations (scrollbars for instance) 
  17.          */ 
  18.            
  19.         // Step 1, draw the background, if needed 
  20.         int saveCount; 
  21.   
  22.         if (!dirtyOpaque) { 
  23.             drawBackground(canvas); 
  24.         } 
  25.           
  26.         // skip step 2 & 5 if possible (common case
  27.         final int viewFlags = mViewFlags; 
  28.         boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 
  29.         boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 
  30.         if (!verticalEdges && !horizontalEdges) { 
  31.             // Step 3, draw the content 
  32.             if (!dirtyOpaque) onDraw(canvas); 
  33.               
  34.             // Step 4, draw the children 
  35.             dispatchDraw(canvas); 
  36.               
  37.             // Overlay is part of the content and draws beneath Foreground 
  38.             if (mOverlay != null && !mOverlay.isEmpty()) { 
  39.                             mOverlay.getOverlayView().dispatchDraw(canvas); 
  40.             } 
  41.                           
  42.             // Step 6, draw decorations (foreground, scrollbars) 
  43.             onDrawForeground(canvas); 
  44.                         
  45.             // we're done... 
  46.             return
  47.         } 
  48.         //省略2&5的情况 
  49.         .... 
  50. }     

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

  • 绘制背景
  • 如果需要的话,保存layers
  • 绘制自身文本
  • 绘制子视图
  • 如果需要的话,绘制fading edges
  • 绘制scrollbars

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

 
 
  1. @Override 
  2.     protected void onDraw(Canvas canvas) { 
  3.         //绘制背景 
  4.         mPaint.setColor(getResources().getColor(R.color.autoCodeBg)); 
  5.         canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); 
  6.  
  7.         mPaint.getTextBounds(autoText, 0, autoText.length(), bounds); 
  8.         //绘制文本 
  9.         for (int i = 0; i < autoText.length(); i++) { 
  10.              mPaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); 
  11.             canvas.drawText(autoText, i, i + 1, getWidth() / 2 - bounds.width() / 2 + i * bounds.width() / autoNum 
  12.                     , bounds.height() + random.nextInt(getHeight() - bounds.height()) 
  13.                     , mPaint); 
  14.         } 
  15.   
  16.         //绘制干扰点 
  17.         for (int j = 0; j < 250; j++) { 
  18.              canvas.drawPoint(random.nextInt(getWidth()), random.nextInt(getHeight()), pointPaint); 
  19.         } 
  20.   
  21.         //绘制干扰线 
  22.         for (int k = 0; k < 20; k++) { 
  23.             int startX = random.nextInt(getWidth()); 
  24.             int startY = random.nextInt(getHeight()); 
  25.             int stopX = startX + random.nextInt(getWidth() - startX); 
  26.             int stopY = startY + random.nextInt(getHeight() - startY); 
  27.              linePaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); 
  28.             canvas.drawLine(startX, startY, stopX, stopY, linePaint); 
  29.         } 
  30.     }  

其实很简单,就是一些绘制的业务逻辑。好了基本就到这里了,下面上传一张示例的效果图,与源码链接

示例图

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



作者:idisfkj
来源:51CTO
目录
相关文章
|
17天前
|
存储 Shell Android开发
基于Android P,自定义Android开机动画的方法
本文详细介绍了基于Android P系统自定义开机动画的步骤,包括动画文件结构、脚本编写、ZIP打包方法以及如何将自定义动画集成到AOSP源码中。
41 2
基于Android P,自定义Android开机动画的方法
|
15天前
|
供应链 物联网 区块链
未来触手可及:探索新兴技术的趋势与应用安卓开发中的自定义视图:从基础到进阶
【8月更文挑战第30天】随着科技的飞速发展,新兴技术如区块链、物联网和虚拟现实正在重塑我们的世界。本文将深入探讨这些技术的发展趋势和应用场景,带你领略未来的可能性。
|
17天前
|
测试技术 Android开发 Python
探索软件测试的艺术:从基础到高级安卓应用开发中的自定义视图
【8月更文挑战第29天】在软件开发的世界中,测试是不可或缺的一环。它如同艺术一般,需要精细的技巧和深厚的知识。本文旨在通过浅显易懂的语言,引领读者从软件测试的基础出发,逐步深入到更复杂的测试策略和工具的使用,最终达到能够独立进行高效测试的水平。我们将一起探索如何通过不同的测试方法来确保软件的质量和性能,就像艺术家通过不同的色彩和笔触来完成一幅画作一样。
|
4天前
|
缓存 搜索推荐 Android开发
安卓应用开发中的自定义View组件实践
【9月更文挑战第10天】在安卓开发领域,自定义View是提升用户体验和实现界面个性化的重要手段。本文将通过一个实际案例,展示如何在安卓项目中创建和使用自定义View组件,包括设计思路、实现步骤以及可能遇到的问题和解决方案。文章不仅提供了代码示例,还深入探讨了自定义View的性能优化技巧,旨在帮助开发者更好地掌握这一技能。
|
6天前
|
Android开发
Android中SurfaceView的双缓冲机制和普通View叠加问题解决办法
本文介绍了 Android 平台上的 SurfaceView,这是一种高效的图形渲染控件,尤其适用于视频播放、游戏和图形动画等场景。文章详细解释了其双缓冲机制,该机制通过前后缓冲区交换来减少图像闪烁,提升视觉体验。然而,SurfaceView 与普通 View 叠加时可能存在 Z-Order 不一致、同步问题及混合渲染难题。文中提供了使用 TextureView、调整 Z-Order 和创建自定义组合控件等多种解决方案。
38 9
|
9天前
|
Android开发 容器
Android经典实战之如何获取View和ViewGroup的中心点
本文介绍了在Android中如何获取`View`和`ViewGroup`的中心点坐标,包括计算相对坐标和屏幕上的绝对坐标,并提供了示例代码。特别注意在视图未完成测量时可能出现的宽高为0的问题及解决方案。
23 7
|
15天前
|
XML 搜索推荐 Android开发
安卓开发中的自定义View组件实践
【8月更文挑战第30天】探索Android世界,自定义View是提升应用界面的关键。本文以简洁的语言带你了解如何创建自定义View,从基础到高级技巧,一步步打造个性化的UI组件。
|
17天前
|
Android开发
Android在rootdir根目录创建自定义目录和挂载点的方法
本文介绍了在Android高通平台的根目录下创建自定义目录和挂载点的方法,通过修改Android.mk文件并使用`LOCAL_POST_INSTALL_CMD`变量在编译过程中添加目录,最终在ramdisk.img的系统根路径下成功创建了`/factory/bin`目录。
38 1
|
30天前
|
API Android开发 开发者
Android经典实战之使用ViewCompat来处理View兼容性问题
本文介绍Android中的`ViewCompat`工具类,它是AndroidX库核心部分的重要兼容性组件,确保在不同Android版本间处理视图的一致性。文章列举了设置透明度、旋转、缩放、平移等功能,并提供了背景色、动画及用户交互等实用示例。通过`ViewCompat`,开发者可轻松实现跨版本视图操作,增强应用兼容性。
68 5
|
7天前
|
前端开发 搜索推荐 Android开发
探索安卓开发中的自定义视图##
【9月更文挑战第6天】 在安卓应用开发的世界里,自定义视图如同绘画艺术中的色彩,它们为界面设计增添了无限可能。通过掌握自定义视图的绘制技巧,开发者能够创造出既符合品牌形象又提升用户体验的独特界面元素。本文将深入浅出地介绍如何从零开始构建一个自定义视图,包括基础框架搭建、关键绘图方法实现、事件处理机制以及性能优化策略。准备好让你的安卓应用与众不同了吗?让我们开始吧! ##