Android自定义View使用详细分析与绘制流程全解

目录

目录.png

1. 自定义View基础

1.1 分类

自定义View的实现方式有以下几种

类型定义
自定义组合控件多个控件组合成为一个新的控件,方便多处复用
继承系统View控件继承自TextView等系统控件,在系统控件的基础功能上进行扩展
继承View不复用系统控件逻辑,继承View进行功能定义
继承系统ViewGroup继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展
继承ViewViewGroup不复用系统控件逻辑,继承ViewGroup进行功能定义

1.2 View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

1.3 坐标系

在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:

Android坐标系.png

除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。

视图坐标系.png

View获取自身高度

由上图可算出View的高度:

  • width = getRight() - getLeft();
  • height = getBottom() - getTop();

View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度和高度,其内部方法和上文所示是相同的,我们可以直接调用来获取View得宽高。

View自身的坐标

通过如下方法可以获取View到其父控件的距离。

getRawX:获取的是真实X坐标,相对于屏幕左上角
getRawY:获取的是真实Y坐标,相对于屏幕左上角
getLeft:获取的是View自身的左边距离父布局的左边的距离
getBottom:获取的是View自身的底部距离父布局的顶部的距离
getRight:获取的是View自身的右边距离父布局的左边的距离
getTop:获取的是View自身的顶部到父布局顶部的距离
getX:获取的是点击事件距离控件左边的距离
getY:获取的是点击事件距离控件顶边的距离
 

1.4 构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建TestView

 
  1. public class TestView extends View {

  2. /**

  3. * 在java代码里new的时候会用到

  4. * @param context

  5. */

  6. public TestView(Context context) {

  7. super(context);

  8. }

  9. /**

  10. * 在xml布局文件中使用时自动调用

  11. * @param context

  12. */

  13. public TestView(Context context, @Nullable AttributeSet attrs) {

  14. super(context, attrs);

  15. }

  16. /**

  17. * 不会自动调用,如果有默认style时,在第二个构造函数中调用

  18. * @param context

  19. * @param attrs

  20. * @param defStyleAttr

  21. */

  22. public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

  23. super(context, attrs, defStyleAttr);

  24. }

  25. /**

  26. * 只有在API版本>21时才会用到

  27. * 不会自动调用,如果有默认style时,在第二个构造函数中调用

  28. * @param context

  29. * @param attrs

  30. * @param defStyleAttr

  31. * @param defStyleRes

  32. */

  33. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)

  34. public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {

  35. super(context, attrs, defStyleAttr, defStyleRes);

  36. }

  37. }

1.5 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:

  1. 自定义一个View
  2. 编写values/attrs.xml,在其中编写styleable和item等标签元素
  3. 在布局文件中View使用自定义的属性(注意namespace)
  4. 在View的构造方法中通过TypedArray获取

实例说明

  • 自定义属性的声明文件

 
  1. <?xml version="1.0" encoding="utf-8"?>

  2. <resources>

  3. <declare-styleable name="test">

  4. <attr name="text" format="string" />

  5. <attr name="testAttr" format="integer" />

  6. </declare-styleable>

  7. </resources>

  • 自定义View类

 
  1. public class MyTextView extends View {

  2. private static final String TAG = MyTextView.class.getSimpleName();

  3. //在View的构造方法中通过TypedArray获取

  4. public MyTextView(Context context, AttributeSet attrs) {

  5. super(context, attrs);

  6. TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);

  7. String text = ta.getString(R.styleable.test_testAttr);

  8. int textAttr = ta.getInteger(R.styleable.test_text, -1);

  9. Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);

  10. ta.recycle();

  11. }

  12. }

  • 布局文件中使用

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

  2. xmlns:tools="http://schemas.android.com/tools"

  3. xmlns:app="http://schemas.android.com/apk/res/com.example.test"

  4. android:layout_width="match_parent"

  5. android:layout_height="match_parent" >

  6. <com.example.test.MyTextView

  7. android:layout_width="100dp"

  8. android:layout_height="200dp"

  9. app:testAttr="520"

  10. app:text="helloworld" />

  11. </RelativeLayout>

属性值的类型format

(1). reference:参考某一资源ID

  • 属性定义:

 
  1. <declare-styleable name = "名称">

  2. <attr name = "background" format = "reference" />

  3. </declare-styleable>

  • 属性使用:

<ImageView android:background = "@drawable/图片ID"/>

(2). color:颜色值

  • 属性定义:

<attr name = "textColor" format = "color" />
  • 属性使用:

<TextView android:textColor = "#00FF00" />

(3). boolean:布尔值

  • 属性定义:

<attr name = "focusable" format = "boolean" />
  • 属性使用:

<Button android:focusable = "true"/>

(4). dimension:尺寸值

  • 属性定义:

<attr name = "layout_width" format = "dimension" />
  • 属性使用:

 
  1. <Button android:layout_width = "42dip"/>

