自己绘制控件

自定义View的步骤

  1. 布局
    • 自定义View的属性
    • XML或Java代码控制布局
      • 测量——onMeasure(int,int)(该函数可重写可不重写,具体看需求)
      • 布局——onLayout(boolean,int,int,int,int)
      • 绘制——onDraw(Canvas canvas)
  2. 处理逻辑
    • 构造函数(获得View属性值,初始化等)
    • 自定义处理逻辑
    • 事件响应(onClickonScroll等)
  3. 提供数据(数据和处理逻辑分开,Adapter)
  4. 使用
    • layout
    • Activity

其他自定义控件的介绍可参见

自定义控件简介

1. 布局

1.1 自定义View的属性

1.1.1 定义公共属性

res/values下面新建attrs.xml属性文件

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--name 是自定义属性名,一般采用驼峰命名,可以随意。 format 是属性的单位-->
<attr name="titleSize" format="dimension"></attr>
<attr name="titleText" format="string"></attr>
<attr name="titleColor" format="color"></attr>
<attr name="titleBackgroundColor" format="color"></attr>
...
</resources>

第一部分是公共的属性
format字段后面的属性单位AS开发的话IDE会自动有提示,基本包括如下:

  • dimension(字体大小)
  • string(字符串)
  • color(颜色)
  • boolean(布尔类型)
  • float(浮点型)
  • integer(整型)
  • enmu(枚举)
  • fraction(百分比)等

1.1.2 定义控件的主题样式

attrs.xml属性文件

1
2
3
4
5
6
7
8
9
10
<resources>
...
<!--name 是自定义控件的类名-->
<declare-styleable name="MyCustomView">
<attr name="titleSize"></attr>
<attr name="titleText"></attr>
<attr name="titleColor"></attr>
<attr name="titleBackgroundColor"></attr>
</declare-styleable>
</resources>

第二部分是自定义控件MyCustomView的主题样式。公共属性可以被多个自定义控件主题样式使用。

1.1.3 获得View的属性值

一般在构造函数中去获得View的属性值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MyCustomView extends View {
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
...
final Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs,
R.styleable.MyCustomView, defStyleAttr, 0);
if (null != a) {
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyCustomView_titleColor:
titleColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.MyCustomView_titleSize:
titleSize = a.getDimensionPixelSize(attr, titleSize);
break;
case R.styleable.MyCustomView_titleText:
titleText = a.getString(attr);
break;
case R.styleable.MyCustomView_titleBackgroundColor:
titleBackgroundColor = a.getColor(attr, Color.WHITE);
break;
}
}
a.recycle();
}
...
}
...
}
  • 第一步通过theme.obtainStyledAttributes()方法获得自定义控件的主题样式数组。
  • 第二步就是遍历每个属性来获得对应属性的值,也就是我们在xml布局文件中写的属性值。

    注意:在分支caseR.styleable.后面的属性名称有一个规则:控件的样式主题名 +_+ 属性名

  • 循环结束之后记得调用a.recycle()回收资源。
  • 至此就获得了自定义控件的属性值,这些属性值可以在onDraw函数中用来绘制View

1.2 XML或Java代码控制布局

XML布局

1
2
android:layout_width="match_parent"
android:background="#CCCCCCCC"

或Java代码控制布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private LinearLayout linearLayout;
private LinearLayout.LayoutParams linearLayoutParams;
private TextView textV;

@Override
protected void onCreate(Bundle savedInstanceState) {
...
linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
setContentView(linearLayout);
linearLayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

textV = new TextView(this);
textV.setTextSize(30.0f);
textV.setTextColor(Color.BLACK);
linearLayout.addView(textV, linearLayoutParams);
...
}

真正起效通过以下三个函数实现

  • 测量——onMeasure(int,int)(该函数可重写可不重写,具体看需求)
  • 布局——onLayout(boolean,int,int,int,int)
  • 绘制——onDraw(Canvas canvas)
    自定义控件可以直接通过重写这三个函数实现,下面将详细分析这三个函数

1.2.1 测量——onMeasure()

  1. 简介

onMeasure函数(default):

