Android 自定义View
来源:互联网 发布:易语言炫舞辅助源码 编辑:程序博客网 时间:2024/06/05 09:36
这篇博客写的很不好,我希望在一篇文章中记录自定义view的各个知识点,但是自定义view涉及到的东西太多,不可能用一篇文章讲清楚。最后导致这篇文章既不精简,又不清晰。
但是我在这里引用的网友的一些文章都是写的很好的,值得一读
- 从ViewRootImpl类分析View绘制的流程
- 自定义View基础 - 最易懂的自定义View原理系列(1)
顶级View
在Activity中指定View布局用的是setContentView()
方法,而不是叫做setView()
,从名字中可以看出我们设置的仅仅是内容视图,我们设置的并不是顶级View。对于顶级View,我只从别的博客文章中知道了一些概念,无法详细的描述它,更加不知道它和Activity、WindowManager、Window、ViewRootImpl之间的关系。等将来了解之后再写一篇吧。下面给出一些关于顶级View的概念
1. 顶级View是DecorView类型的,继承自FrameLayout
2. 它包含一个LinearLayout,分上下两部分,title(比如Actionbar)和content(setContentView)
3. 有的文章说DecorView包含通知栏,搞不懂,通知栏是哪个?应该不是指最上面的状态栏吧?先不管
其实只需要知道我们在Activity中指定的布局不是顶级View,就可以解答一些自定义View中的疑惑了,网上盗图一张,同时可以看《源码分析setContentView加载布局文件的过程》这篇文章,加深理解
创建 view
view 的四个构造方法如下:
// 一般直接 new 出一个 view 的时候使用该方法View(Context context)// 一般在 xml 文件中使用 view 的时候调用该方法View(Context context, AttributeSet attrs)// 下面零个构造器是被上面两个构造器调用的,或者是子类的构造器调用View(Context context, AttributeSet attrs, int defStyleAttr)//第四个构造器在API21中才加入,一般为了兼容性不会去使用它View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
如果使用了自定义属性,可以从第2、3、4个构造器中获得这些属性和它们的值,关于如何给View自定义属性,详见Android自定义属性
看完上面的文章后,还有一个疑惑——为什么需要第3、4个构造器?
第3、4个构造器的用途是给自定义View设置一些默认属性,比如有一个自定义View希望在没有被指定背景颜色的情况下把背景颜色设置为红色,那就可以用这两个构造器来实现。先看一下后面两个参数:
- defStyleAttr:一个指向style资源的属性,该属性必须在这个 app 的主题中被定义
API文档的原文 “An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.” - defStyleRes:一个style资源的id
API文档的原文“A resource identifier of a style resource that supplies default values for the view, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.”
所以说这两个参数到底是干嘛的?文字表述不清,还是用代码来解释吧
<!-- 首先在 attrs.xml 中定义一个属性 --><attr name="extendViewStyle" format="reference" />
<!-- styles.xml --><style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <!-- 在 app 主题中为自定义的属性复制 --> <item name="extendViewStyle">@style/TextView_defStyleAttr</item></style><!-- 自定义两个 style --> <style name="TextView_defStyleAttr"> <item name="android:textSize">46sp</item> </style> <style name="TextView_defStyleRes"> <item name="android:background">@color/colorAccent</item> </style>
//在 Java 代码中使用,例子中的这个 View 是继承自 TextView 的public class ExtendView extends TextView { public ExtendView(Context context) { super(context); } public ExtendView(Context context, @Nullable AttributeSet attrs) { super(context, attrs, R.attr.extendViewStyle); //希望使用 defStyleRes,那就不能使用 defStyleAttr,所以这里把 defStyleAttr 设为 0 //super(context, attrs, 0,R.style.TextView_defStyleRes); }}
关于 View 的主题样式,更详细的内容可参考Android中自定义样式与View的构造函数中的第三个参数defStyle的意义
确定view大小 onMeasure
长谈:关于 View Measure 测量机制,让我一次把话说完
上面这篇文章写的很好很细,值得一看
一般要设置 view 的大小,都是在 xml 文件中使用这两个属性来确定 view 的大小
android:layout_width="50dp"android:layout_height="50dp"
上面这两个 xml 属性是属于ViewGroup.LayoutParams
类的(这个类也就只有这两个xml属性) ,view 持有这个类型的对象,该对象可以告诉ViewGroup,当前View希望如何被放置,但它并不能真正决定View的大小
真正决定 View 大小的是 View 的onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法,也是在自定义 View 时需要被重写的方法。我们在onMeasure中计算View的大小,之后使用setMeasuredDimension(int measuredWidth, int measuredHeight)
设置View的大小。对于ViewGroup,除了自己的measure外,还要遍历子View的measure方法,measure再调用自身的onMeasure方法测量自身的大小,而onMeasure方法被执行后,ViewGroup就可以调用View的getMeasuredWidth/getMeasuredHeight方法来获得它的测量大小了
viewGroup 为什么不直接调用view的onMeasure()
? onMeasure()
可以被重写,用来实际测量 view 的大小,而measure()
是final的,不能被重写,之所以要有这个measure()
方法,网上的说法是为了保证 view 的 measure 框架不被改变
onMeasure方法的两个参数(widthMeasureSpec/heightMeasureSpec)中保存有view的大小与模式,可以通过MeasureSpec类来获取。这两个参数是根据View的ViewGroup.LayoutParams以及父View的widthMeasureSpec/heightMeasureSpec的成的,所以虽然我们用ViewGroup.LayoutParams来设置View的大小,但是它并不决定View的大小,还需要根据模式来确定View的最终大小
ViewGroup的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);}
MeasureSpec
很多文章上都有解释 widthMeasureSpec/heightMeasureSpec 如何来存储模式和大小,我认为没必要知道其原理,只需要知道下面几点就可以了
- 模式、大小以及 widthMeasureSpec/heightMeasureSpec 都是 int 型的
- MeasureSpec 类提供方法将两个分别表示模式和大小的 int 值合成一个值,也就成为了 widthMeasureSpec/heightMeasureSpec
- MeasureSpec 类提供方法解析 widthMeasureSpec/heightMeasureSpec 以获得具体的模式和大小
网上的说法是普通开发者几乎不会遇到这种模式,而且网上也找不到相关例子
故先忽略这种模式 EXACTLY 父 view 给出了子 view 的确切大小,这个大小就是View的大小,不管它是否超过了父 View AT_MOST 子 view 需要自己测量自己的大小,但是不能超过父 view 给出的值
一般情况下,在AT_MOST
模式的时候,需要测量View的大小,而在 EXACTLY模式下直接使用精确值,对于UNSPECIFIED模式,则不考虑
View模式的生成规则如下:
child(下)
EXACTLY
AT_MOST
UNSPECIFIED
dp
EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent
EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content
AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
顶级view因为没有父View,它的规则会有所不同
match_parent
EXACTLY
,大小为窗口大小 wrap_content
AT_MOST
,大小不能超过窗口大小 固定大小(dp)
EXACTLY
,大小为 LayoutParams 的指定大小光学(视觉)边界
计算完View的大小后使用setMeasuredDimension设置mMeasuredWidth/mMeasuredHeight,从源码中可以看到setMeasuredDimension方法不是简单的赋值,它有可能会调整measuredWidth/measuredHeight
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight);}
这是根据View是否使用了视觉边界(或者是翻译为光学边界,Optical bounds layout)来调整View的大小,我没找到介绍这个视觉边界的比较好的文章,姑且看一下这个——Android 4.3中的视觉边界布局(Optical bounds layout)
确定view位置 onLayout
View有四个字段来确定自身在父View中的位置
mLeft:view自身的左边到其父布局【左边】的距离
mTop:view自身的顶边到其父布局【顶边】的距离
mRight:view自身的右边到其父布局【左边】的距离
mBottom:view自身的底边到其父布局【顶边】的距离
网上盗个图
简单的说这四个字段就是View左上角与右下角两个顶点的坐标,这两个点的坐标确定后,View这个矩形的位置和大小也就确定了,没错,大小也是根据这四个值确定的,可以直接无视setMeasuredDimension方法中设置的值,直接使用View的setXX方法来设置View的大小,不过这个方法只能在【layout系统】中使用。关于这个layout系统,我想指的就是View的layout和onLayout方法吧。我在onLayout方法中成功通过setXXX设置过View的大小,但是在Activity的onCreate方法中使用View的setXXX方法不起作用
简单的说就是layout方法确定自己的位置(给mLeft等赋值),onLayout方法计算子View的位置并调用子View的layout方法,如果没有不是ViewGroup,没有子View,就不用重写onLayout方法了,更加详细的流程请见写给新人看的自定义View-onLayout篇
getMeasuredWidth与getWidth的区别
- getMeasuredWidth方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定
- getWidth方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的
- 一般情况下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法
- 不管View的大小如何,一般情况下这两个方法返回的值是相同的
更详细的解释可以参考这篇文章——- Android开发之getMeasuredWidth和getWidth区别从源码分析
有一个问题是getMeasuredWidth和getMeasuredWidthAndState返回的值也是一样的,但是我不知道他们有什么区别
绘制view onDraw
View的绘制由Draw方法来完成,Draw方法的绘制过程为:
1. 绘制背景 drawBackground(canvas);
2. 绘制自己 onDraw(canvas);
3. 绘制子view dispatchDraw(canvas);该方法会遍历所有子元素的draw方法
4. 绘制装饰 onDrawForeground(canvas);
值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和getIntrinsicWidth(),getIntrinsicHeight()方法,然后设为背景。
自定义 view 的案例
Android 一个绚丽的loading动效分析与实现!
上面这个自定义View的案例非常炫酷,但是对于初学者而言,我并不建议去看这个例子,我认为对于初学者而言,最重要的是了解onMeasure和onLayout,至于onDraw,我还需要更深入的去学习如何绘图,所以下面这个例子可能更好一点
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
图形绘制
我本来试图来做一个关于如何绘制图形的笔记,后来我放弃了,这个实在是太难了,如果真要做那需要好多篇文章,好多个例子,才能了解图形绘制的一些基本操作。网上已经有很多的博客了,我不想再做无用功
下面这三篇文章讲述了如何绘制图形,第一篇文章很大很全,后面两篇作为补充
- 安卓自定义View教程目录
- Android Paint API总结和使用方法
- Android Paint的使用详解
绘制图形踩过的坑
一、绘制文字的位置问题
文字的位置是由文字的 X 轴坐标(受文字对齐方式影响,默认是左对齐),和下滑线的 Y 轴坐标决定的;我先前以为这两个坐标是文字左上角的 X、Y 轴坐标……
//设置文字下划线,这样结果更清晰mPaint.setUnderlineText(true);//设置文本对齐方式为左对齐mPaint.setTextAlign(Paint.Align.LEFT);//文字下划线的Y轴坐标是0,所以看不到文字canvas.drawText("hello",0,0,mPaint);//向下移动移动画布,canvas是绘制规则,而不是日常生活中说的画布,所以移动canvas后,先前绘制的图像不会跟着移动canvas.translate(0,100);canvas.drawText("hello",0,0,mPaint);//设置文本对齐方式为中心对齐mPaint.setTextAlign(Paint.Align.CENTER);canvas.drawText("hello",0,100,mPaint);
所以文字的水平居中很简单,只要设置对齐方式是 center ,再计算 view 中心的水平坐标即可,但是垂直居中就比较难算了
二、绘制图片的大小
绘制图片时需要知道图像的大小,这里有一个问题,如果根据 drawable 资源创建一个 bitmap 对象 Bitmap mImageSrc = BitmapFactory.decodeResource(getResources(), src);
根据mImageSrc.getWidth()、mImageSrc.getHeight()
方法得到的不是这张图片的宽和高
而是图像的原本宽高乘以dp/px
后的大小,这里我认为android是把图片资源的1px
当做是1dp
在计算,才造成了这种结果
解决的办法是计算图片的原始大小
private void setImgSize(Bitmap bitmap, Size imgSize){ float scale = bitmap.getDensity()/160f; Log.d(TAG, "getImageSize: scale: "+scale); int width = (int)(bitmap.getWidth()/scale); int height = (int)(bitmap.getHeight()/scale); imgSize.set(width,height);}
三、图层的叠加
就是 Paint 的 setXfermode 方法在叠加图像的时候,会将所有已存在的颜色图像都当做背景,来与前景图像做叠加。所以如果直接使用 setXfermode 方法,那整个 content view(也就是不包括 Actionbar)的这一块全是背景。为了正确地做到图层的叠加,可以先新建一个 bitmap ,在bitmap上绘制完成背景与前景后,再把这个 bitmap 绘制到 view 上,或者是使用图层
图层的概念类似于 photoshop 中的图层,就是一张透明的图片
//创建图层,新建的图层在原画面之上,所以后面绘制的所有图像都将绘制在这个图层上canvas.saveLayer(0,0,canvas.getWidth(),canvas.getHeight(),null);//绘制背景canvas.drawCircle(canvas.getWidth()/2,canvas.getHeight()/2,Math.min(mRectDst.width()/2,mRectDst.height()/2),mPaint);//设置叠加模式mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//绘制前景canvas.drawBitmap(mImageSrc,mRectSrc,mRectDst,mPaint);//restore后,图图层上的图像会被绘制到下面canvas.restore();
- Android View---自定义View
- Android View---自定义View
- Android 自定义View 之 自定义View属性
- 【自定义View系列】android自定义View概述
- Android自定义view自定义属性
- Android自定义控件 -- 自定义View
- android自定义view(自定义数字键盘)
- Android自定义View-自定义属性
- Android自定义View-自定义属性
- Android 自定义View
- Android 自定义 View
- android自定义View
- Android 中自定义 view
- android 自定义view组件
- Android 自定义 View
- android 自定义view
- Android:如何自定义View
- android 自定义View
- 变量常量
- 前言的闲话以及第一章的入门(五)
- 古文觀止卷八_送溫處士赴河陽軍序_韓愈
- HashMap实现原理
- 个人公众号
- Android 自定义View
- 标识符.关键字
- POJ 3692 Kindergarten (补图的最大独立集||匈牙利算法)
- java8_LocalDate类实现日历打印
- QT之LineEdit
- 51Nod-1134-最长递增子序列
- OpenCV编译Python调用的库
- 前言的闲话以及第一章的入门(六)
- 算法题/顺时针打印矩阵