(5). float:浮点值

  • 属性定义:

<attr name = "fromAlpha" format = "float" />
  • 属性使用:

<alpha android:fromAlpha = "1.0"/>

(6). integer:整型值

  • 属性定义:

<attr name = "framesCount" format="integer" />
  • 属性使用:

<animated-rotate android:framesCount = "12"/>

(7). string:字符串

  • 属性定义:

<attr name = "text" format = "string" />
  • 属性使用:

<TextView android:text = "我是文本"/>

(8). fraction:百分数

  • 属性定义:

<attr name = "pivotX" format = "fraction" />
  • 属性使用:

<rotate android:pivotX = "200%"/>

(9). enum:枚举值

  • 属性定义:

 
  1. <declare-styleable name="名称">

  2. <attr name="orientation">

  3. <enum name="horizontal" value="0" />

  4. <enum name="vertical" value="1" />

  5. </attr>

  6. </declare-styleable>

  • 属性使用:

 
  1. <LinearLayout

  2. android:orientation = "vertical">

  3. </LinearLayout>

注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"

(10). flag:位或运算

  • 属性定义:

 
  1. <declare-styleable name="名称">

  2. <attr name="gravity">

  3. <flag name="top" value="0x01" />

  4. <flag name="bottom" value="0x02" />

  5. <flag name="left" value="0x04" />

  6. <flag name="right" value="0x08" />

  7. <flag name="center_vertical" value="0x16" />

  8. ...

  9. </attr>

  10. </declare-styleable>

  • 属性使用:

<TextView android:gravity="bottom|left"/>

注意:位运算类型的属性在使用的过程中可以使用多个值

(11). 混合类型:属性定义时可以指定多种类型值

  • 属性定义:

 
  1. <declare-styleable name = "名称">

  2. <attr name = "background" format = "reference|color" />

  3. </declare-styleable>

  • 属性使用:

 
  1. <ImageView

  2. android:background = "@drawable/图片ID" />

  3. 或者:

  4. <ImageView

  5. android:background = "#00FF00" />

android控件获取属性值的优先顺序:

1.在XMl文件中直接定义;

2.在XMl文件引用的style;

3.就是从如上所说的defStyleAttr中取值;

4.从defStyleRes取值;

5.从Activity或者Application的Theme中取值;

2. View绘制流程

这一章节偏向于解释View绘制的源码实现,可以更好地帮助我们掌握整个绘制过程。

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

2.1 Measure()

MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec = mode + size

MeasureSpec当中一共存在三种modeUNSPECIFIEDEXACTLY 和
AT_MOST

对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

使用方式

 
  1. // 获取测量模式(Mode)

  2. int specMode = MeasureSpec.getMode(measureSpec)

  3. // 获取测量大小(Size)

  4. int specSize = MeasureSpec.getSize(measureSpec)

  5. // 通过Mode 和 Size 生成新的SpecMode

  6. int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

在View当中,MeasureSpace的测量代码如下:

 
  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

  2. int specMode = MeasureSpec.getMode(spec);

  3. int specSize = MeasureSpec.getSize(spec);

  4. int size = Math.max(0, specSize - padding);

  5. int resultSize = 0;

  6. int resultMode = 0;

  7. switch (specMode) {

  8. //当父View要求一个精确值时,为子View赋值

  9. case MeasureSpec.EXACTLY:

  10. //如果子view有自己的尺寸,则使用自己的尺寸

  11. if (childDimension >= 0) {

  12. resultSize = childDimension;

  13. resultMode = MeasureSpec.EXACTLY;

  14. //当子View是match_parent,将父View的大小赋值给子View

  15. } else if (childDimension == LayoutParams.MATCH_PARENT) {

  16. resultSize = size;

  17. resultMode = MeasureSpec.EXACTLY;

  18. //如果子View是wrap_content,设置子View的最大尺寸为父View

  19. } else if (childDimension == LayoutParams.WRAP_CONTENT) {

  20. resultSize = size;

  21. resultMode = MeasureSpec.AT_MOST;

  22. }

  23. break;

  24. // 父布局给子View了一个最大界限

  25. case MeasureSpec.AT_MOST:

  26. if (childDimension >= 0) {

  27. //如果子view有自己的尺寸,则使用自己的尺寸

  28. resultSize = childDimension;

  29. resultMode = MeasureSpec.EXACTLY;

  30. } else if (childDimension == LayoutParams.MATCH_PARENT) {

  31. // 父View的尺寸为子View的最大尺寸

  32. resultSize = size;

  33. resultMode = MeasureSpec.AT_MOST;

  34. } else if (childDimension == LayoutParams.WRAP_CONTENT) {

  35. //父View的尺寸为子View的最大尺寸

  36. resultSize = size;

  37. resultMode = MeasureSpec.AT_MOST;

  38. }

  39. break;

  40. // 父布局对子View没有做任何限制

  41. case MeasureSpec.UNSPECIFIED:

  42. if (childDimension >= 0) {

  43. //如果子view有自己的尺寸,则使用自己的尺寸

  44. resultSize = childDimension;

  45. resultMode = MeasureSpec.EXACTLY;

  46. } else if (childDimension == LayoutParams.MATCH_PARENT) {

  47. //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0

  48. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

  49. resultMode = MeasureSpec.UNSPECIFIED;

  50. } else if (childDimension == LayoutParams.WRAP_CONTENT) {

  51. //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0

  52. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

  53. resultMode = MeasureSpec.UNSPECIFIED;

  54. }

  55. break;

  56. }

  57. return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

  58. }

