【Android - 进阶】之自定义视图浅析

1       概述

Android自定义View / ViewGroup的步骤大致如下:

1)  自定义属性;
2)  选择和设置构造方法;
3)  重写onMeasure()方法;
4)  重写onDraw()方法;
5)  重写onLayout()方法;
6)  重写其他事件的方法(滑动监听等)。

2       自定义属性

Android自定义属性主要有定义、使用和获取三个步骤。

2.1    定义自定义属性

参考:http://blog.csdn.net/lmj623565791/article/details/45022631/

我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。

先来看一段示例代码:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="rightPadding" format="dimension" />

    <declare-styleable name="CustomMenu">
        <attr name="rightPadding" />
    </declare-styleable>
</resources> 

可以看到,我们先是定义了一个属性rightPadding,然后又在CustomMenu中引用了这个属性。下面说明一下:

  • 首先,我们可以在declare-stylable标签中直接定义属性而不需要引用外部定义好的属性,但是为了属性的重用,我们可以选择上面的这种方法:先定义,后引用;
  • declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称;
  • 所谓的在declare-stylable标签中的引用,就是去掉了外部定义的format属性,如果没有去掉format,则会报错;如果外部定义中没有format而在内部引用中又format,也一样会报错。

常用的format类型:

1)  string:字符串类型;
2)  integer:整数类型;
3)  float:浮点型;
4)  dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
5)  Boolean:布尔值;
6)  reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
7)  color:颜色,必须是“#”符号开头;
8)  fraction:百分比,必须是“%”符号结尾;
9)  enum:枚举类型

下面对format类型说明几点:

  • format中可以写多种类型,中间使用“|”符号分割开,表示这几种类型都可以传入这个属性;
  • enum类型的定义示例如下代码所示:
<resources>
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>

    <declare-styleable name="CustomView">
        <attr name="orientation" />
    </declare-styleable>
</resources>

使用时通过getInt()方法获取到value并判断,根据不同的value进行不同的操作即可。

2.2    使用自定义属性

在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace。Android中默认的namespace是android,因此我们通常可以使用“android:xxx”的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:

xmlns:android="http://schemas.android.com/apk/res/android"

我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。

自定义属性的命名空间如下:

xmlns:app="http://schemas.android.com/apk/res-auto"

可以看出来,除了将命名空间的名称从android改成app之外,就是将最后的“res/android”改成了“res-auto”。

注意:自定义namespace的名称可以自己定义,不一定非得是app。

2.3    获取自定义属性

在自定义View / ViewGroup中,我们可以通过TypedArray获取到自定义的属性。示例代码如下:

public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0);
    int indexCount = a.getIndexCount();
    for (int i = 0; i < indexCount; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            case R.styleable.CustomMenu_rightPadding:
                mMenuRightPadding = a.getDimensionPixelSize(attr, 0);
                break;
        }
    }
    a.recycle();
}

这里需要说明一下:

  • 获取自定义属性的代码通常是在三个参数的构造方法中编写的(具体为什么是三个参数的构造方法,下面的章节中会有解释);
  • 在获取TypedArray对象时就为其绑定了该自定义View的自定义属性集(CustomMenu),通过getIndexCount()方法获取到自定义属性的数量,通过getIndex()方法获取到某一个属性,最后通过switch语句判断属性并进行相应的操作;
  • 在TypedArray使用结束后,需要调用recycle()方法回收它。

3       构造方法

当我们定义一个新的类继承了View或ViewGroup时,系统都会提示我们重写它的构造方法。View / ViewGroup中又四个构造方法可以重写,它们分别有一、二、三、四个参数。四个参数的构造方法我们通常用不到,因此这个章节中我们主要介绍一个参数、两个参数和三个参数的构造方法(这里以CustomMenu控件为例)。

3.1    一个参数的构造方法

构造方法的代码: public CustomMenu(Context context) { …… } 

这个构造方法只有一个参数Context上下文。当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法。

3.2    两个参数的构造方法

构造方法的代码: public CustomMenu(Context context, AttributeSet attrs) { …… } 

这个构造方法有两个参数:Context上下文和AttributeSet属性集。当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。

我们可以通过AttributeSet对象的getAttributeCount()方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。

注意:不管有没有使用自定义属性,都会默认调用这个构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。

3.3    三个参数的构造方法

构造方法的代码: public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… } 

这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。

defStyleAttr指向的是自定义属性的<declare-styleable>标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。

3.4    三个构造方法的整合

一般情况下,我们会将这三个构造方法串联起来,即层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:

public CustomMenu(Context context) {
    this(context, null);
}

public CustomMenu(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // 业务代码
}

这样一来,就可以保证无论使用什么方式创建这个控件,最终都会到三个参数的构造方法中处理,减少了重复代码。

4       onMeasure()

onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式测量值(测量 = 测量模式 + 测量值)。

widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。

测量模式分为以下三种情况:

