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加载布局文件的过程》这篇文章,加深理解

Alt text


自定义 View 的简单流程

Created with Raphaël 2.1.0创建 view确定 view 大小确定 view 位置绘制 view

创建 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希望在没有被指定背景颜色的情况下把背景颜色设置为红色,那就可以用这两个构造器来实现。先看一下后面两个参数:

  1. 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.”
  2. 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 如何来存储模式和大小,我认为没必要知道其原理,只需要知道下面几点就可以了

  1. 模式、大小以及 widthMeasureSpec/heightMeasureSpec 都是 int 型的
  2. MeasureSpec 类提供方法将两个分别表示模式和大小的 int 值合成一个值,也就成为了 widthMeasureSpec/heightMeasureSpec
  3. MeasureSpec 类提供方法解析 widthMeasureSpec/heightMeasureSpec 以获得具体的模式和大小
MeasureSpec 提供的模式 描述 UNSPECIFIED 父 view 没给子 view 添加限制,子 view 可以是任意大小
网上的说法是普通开发者几乎不会遇到这种模式,而且网上也找不到相关例子
故先忽略这种模式 EXACTLY 父 view 给出了子 view 的确切大小,这个大小就是View的大小,不管它是否超过了父 View AT_MOST 子 view 需要自己测量自己的大小,但是不能超过父 view 给出的值

一般情况下,在AT_MOST模式的时候,需要测量View的大小,而在 EXACTLY模式下直接使用精确值,对于UNSPECIFIED模式,则不考虑
View模式的生成规则如下:

parent(右)
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,它的规则会有所不同

LayoutParams 模式 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自身的底边到其父布局【顶边】的距离
网上盗个图
Alt text
简单的说这四个字段就是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的区别

  1. getMeasuredWidth方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定
  2. getWidth方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的
  3. 一般情况下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法
  4. 不管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

图形绘制

我本来试图来做一个关于如何绘制图形的笔记,后来我放弃了,这个实在是太难了,如果真要做那需要好多篇文章,好多个例子,才能了解图形绘制的一些基本操作。网上已经有很多的博客了,我不想再做无用功
下面这三篇文章讲述了如何绘制图形,第一篇文章很大很全,后面两篇作为补充

  1. 安卓自定义View教程目录
  2. Android Paint API总结和使用方法
  3. 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();
原创粉丝点击