这里需要注意,这段代码只是在为子View设置MeasureSpec参数而不是实际的设置子View的大小。子View的最终大小需要在View中具体设置。

从源码可以看出来,子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的。

在测量子View大小时:

父View mode子View
UNSPECIFIED父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0
EXACTLY父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围
AT_MOST父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围

onMeasure()

整个测量过程的入口位于Viewmeasure方法当中,该方法做了一些参数的初始化之后调用了onMeasure方法,这里我们主要分析onMeasure

onMeasure方法的源码如下:

 
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  2. setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

  3. getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

  4. }

很简单这里只有一行代码,涉及到了三个方法我们挨个分析。

  • setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高,在我们自定义View时也会经常用到。
  • getDefaultSize(int size, int measureSpec):该方法用来获取View默认的宽高,结合源码来看。

 
  1. /**

  2. * 有两个参数size和measureSpec

  3. * 1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。

  4. * 2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式

  5. */

  6. public static int getDefaultSize(int size, int measureSpec) {

  7. int result = size;

  8. int specMode = MeasureSpec.getMode(measureSpec);

  9. int specSize = MeasureSpec.getSize(measureSpec);

  10. //从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。

  11. switch (specMode) {

  12. case MeasureSpec.UNSPECIFIED:

  13. result = size;

  14. break;

  15. case MeasureSpec.AT_MOST:

  16. case MeasureSpec.EXACTLY:

  17. result = specSize;

  18. break;

  19. }

  20. return result;

  21. }

  • getSuggestedMinimumWidth():getHeight和该方法原理是一样的,这里只分析这一个。

 
  1. //当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0.

  2. //如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。

  3. protected int getSuggestedMinimumWidth() {

  4. return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

  5. }


ViewGroup的测量过程与View有一点点区别,其本身是继承自View,它没有对Viewmeasure方法以及onMeasure方法进行重写。

为什么没有重写onMeasure呢?ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,而不同的布局测量方式也都不同(可参考LinearLayout以及FrameLayout),所以没有办法统一设置。因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。

measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用Viewmeasure()方法,让子View测量自身大小。具体测量流程上面也以及介绍过了


measure过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下LinearLayoutonMeasure方法。

2.2 Layout()

要计算位置首先要对Android坐标系有所了解,前面的内容我们也有介绍过。

layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。

layout()方法是整个Layout()流程的入口,看一下这部分源码

 
  1. /**

  2. * 这里的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。

  3. *

  4. */

  5. public void layout(int l, int t, int r, int b) {

  6. if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {

  7. onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);

  8. mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

  9. }

  10. int oldL = mLeft;

  11. int oldT = mTop;

  12. int oldB = mBottom;

  13. int oldR = mRight;

  14. //这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。

  15. boolean changed = isLayoutModeOptical(mParent) ?

  16. setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

  17. //调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。

  18. if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

  19. onLayout(changed, l, t, r, b);

  20. }

  21. }

从源码我们知道,在layout()方法中已经通过setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。

有兴趣的可以看一下LinearLayoutonLayout源码,可以帮助加深理解。

2.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

通过各个步骤的源码再做分析:

 
  1. public void draw(Canvas canvas) {

  2. int saveCount;

  3. // 1. 如果需要,绘制背景

  4. if (!dirtyOpaque) {

  5. drawBackground(canvas);

  6. }

  7. // 2. 有过有必要,保存当前canvas。

  8. final int viewFlags = mViewFlags;

  9. if (!verticalEdges && !horizontalEdges) {

  10. // 3. 绘制View的内容。

  11. if (!dirtyOpaque) onDraw(canvas);

  12. // 4. 绘制子View。

  13. dispatchDraw(canvas);

  14. drawAutofilledHighlight(canvas);

  15. // Overlay is part of the content and draws beneath Foreground

  16. if (mOverlay != null && !mOverlay.isEmpty()) {

  17. mOverlay.getOverlayView().dispatchDraw(canvas);

  18. }

  19. // 6. 绘制装饰,如滚动条等等。

  20. onDrawForeground(canvas);

  21. // we're done...

  22. return;

  23. }

  24. }

  25. /**

  26. * 1.绘制View背景

  27. */

  28. private void drawBackground(Canvas canvas) {

  29. //获取背景

  30. final Drawable background = mBackground;

  31. if (background == null) {

  32. return;

  33. }

  34. setBackgroundBounds();

  35. //获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。

  36. final int scrollX = mScrollX;

  37. final int scrollY = mScrollY;

  38. if ((scrollX | scrollY) == 0) {

  39. background.draw(canvas);

  40. } else {

  41. canvas.translate(scrollX, scrollY);

  42. background.draw(canvas);

  43. canvas.translate(-scrollX, -scrollY);

  44. }

  45. }

  46. /**

  47. * 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。

  48. */

  49. protected void onDraw(Canvas canvas) {

  50. }

  51. /**

  52. * 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。

  53. * 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。

  54. */

  55. protected void dispatchDraw(Canvas canvas) {

  56. }