1)  EXACTLY:当宽高值设置为具体值时使用,如100DIP、match_parent等,此时取出的size是精确的尺寸;
2)  AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
3)  UNSPECIFIED:当没有指定宽高值时使用(很少见)。

onMeasure()方法中常用的方法:

1)  getChildCount():获取子View的数量;
2)  getChildAt(i):获取第i个子控件;
3)  subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
4)  measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
5)  child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
6)  getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
7)  setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。

注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。

5       onDraw()

onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。

要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。

注意:每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。

5.1    Paint类

Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。

Paint类中有如下方法:

1、图形绘制:

1)  setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
2)  setAlpha(int a):设置绘制的图形的透明度;
3)  setColor(int color):设置绘制的颜色;
4)  setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
5)  setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
6)  setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
7)  setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
8)  setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;
9)  setPathEffect(PathEffect pe):设置绘制的路径的效果;
10) setShader(Shader s):设置Shader绘制各种渐变效果;
11) setShadowLayer(float r, int x, int y, int c):在图形下面设置阴影层,r为阴影角度,x和y为阴影在x轴和y轴上的距离,c为阴影的颜色;
12) setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
13) setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;
14) setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;
15) setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
16) setXfermode(Xfermode m):设置图形重叠时的处理方式;

2、文本绘制:

1)  setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
2)  setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
3)  setTextSize(float s):设置字号;
4)  setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
5)  setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
6)  setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
7)  setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
8)  setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;
9)  setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;

3、其他方法:

1)  getTextBounds(String t, int s, int e, Rect b):将页面中t文本从s下标开始到e下标结束的所有字符所占的区域宽高封装到b这个矩形中;
2)  clearShadowLayer():清除阴影层;
3)  measureText(String t, int s, int e):返回t文本中从s下标开始到e下标结束的所有字符所占的宽度;
4)  reset():重置画笔为默认值。

这里需要就几个方法解释一下:

1、setPathEffect(PathEffect pe):设置绘制的路径的效果:

常见的有以下几种可选方案:

1)  CornerPathEffect:可以用圆角来代替尖锐的角;
2)  DathPathEffect:虚线,由短线和点组成;
3)  DiscretePathEffect:荆棘状的线条;
4)  PathDashPathEffect:定义一种新的形状并将其作为原始路径的轮廓标记;
5)  SumPathEffect:在一条路径中顺序添加参数中的效果;
6)  ComposePathEffect:将两种效果组合起来,先使用第一种效果,在此基础上应用第二种效果。

2、setXfermode(Xfermode m):设置图形重叠时的处理方式:

关于Xfermode的多种效果,我们可以参考下面一张图:

在使用的时候,我们需要通过paint. setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))来设置,XXX是上图中的某种模式对应的常量参数,如DST_OUT。

5.2    Canvas类

Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。

Canvas对象中可以绘制:

1)  drawArc():绘制圆弧;
2)  drawBitmap():绘制Bitmap图像;
3)  drawCircle():绘制圆圈;
4)  drawLine():绘制线条;
5)  drawOval():绘制椭圆;
6)  drawPath():绘制Path路径;
7)  drawPicture():绘制Picture图片;
8)  drawRect():绘制矩形;
9)  drawRoundRect():绘制圆角矩形;
10) drawText():绘制文本;
11) drawVertices():绘制顶点。

Canvas对象的其他方法:

1)  canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;
2)  canvas.restore():把当前画布调整到上一个save()之前的状态;
3)  canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;
4)  canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;
5)  canvas.rotate(angle):将当前画布顺时针旋转angle度。

6       onLayout()

onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。

onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。

以下是onLayout()方法中常用的方法:

1)  getChildCount():获取子View的数量;
2)  getChildAt(i):获取第i个子View
3)  getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
4)  child.getLayoutParams():获取到子View的LayoutParams对象;
5)  child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
6)  getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
7)  child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。

7       其他方法

7.1    generateLayoutParams()

generateLayoutParams()方法用在自定义ViewGroup中,用来指明子控件之间的关系,即与当前的ViewGroup对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。

在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的属性集,系统根据这个ViewGroup的属性集来定义子View的布局规则,供子View使用。

例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在generateLayoutParams()方法中返回一个new MarginLayoutParams()即可。

7.2    onTouchEvent()

onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。

7.3    onScrollChanged()

如果我们的自定义View / ViewGroup是继承自ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。

这个方法中有四个参数:l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;oldl和oldt分别表示上次滑动到的点在水平和竖直方向上的坐标。我们可以通过这四个值对滑动进行处理,如添加属性动画等。

7.4    invalidate()

invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。

一般会引起invalidate()操作的函数如下:

1)  直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身;
2)  调用setSelection()方法,请求重新draw(),但只会绘制调用者本身;
3)  调用setVisibility()方法,会间接调用invalidate()方法,继而绘制该View;
4)  调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图,包括调用者本身。

7.5    postInvalidate()

功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。

