Android -- 从源码的角度一步步打造自己的TextView
1,自定义控件一直是我们的痛点,今天就和大家一点点去了解了解,首先一般的自定义控件都是继承于View类,所以我们先来看看view的一些重要的方法,这是官方文档,大家想了解更多也可以去看看,这里我展示对我们常用到的,如下所示:
根据上面的方法,发现我们想绘制自己的view的话最简单的就是重写一下OnDraw()方法就行,今天和大家一起打造自己的Textview。
2,重写OnDraw()方法
创建一个MyTextView,继承自View类,这里我们要重写它的四个构造方法,一般重写前三个构造方法就行,它们的参数不一样分别对应不同的创建方式,比如只有一个Context参数的构造方法通常是通过代码初始化控件时使用;而两个参数的构造方法通常对应布局文件中控件被映射成对象时调用(需要解析属性);通常我们让这两个构造方法最终调用三个参数的构造方法,然后在第三个构造方法中进行一些初始化操作。
package com.qianmo.activitydetail; import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View; /**
* Created by wangjitao on 2017/3/20 0020.
* E-Mail:543441727@qq.com
*/ public class MyTextView extends View {
private final static String TAG = "MyTextView"; //文字
private String mText; //文字的颜色
private int mTextColor; //文字的大小
private int mTextSize; //绘制的范围
private Rect mBound;
private Paint mPaint; public MyTextView(Context context) {
this(context, null);
} public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
} /**
* 初始化数据
*/
private void init() {
//初始化数据
mText = " I Love You !";
mTextColor = Color.RED;
mTextSize = 30; //初始化Paint数据
mPaint = new Paint();
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize); //获取绘制的宽高
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
Log.i(TAG, "Left :" + mBound.left + ",Right:" + mBound.right + ",Top:" + mBound.top + ",Bottom:" + mBound.bottom);
} @Override
protected void onDraw(Canvas canvas) {
//绘制文字
canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
//注意一下我们这里的getWidth()和getHeight()是获取的px
Log.i(TAG, "getWidth():" + getWidth() + ",getHeight(): " + getHeight());
}
}
布局文件
<?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:myview="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
> <com.qianmo.activitydetail.MyTextView
android:layout_width="200dp"
android:layout_height="100dp"
/> </LinearLayout>
运行效果如下:
注意一下,这里我们一直有一个疑问一下我们在onDraw()方法中得到的宽度和高度的单位是什么呢?是px还是sp还dp?让我们看一看源码子解释的,这时候我们看一下源码
源码高速我们返回的是px像素,那么我们在xml文件中设置的宽度是200dp怎么转换成我们的px的呢?
举例:HVGA屏320*480,一般是3.5寸,计算点密度为√ (320^2 + 480^2) / 3.5 = 164,约等于160,1pd=1px,WVGA屏480*800,按3.8寸屏算,点密度 √ (480^2 + 800^2) / 3.8 = 245,约等于240,1dp=1.5px。
PPI = √(长度像素数² + 宽度像素数²) / 屏幕对角线英寸数
dp:Density-independent pixels,以160PPI屏幕为标准,则1dp=1px,
dp和px的换算公式 :
dp*ppi/160 = px。比如1dp x 320ppi/160 = 2px。
我们这个模拟器是1920*1080分辨率尺寸是5.2 按照上面的公式计算,我们机型的换算为大约得到 1dp = 2.75px
ok,题外话扯太多了
上面的代码我只是简单的写了下onDraw()方法,那我们现在有这一种需求,所写入的文字、文字颜色、文字大小等属性是可以有开发者动态控制的,那这时候我们该怎么办呢?这时候就要使用到我们的自定义属性了
3,自定义属性
在res/values文件下创建attrs文件,在文件下添加我们需要的自定义属性,format是这个属性的输入格式 ,具体代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyTextView2">
<attr name="myText" format="string"/>
<attr name="myTextColor" format="color"/>
<attr name="myTextSize" format="dimension"/>
</declare-styleable>
</resources>
这时候我们在布局文件中想要使用我们的自定义属性需要在根父容器加上自己命名空间 如:xmlns:myview="http://schemas.android.com/apk/res-auto" ,前面的myview名字可以自己改变,再看一下我们在布局文件中怎么动态的设置我们的属性的
<com.qianmo.activitydetail.MyTextView2
android:layout_width="200dp"
android:layout_height="100dp"
android:background="#00ff00"
myview:myText=" I Love You ......"
myview:myTextColor="#ff3399"
myview:myTextSize="25sp"
/>
这时候在构造函数中去获取我们的自定义属性
public MyTextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
} /**
* 初始化数据
*/
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
//获取自定义属性的值
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView2, defStyleAttr, 0);
mText = a.getString(R.styleable.MyTextView2_myText);
mTextColor = a.getColor(R.styleable.MyTextView2_myTextColor, Color.BLACK);
mTextSize = a.getDimension(R.styleable.MyTextView2_myTextSize, 30f);
a.recycle(); //初始化Paint数据
mPaint = new Paint();
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize); //获取绘制的宽高
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
Log.i(TAG, "Left :" + mBound.left + ",Right:" + mBound.right + ",Top:" + mBound.top + ",Bottom:" + mBound.bottom);
}
看一下我们的运行效果
可以看到我们的效果出来了,可以由开发者自定义,现在我们将myText属性修改为“I Love You ...... I Love You ...... ”
发现不文明现象,那我们试着按照我们的正常思维,将宽度和高度设置为wrap_content,看看效果
感觉一脸的懵逼,我是宽度匹配内容物宽度,为什么会占用满屏???既然出现了问题我们要找找错误的原因,我们看看上面展示的官方文档,既然是大小出问题了,那就看看我们的onMeasure()源码
4,onMeasure方法
在学习onMeasure方法之前我们来了解MeasuerSpec这个类,点进入源码,发现他是个尺寸和测量模式的集合,用来描述父控件对子控件的约束,接下来我们来看看部分源码
/**MeasureSpec 封装了父控件对其孩子的布局要求
有大小和模式两种,而模式则有三种模式
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; //0
//父控件给子控件一个精确的值
public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824
//父控件给子控件竟可能最大的值
public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648
//设定尺寸和模式创建的统一约束规范
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 从规范中获取模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//从规范中获取尺寸
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
这里我们重写onMeasure方法,在方法里面打印一下我们的在xml文件中的布局对应的MeasuerSpec的模式,重写的onMeasure()代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 Log.i(TAG, "widthMode:" + widthMode);
Log.i(TAG, "heightMode:" + heightMode);
Log.i(TAG, "widthSize:" + widthSize);
Log.i(TAG, "heightSize:" + heightSize);
}
场景一:设置MyView的宽高为wrap_content
<com.qianmo.activitydetail.MyTextView2
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00ff00"
myview:myText=" I Love You ......I Love You ......"
myview:myTextColor="#ff3399"
myview:myTextSize="25sp"
/>
看一下我们的输出结果
03-20 05:09:20.020 15255-15255/? I/MyTextView: widthMode:-2147483648
03-20 05:09:20.020 15255-15255/? I/MyTextView: heightMode:-2147483648
03-20 05:09:20.020 15255-15255/? I/MyTextView: widthSize:1080
03-20 05:09:20.020 15255-15255/? I/MyTextView: heightSize:1731
场景二:设置MyView的宽高为match_parent
<com.qianmo.activitydetail.MyTextView2
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00"
myview:myText=" I Love You ......I Love You ......"
myview:myTextColor="#ff3399"
myview:myTextSize="25sp"
/>
看一下输出结果
03-20 05:08:17.041 14272-14272/? I/MyTextView: widthMode:1073741824
03-20 05:08:17.041 14272-14272/? I/MyTextView: heightMode:1073741824
03-20 05:08:17.041 14272-14272/? I/MyTextView: widthSize:1079
03-20 05:08:17.041 14272-14272/? I/MyTextView: heightSize:1541
ok,这样我们可以总结出了我们以前得到的一些结果,如下表所示
那么我们现在有一个疑问了,为什么我们的wrap_content会占用的是整个屏幕的宽度和高度,这时候我们就要看一下我们OnMeasure()源码中是怎么处理的
View的源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
} public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看到我们的源码中调用是自身的getDefaultSize()方法,然后在MeasureSpec.AT_MOST和MeasureSpec.EXACTLY全部返回的是specSize,而specSize表示的是父控件剩余宽度,也就是我们看到的全屏。所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
4,重写onMeasuer()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 Log.i(TAG, "widthMode:" + widthMode);
Log.i(TAG, "heightMode:" + heightMode);
Log.i(TAG, "widthSize:" + widthSize);
Log.i(TAG, "heightSize:" + heightSize); //下面对wrap_content这种模式进行处理
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//如果是wrap_content,我们要得到控件需要多大的尺寸
//首先丈量文本的宽度
float textWidth = mBound.width();
//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
}
if (heightMode == MeasureSpec.EXACTLY) {
height = widthSize;
} else {
//如果是wrap_content,我们要得到控件需要多大的尺寸
//首先丈量文本的宽度
float textHeight = mBound.height();
//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
} //保存丈量结果
setMeasuredDimension(width, height);
}
看一下我们实现的效果,这样我们的内容就全部展示出来了
现在我们继续该我们的需求,将myText属性变成“I Love You ...... I Love You ...... I Love You ...... ” 看一下我们的效果,貌似没把后面的内容展示完全,这时候我们想实现它自动换行
5,实现TextView的自动换行
先说一下我们的思路,这里说一下我们打算先计算我们要展示的textview需要花费多少行展示,然后再将里面展示的内容切割,放到数组里面,在用过onDraw绘制文字,这种只是一种实现方式,而且感觉性能不太好,大家有时间的话可以去看一下TextView的源码,看一些官方是怎么试下的,下面看一下我们切割text内容的代码
package com.qianmo.activitydetail; import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.TextView; import java.util.ArrayList; /**
* Created by wangjitao on 2017/3/20 0020.
* E-Mail:543441727@qq.com
*/ public class MyTextView4 extends View {
private final static String TAG = "MyTextView"; //文字
private String mText;
private ArrayList<String> mTextList; //文字的颜色
private int mTextColor; //文字的大小
private float mTextSize; //绘制的范围
private Rect mBound;
private Paint mPaint; public MyTextView4(Context context) {
this(context, null);
} public MyTextView4(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public MyTextView4(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
} /**
* 初始化数据
*/
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
//获取自定义属性的值
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView2, defStyleAttr, 0);
mText = a.getString(R.styleable.MyTextView2_myText);
mTextColor = a.getColor(R.styleable.MyTextView2_myTextColor, Color.BLACK);
mTextSize = a.getDimension(R.styleable.MyTextView2_myTextSize, 30f);
a.recycle(); //初始化Paint数据
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize); //获取绘制的宽高
mBound = new Rect();
mTextList = new ArrayList<>();
mPaint.getTextBounds(mText, 0, mText.length(), mBound); } @Override
protected void onDraw(Canvas canvas) {
//绘制文字
for (int i = 0; i < mTextList.size(); i++) {
mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound);
Log.v(TAG, "在X:" + (getWidth() / 2 - mBound.width() / 2) + " Y:" + (getPaddingTop() + (mBound.height() * i)) + " 绘制:" + mTextList.get(i));
canvas.drawText(mTextList.get(i), (getWidth() / 2 - mBound.width() / 2), (getPaddingTop() + (mBound.height() * (i + 1))), mPaint);
Log.i(TAG, "getWidth() :" + getWidth() + ", mBound.width():" + mBound.width() + ",getHeight:" + getHeight() + ",mBound.height() *i:" + mBound.height() * i);
}
} boolean isOneLine = true;
float lineNum;
float splineNum; @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 Log.i(TAG, "widthMode:" + widthMode);
Log.i(TAG, "heightMode:" + heightMode);
Log.i(TAG, "widthSize:" + widthSize);
Log.i(TAG, "heightSize:" + heightSize); float textWidth = mBound.width(); //文本的宽度
if (mTextList.size() == 0) {
//将文本分段
int padding = getPaddingLeft() + getPaddingRight();
int specWidth = widthSize - padding; //剩余能够显示文本的最宽度
if (textWidth <= specWidth) {
//可以显示一行
lineNum = 1;
mTextList.add(mText);
} else {
//超过一行
isOneLine = false;
splineNum = textWidth / specWidth;
//如果有小数的话则进1
if ((splineNum + "").contains(".")) {
lineNum = (int) (splineNum + 0.5);
// lineNum = Integer.parseInt((splineNum + "").substring(0, (splineNum + "").indexOf("."))) + 1;
} else {
lineNum = splineNum;
}
int lineLength = (int) (mText.length() / splineNum);
for (int i = 0; i < lineNum; i++) {
String lineStr;
//判断是否可以一行展示
if (mText.length() < lineLength) {
lineStr = mText.substring(0, mText.length());
} else {
lineStr = mText.substring(0, lineLength);
}
mTextList.add(lineStr);
if (!TextUtils.isEmpty(mText)) {
if (mText.length() < lineLength) {
mText = mText.substring(0, mText.length());
} else {
mText = mText.substring(lineLength, mText.length());
}
} else {
break;
}
} }
} //下面对wrap_content这种模式进行处理
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//如果是wrap_content,我们要得到控件需要多大的尺寸
if (isOneLine) {
//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
} else {
//如果是多行,说明控件宽度应该填充父窗体
width = widthSize;
} }
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//如果是wrap_content,我们要得到控件需要多大的尺寸
//首先丈量文本的宽度
float textHeight = mBound.height();
if (isOneLine) {
//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
} else {
//如果是多行
height = (int) (getPaddingTop() + textHeight * lineNum + getPaddingBottom());
}
} //保存丈量结果
setMeasuredDimension(width, height);
}
}
效果如下:
ok,自动换行实现了,下面的是Github源码地址,有需要的同学可以下下来看看。See You Next Time ····
Android -- 从源码的角度一步步打造自己的TextView的更多相关文章
- Android -- ViewGroup源码分析+自定义
1,我们前三篇博客了解了一下自定义View的基本方法和流程 从源码的角度一步步打造自己的TextView 深入了解自定义属性 onMeasure()源码分析 之前,我们只是学习过自定义View,其实自 ...
- Android AsyncTask完全解析,带你从源码的角度彻底理解
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进 ...
- [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...
- 【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中Vi ...
- [学习总结]7、Android AsyncTask完全解析,带你从源码的角度彻底理解
我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制,感兴 ...
- Android fragment源码全解析
Fragment 相信基本上每个android developer都用过,但是知晓其原理 用的好的还是不多,今天就从源码的角度上来带着大家分析一下Fragment的源码,对fragment有了更深层次 ...
- Android 如何在Eclipse中查看Android API源码 及 support包源码
当我们阅读android API开发文档时候,上面的每个类,以及类的各个方法都是已经写好的方法和控件,可是我们只是在搬来使用,不知道它的原理,它是如何被实现的.android系统是开源的,所以谷歌官方 ...
- 从源码的角度分析ViewGruop的事件分发
从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...
- 从源码的角度解析View的事件分发
有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图 ...
随机推荐
- Java 获取字符串Hash值
Java 生成字符串的Hash值: /** * A hashing method that changes a string (like a URL) into a hash suitable for ...
- centos 7 安装MySQL 5.6
本文根据mysql的官方文档操作:https://dev.mysql.com/doc/mysql-yum-repo-quick-guide/en/ 由于Centos7 默认数据库是mariabd(网上 ...
- JQuery获取Dom元素的方法
(function (window) { var arr = []; var VP = function (selector, context) { return new VP.fn.init(sel ...
- Linux - iostat命令详解
简介 iostat可以提供更丰富的IO性能状态数据,iostat命令有两个用途: 输出CPU的统计信息 输出设备和分区的I/O统计信息 命令语法及参数说明 语法: iostat [ -c | -d ] ...
- JavaWeb 后端 <七> 之 mvc3层架构
- Excel多表合并的宏
Sub 合并当前目录下所有工作簿的全部工作表() Dim MyPath, MyName, AWbName Dim Wb As Workbook, WbN As String Dim G As Long ...
- MAC本上appium连接真机
简单介绍一下appium连接ios真机测试环境的软件安装及配置过程: 目前我用的是desktop版本的appium, 所以MAC版本必须要升级到10.12以上,Xcode版本必须要在8.0以上,否则亲 ...
- Android开发相关操作
命令行启动DDMS工具,前提是有这个工具 ~/rustsoftware/adt-bundle-linux-x86_64-20140702/sdk/tools$ ./ddms 查看机器内存情况: adb ...
- Android - 读取文件存储的数据
存取手机中的文件数据. 写入和读取的操作格式均为UTF-8. import java.io.File; import java.io.FileInputStream; import java.io.F ...
- JDBC之组件封装
本文所需架包:mysql-connector-java-5.1.7-bin.jar(连接MySQL数据库需要),ojdbc6.jar(连接Oracle数据库需要) 1.JDBC工具类(JDBCUtil ...