3. 自定义组合控件

自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以把他们组合成一个新的控件。

我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。

1. 编写布局文件

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

  2. android:layout_width="match_parent"

  3. android:id="@+id/header_root_layout"

  4. android:layout_height="45dp"

  5. android:background="#827192">

  6. <ImageView

  7. android:id="@+id/header_left_img"

  8. android:layout_width="45dp"

  9. android:layout_height="45dp"

  10. android:layout_alignParentLeft="true"

  11. android:paddingLeft="12dp"

  12. android:paddingRight="12dp"

  13. android:src="@drawable/back"

  14. android:scaleType="fitCenter"/>

  15. <TextView

  16. android:id="@+id/header_center_text"

  17. android:layout_width="wrap_content"

  18. android:layout_height="wrap_content"

  19. android:layout_centerInParent="true"

  20. android:lines="1"

  21. android:maxLines="11"

  22. android:ellipsize="end"

  23. android:text="title"

  24. android:textStyle="bold"

  25. android:textColor="#ffffff"/>

  26. <ImageView

  27. android:id="@+id/header_right_img"

  28. android:layout_width="45dp"

  29. android:layout_height="45dp"

  30. android:layout_alignParentRight="true"

  31. android:src="@drawable/add"

  32. android:scaleType="fitCenter"

  33. android:paddingRight="12dp"

  34. android:paddingLeft="12dp"/>

  35. </RelativeLayout>

布局很简单,中间是title的文字,左边是返回按钮,右边是一个添加按钮。

2. 实现构造方法

 
  1. //因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。

  2. //关于各个构造方法的介绍可以参考前面的内容

  3. public class YFHeaderView extends RelativeLayout {

  4. public YFHeaderView(Context context) {

  5. super(context);

  6. }

  7. public YFHeaderView(Context context, AttributeSet attrs) {

  8. super(context, attrs);

  9. }

  10. public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {

  11. super(context, attrs, defStyleAttr);

  12. }

  13. }

3. 初始化UI

 
  1. //初始化UI,可根据业务需求设置默认值。

  2. private void initView(Context context) {

  3. LayoutInflater.from(context).inflate(R.layout.view_header, this, true);

  4. img_left = (ImageView) findViewById(R.id.header_left_img);

  5. img_right = (ImageView) findViewById(R.id.header_right_img);

  6. text_center = (TextView) findViewById(R.id.header_center_text);

  7. layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);

  8. layout_root.setBackgroundColor(Color.BLACK);

  9. text_center.setTextColor(Color.WHITE);

  10. }

4. 提供对外的方法

可以根据业务需求对外暴露一些方法。

 
  1. //设置标题文字的方法

  2. private void setTitle(String title) {

  3. if (!TextUtils.isEmpty(title)) {

  4. text_center.setText(title);

  5. }

  6. }

  7. //对左边按钮设置事件的方法

  8. private void setLeftListener(OnClickListener onClickListener) {

  9. img_left.setOnClickListener(onClickListener);

  10. }

  11. //对右边按钮设置事件的方法

  12. private void setRightListener(OnClickListener onClickListener) {

  13. img_right.setOnClickListener(onClickListener);

  14. }

5. 在布局当中引用该控件

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

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

  3. android:orientation="vertical"

  4. android:layout_width="match_parent"

  5. android:layout_height="match_parent">

  6. <com.example.yf.view.YFHeaderView

  7. android:layout_width="match_parent"

  8. android:layout_height="45dp">

  9. </com.example.yf.view.YFHeaderView>

  10. </LinearLayout>

到这里基本的功能已经有了。除了这些基础功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时候就需要用到自定义属性来解决了。

前面已经简单介绍过自定义属性的相关知识,我们之间看代码

1.首先在values目录下创建attrs.xml

内容如下:

 
  1. <resources>

  2. <declare-styleable name="HeaderBar">

  3. <attr name="title_text_clolor" format="color"></attr>

  4. <attr name="title_text" format="string"></attr>

  5. <attr name="show_views">

  6. <flag name="left_text" value="0x01" />

  7. <flag name="left_img" value="0x02" />

  8. <flag name="right_text" value="0x04" />

  9. <flag name="right_img" value="0x08" />

  10. <flag name="center_text" value="0x10" />

  11. <flag name="center_img" value="0x20" />

  12. </attr>

  13. </declare-styleable>

  14. </resources>