7.6    requestLayout()

requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。

7.7    requestFocus()

请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。

8       总结

最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:

在这些方法中:

onMeasure()会在初始化之后调用一到多次来测量控件或其中的子控件的宽高;

onLayout()会在onMeasure()方法之后被调用一次,将控件或其子控件进行布局;

onDraw()会在onLayout()方法之后调用一次,也会在用户手指触摸屏幕时被调用多次,来绘制控件。

时间: 2024-10-14 11:13:32

【Android - 进阶】之自定义视图浅析的相关文章

Android进阶之自定义View实战(二)九宫格手势解锁实现

一.引言 在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法.作为自定义View的入门篇,仅仅介绍了Canvas的基本使用方法,而对用户交互层面仅仅处理了单击事件接口,在实际的业务中,常常涉及到手势操作,本篇博客以九宫格手势解锁View为例,来说明自定义View如何根据需求处理用户的手势操作.虽然九宫格手势解锁自定义View网上资料有很多,实现原理大同小异,但这里我只是根据自己觉得最优的思路来实现它,目的是让更

Android进阶:自定义视频播放器开发(下)

上一篇文章我们主要讲了视频播放器开发之前需要准备的一个知识,TextureView,用于对图像流的处理.这篇文章开始构建一个基础的视频播放器. 一.准备工作 在之前的文章已经说过了,播放器也是一个view,我们要在这个view上播放视频流.所以我们要自定义一个简单的viewgroup,比如继承FrameLayout.还出就是布局简单,其他控件可以往上面添加.大家见过的视频播放器的控制器都是放在视频的上方的.这样就是用FrameLayout布局是最好的. class SmallVideoPlaye

Android中的自定义视图控件

简介 当现有控件不能满足需求时,就需要自定义控件. 自定义控件属性 自定义控件首先要继承自View,重写两个构造函数. 第一个是代码中使用的: public MyRect(Context context) { super(context); } 另一个是资源解析程序使用的: public MyRect(Context context, AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyled

进阶篇-用户界面:7.android动画-自定义视图动画

1.透明度动画 源代码中实现透明度的变化动画: (1)在布局文件中添加一个button,id为btn. (2)MainActivity.java findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { AlphaAnimation aa = new AlphaAnimation(0,1); aa.setDuration(100

Android开发之自定义视图属性

Android框架中,所有自定义的view类都继承自View,也可以继承Button等view的子类 为了允许ADT能够与view交互,必须提供一个能够获取Context和作为属性的AttributeSet对象的构造函数.这个构造函数允许布局编辑器建立和编辑view的实例. 1 public class MyRect extends View { 2 3 public MyRect(Context context) { 4 super(context); 5 // TODO Auto-gener

Android进阶:自定义类加载器加载加密类文件

之前面试的时候有许多面试官问类加载器相关的问题,所以这是一个很重要的知识点.而且对于高级Android研发来讲,懂得更多类加载相关的东西,对开发也会有很多的帮助,比如热更新,类加密等.其实笔者对类加密比较感兴趣,就稍稍调研了一下.类加密的其实是为了防止APP被反编译,防止反编译的方法有很多种,比如混淆,加固等.自己对类文件进行加密,并自定义类加载器也是一种办法: 首先我们的代码打包编译之后会变成难以读懂的二进制字节码,并且变成.class文件.但是简单的APP编译出来之后可以被反编译,甚至你写的

Android进阶:自定义视频播放器开发(上)

随着快手,抖音,西瓜视频等视频APP的崛起,视频播放已经成为主流,此时作为Android研发的你,想要提高自己的能力还不知道怎么开发视频播放器怎么行?所以今天就带着大家一起开发一个简易播放器:SmallVideoPlayer 需求分析 我们观察一个视频播放器,可以看到视频播放器除了正在播放的视频还有很多控件,比如播放按钮,暂停按钮,播放进度条,播放计时器等.这么多控件显然无法播放视频,但是他们都在控制视频的播放.由此可见视频播放器可以分为两层,一层为视频播放器控制层,一层为真正的视频播放层. 所

Android中自定义视图View之---进阶篇(Canvas的使用)

更多技术内容请移步:我的个人博客 一.前言 今天是周日,昨天刚刚写完了一篇关于如何搭建LNMP环境,让自己可以DIY有个性的个人主页: http://blog.csdn.net/jiangwei0910410003/article/details/50929955 那么今天,我们继续来看一篇关于Android中的UI篇,如何自定义视图View的进阶篇,关于前奏篇之前已经写过了,还没有了解的同学可以去看看:http://blog.csdn.net/jiangwei0910410003/articl

进阶篇-用户界面:5.android绘图api自定义View(视图)

1.自定义视图并为其添加属性     我们平时用的Button啊 TextView啊都是安卓中系统自带的控件供开发者使用,但是,这些事远远不够的,有时候我们需要自定义控件. (1)新建一个类MyView使其继承View 类 import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; /** * C