View.java

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
  • 该函数比onDraw先执行,由子控件重写,决定子控件View的大小(宽高值)。它在调用Measure()时被调用。
  • 父控件将自己的大小和mode,通过MeasureSpec传给子控件。

    • MeasureSpec由大小和模式组成,封装了父布局传递给子布局的布局要求
    • 每个MeasureSpec代表了一组宽度和高度的要求
    • MeasureSpec的三种模式:
      • UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
      • EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
      • AT_MOST(至多),子元素至多达到指定大小的值。
    • MeasureSpec定义于View.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
         public static class MeasureSpec {
      private static final int MODE_SHIFT = 30;
      private static final int MODE_MASK = 0x3 << MODE_SHIFT;

      public static final int UNSPECIFIED = 0 << MODE_SHIFT;
      public static final int EXACTLY = 1 << MODE_SHIFT;
      public static final int AT_MOST = 2 << MODE_SHIFT;

      public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
      if (sUseBrokenMakeMeasureSpec) {
      return size + mode;
      } else {
      return (size & ~MODE_MASK) | (mode & MODE_MASK);
      }
      }
      ...
      }
  • onMeasure函数常用的三个函数:

    • static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一)
    • static int getSize(int measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
    • static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(格式)
  • 子控件根据layout文件中父控件,子控件的"MATCH_PARENT""WRAP_CONTENT"指定值padding内边距margin外边距等等对于View大小的约束,计算宽高值(未必是最终大小)。
  • 计算出实际的高和宽通过setMeasuredDimension()保存,如果所测的视图是ViewGroup通过measureChild方法递归的计算其中的每一个子View
  1. setMeasuredDimension函数:

setMeasuredDimension方法必须由onMeasure(int, int)来调用,来存储测量的宽,高值,定义如下:

1
2
3
4
5
6
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

  1. 多个View或者ViewGroup嵌套:

子控件如果有多个View或者ViewGroup嵌套,需要循环遍历视图中所有的View

1
2
3
4
5
6
7
8
9
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//调用ViewGroup类中测量子类的方法
measureChildren(widthMeasureSpec, heightMeasureSpec);
//调用View类中默认的测量方法
super.onMeasure(widthMeasureSpec,heightMeasureSpec);

}
  • measureChildren函数:

    ViewGroup.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 遍历所有的子view去测量自己(跳过GONE类型View)
    * @param widthMeasureSpec 父视图的宽详细测量值
    * @param heightMeasureSpec 父视图的高详细测量值
    */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
    final View child = children[i];
    if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    }
    }
  • measureChild函数:

    ViewGroup.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    protected void measureChild(View child, int parentWidthMeasureSpec,
    int parentHeightMeasureSpec) {
    // 取得子视图的布局参数
    final LayoutParams lp = child.getLayoutParams();

    // 通过getChildMeasureSpec获取最终的宽高详细测量值
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
    mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom, lp.height);

    // 将计算好的宽高详细测量值传入measure方法,完成最后的测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

1.2.2 布局——onLayout()

View.java

1
2
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

  • public void layout(int l, int t, int r, int b)View.java)调用
  • 递归计算整个View树实际的位置,来实现该控件内部子控件的布局情况
  • 为抽象方法,所以在继承View时必须要重写该方法(onMeasure不需要),并调用其所有子Viewlayout函数
  • 参数介绍
    • changed 当前View的大小和位置改变了
    • left 左部位置(相对于父视图)
    • top 顶部位置(相对于父视图)
    • right 右部位置(相对于父视图)
    • bottom 底部位置(相对于父视图)

×分析至layout函数×
public void layout(int l, int t, int r, int b)
用于当前ViewGroup中的子控件的布局

1.2.3 绘制——onDraw()

如何绘制这个View。

自绘控件的内容都是自己绘制出来的,在View的onDraw方法中完成绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(titleSize);
/**
* 得到自定义ViewtitleText内容的宽和高
*/
mBound = new Rect();
mPaint.getTextBounds(titleText, 0, titleText.length(), mBound);
}

@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(titleBackgroundColor);
canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint);
mPaint.setColor(titleColor);
canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}

2. 处理逻辑

2.1 构造函数

自定义View一般需要实现以下几个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public MyCustomView(Context context) {
this(context, null);
}

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

public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

2.2 自定义处理逻辑

2.3 事件响应(onClickonScroll等)

3. 提供数据(数据和处理逻辑分开,Adapter)

4. 使用自定义View

4.1 layout

activity_main.xml布局中使用自定义View(自定义的属性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.customview.MyCustomView
android:layout_width="wrap_content"
android:layout_height="match_parent"
custom:titleColor="@android:color/black"
custom:titleSize="25sp"
custom:titleBackgroundColor="#ff0000"
custom:titleText="自定义的View" />
</LinearLayout>

4.2 Activity

5. 参考文献

Android自定义控件的三种实现方式
Android自定义控件View(一)
自定义控件详解(五):onMeasure()、onLayout()
Android 自定义 view(四)—— onMeasure 方法理解
ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
ANDROID自定义视图——onLayout源码 流程 思路详解