这里我们定义了三个属性,文字内容、颜色以及要显示的元素。

2.在java代码中进行设置

 
  1. private void initAttrs(Context context, AttributeSet attrs) {

  2. TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);

  3. //获取title_text属性

  4. String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);

  5. if (!TextUtils.isEmpty(title)) {

  6. text_center.setText(title);

  7. }

  8. //获取show_views属性,如果没有设置时默认为0x26

  9. showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);

  10. text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));

  11. mTypedArray.recycle();

  12. showView(showView);

  13. }

  14. private void showView(int showView) {

  15. //将showView转换为二进制数,根据不同位置上的值设置对应View的显示或者隐藏。

  16. Long data = Long.valueOf(Integer.toBinaryString(showView));

  17. element = String.format("%06d", data);

  18. for (int i = 0; i < element.length(); i++) {

  19. if(i == 0) ;

  20. if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  21. if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  22. if(i == 3) ;

  23. if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  24. if(i == 5) ;

  25. }

  26. }

3.在布局文件中进行设置

 
  1. <com.example.yf.view.YFHeaderView

  2. android:layout_width="match_parent"

  3. android:layout_height="45dp"

  4. app:title_text="标题"

  5. app:show_views="center_text|left_img|right_img">

  6. </com.example.yf.view.YFHeaderView>

OK,到这里整个View基本定义完成。整个YFHeaderView的代码如下

 
  1. public class YFHeaderView extends RelativeLayout {

  2. private ImageView img_left;

  3. private TextView text_center;

  4. private ImageView img_right;

  5. private RelativeLayout layout_root;

  6. private Context context;

  7. String element;

  8. private int showView;

  9. public YFHeaderView(Context context) {

  10. super(context);

  11. this.context = context;

  12. initView(context);

  13. }

  14. public YFHeaderView(Context context, AttributeSet attrs) {

  15. super(context, attrs);

  16. this.context = context;

  17. initView(context);

  18. initAttrs(context, attrs);

  19. }

  20. public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {

  21. super(context, attrs, defStyleAttr);

  22. this.context = context;

  23. initView(context);

  24. initAttrs(context, attrs);

  25. }

  26. private void initAttrs(Context context, AttributeSet attrs) {

  27. TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);

  28. String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);

  29. if (!TextUtils.isEmpty(title)) {

  30. text_center.setText(title);

  31. }

  32. showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);

  33. text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));

  34. mTypedArray.recycle();

  35. showView(showView);

  36. }

  37. private void showView(int showView) {

  38. Long data = Long.valueOf(Integer.toBinaryString(showView));

  39. element = String.format("%06d", data);

  40. for (int i = 0; i < element.length(); i++) {

  41. if(i == 0) ;

  42. if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  43. if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  44. if(i == 3) ;

  45. if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);

  46. if(i == 5) ;

  47. }

  48. }

  49. private void initView(final Context context) {

  50. LayoutInflater.from(context).inflate(R.layout.view_header, this, true);

  51. img_left = (ImageView) findViewById(R.id.header_left_img);

  52. img_right = (ImageView) findViewById(R.id.header_right_img);

  53. text_center = (TextView) findViewById(R.id.header_center_text);

  54. layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);

  55. layout_root.setBackgroundColor(Color.BLACK);

  56. text_center.setTextColor(Color.WHITE);

  57. img_left.setOnClickListener(new OnClickListener() {

  58. @Override

  59. public void onClick(View view) {

  60. Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show();

  61. }

  62. });

  63. }

  64. private void setTitle(String title) {

  65. if (!TextUtils.isEmpty(title)) {

  66. text_center.setText(title);

  67. }

  68. }

  69. private void setLeftListener(OnClickListener onClickListener) {

  70. img_left.setOnClickListener(onClickListener);

  71. }

  72. private void setRightListener(OnClickListener onClickListener) {

  73. img_right.setOnClickListener(onClickListener);

  74. }

  75. }

4. 继承系统控件

继承系统的控件可以分为继承View子类(如TextVIew等)和继承ViewGroup子类(如LinearLayout等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

业务需求:为文字设置背景,并在布局中间添加一条横线。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的onMeaseuronLayout流程,所以我们只需要重写onDraw方法 。实现非常简单,话不多说,直接上代码。

 
  1. public class LineTextView extends TextView {

  2. //定义画笔,用来绘制中心曲线

  3. private Paint mPaint;

  4. /**

  5. * 创建构造方法

  6. * @param context

  7. */

  8. public LineTextView(Context context) {

  9. super(context);

  10. init();

  11. }

  12. public LineTextView(Context context, @Nullable AttributeSet attrs) {

  13. super(context, attrs);

  14. init();

  15. }

  16. public LineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

  17. super(context, attrs, defStyleAttr);

  18. init();

  19. }

  20. private void init() {

  21. mPaint = new Paint();

  22. mPaint.setColor(Color.BLACK);

  23. }

  24. //重写draw方法,绘制我们需要的中间线以及背景

  25. @Override

  26. protected void onDraw(Canvas canvas) {

  27. super.onDraw(canvas);

  28. int width = getWidth();

  29. int height = getHeight();

  30. mPaint.setColor(Color.BLUE);

  31. //绘制方形背景

  32. RectF rectF = new RectF(0,0,width,height);

  33. canvas.drawRect(rectF,mPaint);

  34. mPaint.setColor(Color.BLACK);

  35. //绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)

  36. canvas.drawLine(0,height/2,width,height/2,mPaint);

  37. }

  38. }

对于View的绘制还需要对Paint()canvas以及Path的使用有所了解,不清楚的可以稍微了解一下。

这里的实现比较简单,因为具体实现会与业务环境密切相关,这里只是做一个参考。

5. 直接继承View

直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写onDraw外还需要对onMeasure方法进行重写。

我们用自定义View来绘制一个正方形。

  • 首先定义构造方法,以及做一些初始化操作

 
  1. ublic class RectView extends View{

  2. //定义画笔

  3. private Paint mPaint = new Paint();

  4. /**

  5. * 实现构造方法

  6. * @param context

  7. */

  8. public RectView(Context context) {

  9. super(context);

  10. init();

  11. }

  12. public RectView(Context context, @Nullable AttributeSet attrs) {

  13. super(context, attrs);

  14. init();

  15. }

  16. public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

  17. super(context, attrs, defStyleAttr);

  18. init();

  19. }

  20. private void init() {

  21. mPaint.setColor(Color.BLUE);

  22. }

  23. }

  • 重写draw方法,绘制正方形,注意对padding属性进行设置

 
  1. /**

  2. * 重写draw方法

  3. * @param canvas

  4. */

  5. @Override

  6. protected void onDraw(Canvas canvas) {

  7. super.onDraw(canvas);

  8. //获取各个编剧的padding值

  9. int paddingLeft = getPaddingLeft();

  10. int paddingRight = getPaddingRight();

  11. int paddingTop = getPaddingTop();

  12. int paddingBottom = getPaddingBottom();

  13. //获取绘制的View的宽度

  14. int width = getWidth()-paddingLeft-paddingRight;

  15. //获取绘制的View的高度

  16. int height = getHeight()-paddingTop-paddingBottom;

  17. //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)

  18. canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);

  19. }

之前我们讲到过View的measure过程,再看一下源码对这一步的处理

 
  1. public static int getDefaultSize(int size, int measureSpec) {

  2. int result = size;

  3. int specMode = MeasureSpec.getMode(measureSpec);

  4. int specSize = MeasureSpec.getSize(measureSpec);

  5. switch (specMode) {

  6. case MeasureSpec.UNSPECIFIED:

  7. result = size;

  8. break;

  9. case MeasureSpec.AT_MOST:

  10. case MeasureSpec.EXACTLY:

  11. result = specSize;

  12. break;

  13. }

  14. return result;

  15. }

在View的源码当中并没有对AT_MOSTEXACTLY两个模式做出区分,也就是说View在wrap_contentmatch_parent两个模式下是完全相同的,都会是match_parent,显然这与我们平时用的View不同,所以我们要重写onMeasure方法。

  • 重写onMeasure方法

 
  1. /**

  2. * 重写onMeasure方法

  3. *

  4. * @param widthMeasureSpec

  5. * @param heightMeasureSpec

  6. */

  7. @Override

  8. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  9. super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  10. int widthSize = MeasureSpec.getSize(widthMeasureSpec);

  11. int widthMode = MeasureSpec.getMode(widthMeasureSpec);

  12. int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  13. int heightMode = MeasureSpec.getMode(heightMeasureSpec);

  14. //处理wrap_contentde情况

  15. if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

  16. setMeasuredDimension(300, 300);

  17. } else if (widthMode == MeasureSpec.AT_MOST) {

  18. setMeasuredDimension(300, heightSize);

  19. } else if (heightMode == MeasureSpec.AT_MOST) {

  20. setMeasuredDimension(widthSize, 300);

  21. }

  22. }

整个自定义View的代码如下:

 
  1. public class RectView extends View {

  2. //定义画笔

  3. private Paint mPaint = new Paint();

  4. /**

  5. * 实现构造方法

  6. *

  7. * @param context

  8. */

  9. public RectView(Context context) {

  10. super(context);

  11. init();

  12. }

  13. public RectView(Context context, @Nullable AttributeSet attrs) {

  14. super(context, attrs);

  15. init();

  16. }

  17. public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

  18. super(context, attrs, defStyleAttr);

  19. init();

  20. }

  21. private void init() {

  22. mPaint.setColor(Color.BLUE);

  23. }

  24. /**

  25. * 重写onMeasure方法

  26. *

  27. * @param widthMeasureSpec

  28. * @param heightMeasureSpec

  29. */

  30. @Override

  31. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  32. super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  33. int widthSize = MeasureSpec.getSize(widthMeasureSpec);

  34. int widthMode = MeasureSpec.getMode(widthMeasureSpec);

  35. int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  36. int heightMode = MeasureSpec.getMode(heightMeasureSpec);

  37. if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

  38. setMeasuredDimension(300, 300);

  39. } else if (widthMode == MeasureSpec.AT_MOST) {

  40. setMeasuredDimension(300, heightSize);

  41. } else if (heightMode == MeasureSpec.AT_MOST) {

  42. setMeasuredDimension(widthSize, 300);

  43. }

  44. }

  45. /**

  46. * 重写draw方法

  47. *

  48. * @param canvas

  49. */

  50. @Override

  51. protected void onDraw(Canvas canvas) {

  52. super.onDraw(canvas);

  53. //获取各个编剧的padding值

  54. int paddingLeft = getPaddingLeft();

  55. int paddingRight = getPaddingRight();

  56. int paddingTop = getPaddingTop();

  57. int paddingBottom = getPaddingBottom();

  58. //获取绘制的View的宽度

  59. int width = getWidth() - paddingLeft - paddingRight;

  60. //获取绘制的View的高度

  61. int height = getHeight() - paddingTop - paddingBottom;

  62. //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)

  63. canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);

  64. }

  65. }

整个过程大致如下,直接继承View时需要有几点注意:

1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。

6. 继承ViewGroup

自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。

需求实例

实现一个类似于Viewpager的可左右滑动的布局。

代码比较多,我们结合注释分析。

 
  1. public class HorizontaiView extends ViewGroup {

  2. private int lastX;

  3. private int lastY;

  4. private int currentIndex = 0;

  5. private int childWidth = 0;

  6. private Scroller scroller;

  7. private VelocityTracker tracker;

  8. /**

  9. * 1.创建View类,实现构造函数

  10. * 实现构造方法

  11. * @param context

  12. */

  13. public HorizontaiView(Context context) {

  14. super(context);

  15. init(context);

  16. }

  17. public HorizontaiView(Context context, AttributeSet attrs) {

  18. super(context, attrs);

  19. init(context);

  20. }

  21. public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {

  22. super(context, attrs, defStyleAttr);

  23. init(context);

  24. }

  25. private void init(Context context) {

  26. scroller = new Scroller(context);

  27. tracker = VelocityTracker.obtain();

  28. }

  29. /**

  30. * 2、根据自定义View的绘制流程,重写`onMeasure`方法,注意对wrap_content的处理

  31. * 重写onMeasure方法

  32. * @param widthMeasureSpec

  33. * @param heightMeasureSpec

  34. */

  35. @Override

  36. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  37. super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  38. //获取宽高的测量模式以及测量值

  39. int widthMode = MeasureSpec.getMode(widthMeasureSpec);

  40. int widthSize = MeasureSpec.getSize(widthMeasureSpec);

  41. int heightMode = MeasureSpec.getMode(heightMeasureSpec);

  42. int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  43. //测量所有子View

  44. measureChildren(widthMeasureSpec, heightMeasureSpec);

  45. //如果没有子View,则View大小为0,0

  46. if (getChildCount() == 0) {

  47. setMeasuredDimension(0, 0);

  48. } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {

  49. View childOne = getChildAt(0);

  50. int childWidth = childOne.getMeasuredWidth();

  51. int childHeight = childOne.getMeasuredHeight();

  52. //View的宽度=单个子View宽度*子View个数,View的高度=子View高度

  53. setMeasuredDimension(getChildCount() * childWidth, childHeight);

  54. } else if (widthMode == MeasureSpec.AT_MOST) {

  55. View childOne = getChildAt(0);

  56. int childWidth = childOne.getMeasuredWidth();

  57. //View的宽度=单个子View宽度*子View个数,View的高度=xml当中设置的高度

  58. setMeasuredDimension(getChildCount() * childWidth, heightSize);

  59. } else if (heightMode == MeasureSpec.AT_MOST) {

  60. View childOne = getChildAt(0);

  61. int childHeight = childOne.getMeasuredHeight();

  62. //View的宽度=xml当中设置的宽度,View的高度=子View高度

  63. setMeasuredDimension(widthSize, childHeight);

  64. }

  65. }

  66. /**

  67. * 3、接下来重写`onLayout`方法,对各个子View设置位置。

  68. * 设置子View位置

  69. * @param changed

  70. * @param l

  71. * @param t

  72. * @param r

  73. * @param b

  74. */

  75. @Override

  76. protected void onLayout(boolean changed, int l, int t, int r, int b) {

  77. int childCount = getChildCount();

  78. int left = 0;

  79. View child;

  80. for (int i = 0; i < childCount; i++) {

  81. child = getChildAt(i);

  82. if (child.getVisibility() != View.GONE) {

  83. childWidth = child.getMeasuredWidth();

  84. child.layout(left, 0, left + childWidth, child.getMeasuredHeight());

  85. left += childWidth;

  86. }

  87. }

  88. }

  89. }

到这里我们的View布局就已经基本结束了。但是要实现Viewpager的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义View的时候也是会经常用到的,不了解的可以参考之前的文章Android Touch事件分发超详细解析

 
  1. /**

  2. * 4、因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。

  3. * 重写onInterceptTouchEvent,对横向滑动事件进行拦截

  4. * @param event

  5. * @return

  6. */

  7. @Override

  8. public boolean onInterceptTouchEvent(MotionEvent event) {

  9. boolean intercrpt = false;

  10. //记录当前点击的坐标

  11. int x = (int) event.getX();

  12. int y = (int) event.getY();

  13. switch (event.getAction()) {

  14. case MotionEvent.ACTION_MOVE:

  15. int deltaX = x - lastX;

  16. int delatY = y - lastY;

  17. //当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截

  18. if (Math.abs(deltaX) > Math.abs(delatY)) {

  19. intercrpt = true;

  20. }

  21. break;

  22. }

  23. lastX = x;

  24. lastY = y;

  25. //intercrpt = true表示对事件进行拦截

  26. return intercrpt;

  27. }

  28. /**

  29. * 5、当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。

  30. * 重写onTouchEvent方法

  31. * @param event

  32. * @return

  33. */

  34. @Override

  35. public boolean onTouchEvent(MotionEvent event) {

  36. tracker.addMovement(event);

  37. //获取事件坐标(x,y)

  38. int x = (int) event.getX();

  39. int y = (int) event.getY();

  40. switch (event.getAction()) {

  41. case MotionEvent.ACTION_MOVE:

  42. int deltaX = x - lastX;

  43. int delatY = y - lastY;

  44. //scrollBy方法将对我们当前View的位置进行偏移

  45. scrollBy(-deltaX, 0);

  46. break;

  47. //当产生ACTION_UP事件时,也就是我们抬起手指

  48. case MotionEvent.ACTION_UP:

  49. //getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标

  50. //distance存储的就是此次滑动的距离

  51. int distance = getScrollX() - childWidth * currentIndex;

  52. //当本次滑动距离>View宽度的1/2时,切换View

  53. if (Math.abs(distance) > childWidth / 2) {

  54. if (distance > 0) {

  55. currentIndex++;

  56. } else {

  57. currentIndex--;

  58. }

  59. } else {

  60. //获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点

  61. tracker.computeCurrentVelocity(1000);

  62. float xV = tracker.getXVelocity();

  63. //当X轴加速度>50时,也就是产生了快速滑动,也会切换View

  64. if (Math.abs(xV) > 50) {

  65. if (xV < 0) {

  66. currentIndex++;

  67. } else {

  68. currentIndex--;

  69. }

  70. }

  71. }

  72. //对currentIndex做出限制其范围为【0,getChildCount() - 1】

  73. currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;

  74. //滑动到下一个View

  75. smoothScrollTo(currentIndex * childWidth, 0);

  76. tracker.clear();

  77. break;

  78. }

  79. lastX = x;

  80. lastY = y;

  81. return true;

  82. }

  83. private void smoothScrollTo(int destX, int destY) {

  84. //startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离

  85. scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);

  86. //invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法

  87. invalidate();

  88. }

  89. //重写computeScroll方法

  90. @Override

  91. public void computeScroll() {

  92. super.computeScroll();

  93. //当scroller.computeScrollOffset()=true时表示滑动没有结束

  94. if (scroller.computeScrollOffset()) {

  95. //调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置

  96. scrollTo(scroller.getCurrX(), scroller.getCurrY());

  97. //没有滑动结束,继续刷新View

  98. postInvalidate();

  99. }

  100. }

这部分代码比较多,为了方便阅读,在代码当中进行了注释。
之后就是在XML代码当中引入自定义View

 
  1. <com.example.yf.view.HorizontaiView

  2. android:id="@+id/test_layout"

  3. android:layout_width="match_parent"

  4. android:layout_height="400dp">

  5. <ListView

  6. android:id="@+id/list1"

  7. android:layout_width="match_parent"

  8. android:layout_height="match_parent">

  9. </ListView>

  10. <ListView

  11. android:id="@+id/list2"

  12. android:layout_width="match_parent"

  13. android:layout_height="match_parent">

  14. </ListView>

  15. <ListView

  16. android:id="@+id/list3"

  17. android:layout_width="match_parent"

  18. android:layout_height="match_parent">

  19. </ListView>

  20. </com.example.yf.view.HorizontaiView>

好了,可以运行看一下效果了。

总结

本篇文章对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。对各种实现方式写了简单的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值