Android 自定义View实战
上面的这些Canvas方法固然已经很强大了,但是我们如果想要绘制一些不规则的图形怎么办,这时候就要用到强大的drawPath()方法了,通过对Path进行设置不同的坐标、添加不同图形,最后传入drawPath方法中可以绘制出复杂的且不规则的形状。Canvas除了可以绘制图形之外,还可以绘制文字,Canvas的绘制文字的方法有drawText()、drawTextOnPath()、drawTextR
文章目录
一、Canvas 画布
为了学习和实践相结合,先来做一些准备工作,创建一个自定义View框架,先初始化一下Paint画笔,并设置相关方法:
public class StudyView extends View {
private Paint mPaint;
private Context mContext;
public StudyView(Context context) {
super(context);
init(context);
}
public StudyView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mPaint = new Paint();
// 设置抗锯齿,即绘制时,会根据像素点进行优化,使绘制的图像更平滑
mPaint.setAntiAlias(true);
// 设置画笔笔尖的宽度
mPaint.setStrokeWidth(5);
// 设置画笔的样式,画笔的样式为空心,即不填充
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
1.1 绘制圆弧和扇形
Canvas提供drawArc()方法,通过传递不同的参数可用来绘制圆弧和扇形,此方法有两个重载方法,详细参数如下:
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {}
- left:扇形或圆弧所占区域的左边界线x坐标
- top:扇形或圆弧所占区域的上边界线y坐标
- right:右边界线x坐标
- bottom:下边界线y坐标
- startAngle:扇形或圆弧的起始角度
- sweepAngle:扫过的角度
- userCenter:此参数可以理解为true就是画扇形,false就是画圆弧
- paint:画笔
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint) {}
- RectF oval:RectF类,也是边界,就是把一个方法的left,top,right,bottom封装到了RectF类中,剩余参数与上一个方法一致。
接下来用着两个重载方法分别绘制两个90°的扇形和两个90°的圆弧:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制一个扇形
canvas.drawArc(0, 0, 200, 200, 0, 90, true, mPaint);
RectF rectF = new RectF(0, 0, 200, 200);
canvas.drawArc(rectF, 180, 90, true, mPaint);
// 绘制一个圆弧
canvas.drawArc(300, 0, 500, 200, 0, 90, false, mPaint);
RectF rectF1 = new RectF(300, 0, 500, 200);
canvas.drawArc(rectF1, 180, 90, false, mPaint);
}
效果图如下:
1.2 绘制 Bitmap
在Canvas中提供了drawBitmap方法,此方法可以让我们直接获取一张图片绘制到画布上,有了它可以让我们的自定义View锦上添花,同时也让我们实现一些复杂效果有了一个更加方便的途径。下面是drawBitmap的几个比较常用的重载方法:
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint) {}
- bitmap:Bitmap资源文件
- left和top:代表了图片左上角落入的位置坐标。
- top:看2
- paint:画笔
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
@Nullable Paint paint) {}
- src:在Bitmap图片上截取一部分作为绘制源,可null
- det:将绘制目标拉伸平铺到det指定的矩形中
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint) {}
同第二个重载方法,几乎一毛一样。
/**
* 使用指定的矩阵绘制位图。
*
* @param bitmap 要绘制的位图
* @param matrix 绘制位图时用于变换位图的矩阵
* @param paint 可能为 null。用于绘制位图的绘制
*/
public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint) {
super.drawBitmap(bitmap, matrix, paint);
}
matrix:Matrix的参数传入是的drawBitmap功能变得异常强大,通过matrix可以实现图片的平移(postTranslate())、缩放postScale())、旋转(postRotate())、错切(postSkew())等等花式炫酷效果。
待补充:探索 Matrix
在 onDraw 方法中 drawBitmap 的以上重载方法,注意在使用完 Bitmap 之后记得用Bitmap.recycle() 来回收掉资源,以防止 oom。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制图片
canvas.drawBitmap(bitmap, 0, 300, null);
// 将图片拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, null, new RectF(200, 300, 500, 500), null);
// 截取图片的四分之一拉伸平铺在RectF矩形内
canvas.drawBitmap(bitmap, new Rect(0, 0, bitmap.getWidth() / 2, bitmap.getHeight() / 2), new RectF(500, 300, 800, 500), null);
Matrix matrix = new Matrix();
// 将bitmap平移到此位置
matrix.postTranslate(800, 300);
canvas.drawBitmap(bitmap, matrix, mPaint);
//为防止oom,及时回收bitmap
bitmap.recycle();
}
效果图:
1.4 绘制圆形
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {}
- cx:圆心x坐标
- cy:圆心y坐标
- radius:半径
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制圆形
canvas.drawCircle(100,700,100,mPaint);
}
效果图:
1.5 绘制点
public void drawPoint(float x, float y, @NonNull Paint paint) {}
- x:点的x坐标
- y:点的y坐标
- drawPoints(float[]pts,Paintpaint)绘制一组点
- pts:float数组,两位为一组,两两结合代表x、y坐标,例如:pts[0]、pts[1]代表第一个点的x、y坐标,pts[2]、pts[3]代表第二个点的x、y坐标,依次类推。
/**
* @Size(multiple = 2) 表示参数数组的长度必须是2的倍数
*/
public void drawPoints(@Size(multiple = 2) float[]pts, intoffset, intcount, @NonNull Paintpaint) {}
- pts:float数组,两位为一组,两两结合代表x、y坐标,例如:pts[0]、pts[1]代表第一个点的x、y坐标,pts[2]、pts[3]代表第二个点的x、y坐标,依次类推。
- offset:代表数组开始跳过几个只开始绘制点,注意这里不是指数组的下标,而是代表跳过几个值。
- count:在跳过offset个值后,处理几个值,注意这里的count不是代表点的个数,而是代表数组中值的个数。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制点
canvas.drawPoint(100, 700, mPaint);
float[] points = new float[]{
130, 700,
160, 700,
190, 700,
210, 700,
240, 700
};
// 绘制一组点(代表跳过前两个值,处理4个值,也就是实际绘制2个点)
canvas.drawPoints(points, 2, 4, mPaint);
}
效果图:
1.6 绘制椭圆
public void drawOval(@NonNull RectF oval, @NonNull Paint paint) {}
public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint) {}
- 在left、top、right、bottom围成的区域内绘制一个椭圆。
- 将第一个重载方法的left、top、right、bottom封装到RectF类中,与扇形的重载方法异曲同工。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制椭圆
// 创建一个RectF
RectF rectF2 = new RectF(300, 600, 700, 800);
canvas.drawOval(rectF2, mPaint);
}
效果图:
1.7 绘制矩形
public void drawRect(@NonNull RectF rect, @NonNull Paint paint) {}
public void drawRect(@NonNull Rect r, @NonNull Paint paint) {}
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) {}
drawRect的参数非常好理解,直接上代码看效果:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制椭圆
// 创建一个RectF
RectF rectF2 = new RectF(300, 600, 700, 800);
canvas.drawOval(rectF2, mPaint);
// 绘制矩形
canvas.drawRect(rectF2, mPaint);
}
注: 这里的rectF2即上文绘制椭圆时创建的RectF对象。
效果图:
1.8 绘制圆角矩形
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {}
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, @NonNull Paint paint) {}
- drawRoundRect是绘制圆角矩形,用法和drawRect类似,唯一不同的是多了两个参数:
- rx:x轴方向的圆角弧度
- ry:y轴方向的圆角弧度上代码,看效果:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制椭圆
// 创建一个RectF
RectF rectF2 = new RectF(300, 600, 700, 800);
canvas.drawOval(rectF2, mPaint);
// 绘制矩形
canvas.drawRect(rectF2, mPaint);
// 绘制圆角矩形
canvas.drawRoundRect(rectF2, 60, 30, mPaint);
}
这里为了突出两个方向的圆角弧度,特地将rx和ry设置差距比较大,效果如下:
1.9 绘制直线
public void drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) {}
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count, @NonNull Paint paint) {}
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint) {}
drawLine和drawLines一个是绘制一个点,一个是绘制一组点,其中drawLines 中的float数组中四个值为一组点,其用法可以参照drawPoints。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 绘制直线
canvas.drawLine(100, 820, 800, 820, mPaint);
float[] lines = new float[]{
100f, 850f, 800f, 850f,
100f, 900f, 800f, 900f,
100f, 950f, 800f, 950f
};
// 按floats数组中,四个数为1组,绘制多条线
canvas.drawLines(lines, mPaint);
}
效果图:
1.10 drawPath() 绘制不规则图形
上面的这些Canvas方法固然已经很强大了,但是我们如果想要绘制一些不规则的图形怎么办,这时候就要用到强大的drawPath()方法了,通过对Path进行设置不同的坐标、添加不同图形,最后传入drawPath方法中可以绘制出复杂的且不规则的形状。以下是drawPath的方法及参数:
public void drawPath(@NonNull Path path, @NonNull Paint paint) {}
这里的关键参数就是Path,Path类的方法较多,大部分用法与上述方法类似,这里挑几个介绍:
Path类
addArc(RectFoval,floatstartAngle,floatsweepAngle) -往path里面添加一个圆弧
addCircle(floatx,floaty,floatradius,Path.Directiondir) -添加一个圆形
addOval(RectFoval,Path.Directiondir) -添加一个椭圆
addRect(RectFrect,Path.Directiondir) -添加一个矩形
lineTo(floatx,floaty) -连线到坐标(x,y)
moveTo(floatx,floaty) -将path绘制点移动到坐标(x,y)
close() -用直线闭合图形,调用此方法后,path会将最后一处点与起始用直线连接起来,path起始点为moveTo()方法的坐标上,如果没有调用moveTo()起始点将默认为(0,0)坐标。
接下来使用drawPath绘制一个楼梯:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 使用 Path 绘制一个楼梯
Path path = new Path();
path.lineTo(0, 1000);
path.lineTo(100, 1000);
path.lineTo(100, 1100);
path.lineTo(200, 1100);
path.lineTo(200, 1200);
path.lineTo(300, 1200);
path.lineTo(300, 1300);
path.lineTo(400, 1300);
path.lineTo(400, 1400);
path.lineTo(0, 1000);
path.close();
canvas.drawPath(path, mPaint);
}
效果图:
再用drawPath方法绘制一个Android小机器人:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 再用 drawPath 方法绘制一个 Android 小机器人:
// 绘制两个触角
path.reset();
path.moveTo(625, 1050);
path.lineTo(650, 1120);
path.moveTo(775, 1050);
path.lineTo(750, 1120);
// 绘制头部
path.addArc(new RectF(600, 1100, 800, 1300), 180, 180);
// 绘制眼睛,CW:顺时针绘制,CCW:逆时针绘制
path.addCircle(666.66f, 1150, 10, Path.Direction.CW);
path.addCircle(733.33f, 1150, 10, Path.Direction.CW);
// 身体
path.addRect(new RectF(600, 1200, 800, 1300), Path.Direction.CW);
canvas.drawPath(path, mPaint);
}
二、Paint 画笔
Canvas除了可以绘制图形之外,还可以绘制文字,Canvas的绘制文字的方法有drawText()、drawTextOnPath()、drawTextRun()等方法,在绘制文字是和Paint的结合更为紧密,所以绘制文字的方法放在下文。
先做好准备工作:
public class PaintStudyView extends View {
// 绘制文字的Paint
private Paint mTextPaint;
// 绘制参考点的Paint
private Paint mPointPaint;
private Context mContext;
// y轴方向的间距
private final static float Y_SPACE = 100;
public PaintStudyView(Context context){
super(context);
init(context);
}
public PaintStudyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mTextPaint = new Paint();
// 消除锯齿
mTextPaint.setAntiAlias(true);
// 设置笔尖宽度
mTextPaint.setStrokeWidth(1);
// 填充
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(30);
mPointPaint = new Paint();
mPointPaint.setAntiAlias(true);
mPointPaint.setStrokeWidth(5);
// 将参考点的Paint设置为红色
mPointPaint.setColor(Color.RED);
// 不填充
mPointPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
2.1 Canvas 的绘制文字的相关方法
2.1.1 drawText() 的重载方法
drawText()是Canvas的绘制文字中的最长用的方法,它只能按照从左至右的普通方式来绘制文字。
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {}
public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint) {}
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y, @NonNull Paint paint) {}
public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint) {}
- text:待绘制的文字内容
- x:文字绘制位置的x坐标
- y:文字绘制位置的y坐标
- paint:Paint画笔,可以通过Paint.setTextAlign()来决定文字的方位,有Paint.Align.LEFT(居左),Paint.Align.RIGHT(居右),Paint.Align.CENTER (居中)三个位置。
- start:代表从text中的第几个字符开始截取绘制,包含第start个字符。
- end:代表截取到text的第几个字符,不包含第end个字符。
以下示例说明了文字的不同位置,同时也说明了第二个和第四个重载方法对字符串截取时的用法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 待绘制文字
String str = "我是一个自定义View的控件";
float x = (float) getWidth() / 2;
float y = 100;
// 绘制参考点,便于观察文字处于x,y坐标的位置,从而来学习setTextAlign()方法
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(str, x, y, mTextPaint);
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(str, 0, 6, x, y, mTextPaint);
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(str.toCharArray(), 1, 6, x, y, mTextPaint);
}
效果图:
2.1.2 drawTextOnPath() 的重载方法
drawTextOnPath()由方法名字我们就可以看出来他可以按照Path的走向来绘制文字,例如我们在path中传入一个圆弧,那么绘制出来的文字走向就是圆弧状的,是不是很酷,来看一下它的重载方法:
public void drawTextOnPath(String text,Path path,float hOffset,float vOffset,Paint paint) {}
public void drawTextOnPath(char[]text,int index,int count,Path path,float hOffset,float vOffset,Paint paint)
- text:同drawText的第一个参数。
- path:Path参数,用法在前文已经说过了。
- hOffset:水平方向的偏移量。
- vOffset:垂直方向的偏移量。
- index: 要绘制的文本中的起始索引
- count: 从索引开始,绘制的字符数
关键点:有一点一定要提的就是,这里的hOffset是相对于path路径的水平偏移量,而vOffset也是相对于path路径的垂直偏移量,这么说可能还有点不清楚,结合下面的示例来说明,请仔细体会这里的意思:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
// 1、下开口圆弧方向绘制文字
mTextPaint.setTextAlign(Paint.Align.LEFT);
y += Y_SPACE;
Path path = new Path();
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 180, 180);
// 参考弧度线
canvas.drawPath(path, mPointPaint);
// 按照path路径绘制文字,不偏移
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
// 向水平、垂直方向各偏移30
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);
// 向水平、垂直方向各偏移60
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);
// 向水平、垂直方向各偏移90
canvas.drawTextOnPath(str, path, 90, 90, mTextPaint);
// 2、上开口圆弧方向绘制文字
path.reset();
y += Y_SPACE;
path.addArc(new RectF(x - 150, y, x + 150, y + 300), 0, 180);
// 参考弧度线
canvas.drawPath(path, mPointPaint);
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 30, mTextPaint);
canvas.drawTextOnPath(str, path, 60, 60, mTextPaint);
path.close();
// 3、竖直方向绘制文字
path.reset();
path.moveTo(200, y);
path.lineTo(200, y + 4 * Y_SPACE);
// 参考弧度线
canvas.drawPath(path, mPointPaint);
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);
y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;
y += Y_SPACE;
//4、水平方向绘制文字
path.reset();
path.moveTo(x, y);
path.lineTo(x + 4 * Y_SPACE, y);
// 参考弧度线
canvas.drawPath(path, mPointPaint);
canvas.drawTextOnPath(str, path, 0, 0, mTextPaint);
canvas.drawTextOnPath(str, path, 30, 60, mTextPaint);
}

2.1.3 drawTextRun() 的重载方法
drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)
drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)
drawTextRun()可以文字的是从左到右还是从右到左的顺序来绘制,其中倒数第
二个参数 isRtl 就是用来控制方向的,true 就是倒序绘制,false 就是正序绘制,
其他的参数就没啥好说的了,这个方法用法比较简单,这里就不贴代码了。另外
这个方法是在 API 23 才开始添加的,使用时要注意。
2.2 使用 Paint 测量文字的尺寸,定位文字
我们在开发自定义控件时,免不了要精确定位文字的文字,例如必须把文字放在
某个区域的正中间,或者必须让一行文字的几何中心精确的处于某个点上,这时
我们如果不懂这里的窍门可能就要盲目的试位置了,这样一点一点试出来的位置
很不可靠,可能换个屏幕尺寸位置就不对了,接下来怎么来看看怎么样用最优雅
的姿势来精确的定位文字。
其实在水平方向的定位还比较好说,直接使用 Paint.setTextAlign()就能搞定
大多需求,主要是在水平方向上稍稍复杂一点,想要定位位置,首先需要先获取
文字的高度,要用到 Paint 的以下两个方法:
float ascent():根据文字大小获取文字顶端到文字基线的距离(返回的是负值)
float descent():根据文字大小获取文字底部到文字基线的距离(返回的事正值)
有了这两个方法那就非常好办了,首先用代码结合效果图说明一下基线、ascent、
descent 和文字的关系:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawLine(x - 300, y, x + 300, y, mPointPaint);
mTextPaint.setTextAlign(Paint.Align.CENTER);// 水平方向上让文字居中
float ascent = mTextPaint.ascent(); // 根据文字大小获取文字顶端到文字基线的距离(返回的是负值)
float descent = mTextPaint.descent(); // 根据文字大小获取文字底部到文字基线的距离(返回的事正值)
canvas.drawLine(x - 300, y + ascent, x + 300, y + ascent, mPointPaint);
canvas.drawLine(x - 300, y + descent, x + 300, y + descent, mPointPaint);
canvas.drawText(str, x, y, mTextPaint);
}
效果图:
接下来就让文字的中心落在参考点上:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
//将文字的中心定位在参考点上
y += Y_SPACE;
canvas.drawPoint(x, y, mPointPaint);
canvas.drawText(str, x, y - ascent / 2 - descent / 2, mTextPaint);
}
效果图如下,仔细看参考点(红点)和文字的位置:

2.3 利用Paint.setShader()(着色器)绘制渐变色
使用setShader()方法可以添加渐变颜色也可以使用图片作为背景,其参数是一个Shader类,传入不同的Shader子类可以实现不同的渐变效果或者添加背景图片,其子类有一下几种:
- LinearGradient:线性渐变
- RadialGradient:放射状渐变
- SweepGradient:扫描渐变
- BitmapShader:添加背景图片
- ComposeShader:多种Shader组合
上面接个Shader的子类在使用方式上都差不多,这里只用LinearGradient为例说明一下,并注意对LinearGradient构造器的最后一个参数传入不同的参数对应的效果图:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// ......
/*Shader渐变*/
y = 100;
Shader shader = new LinearGradient(x - 50, y - 80, x + 50, y + 80, Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"), Shader.TileMode.CLAMP);
mTextPaint.setShader(shader);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
y += 3 * Y_SPACE;
Shader shader1 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"),
Shader.TileMode.REPEAT);
mTextPaint.setShader(shader1);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
y += 3 * Y_SPACE;
Shader shader2 = new LinearGradient(x - 50, y - 80, x + 50, y + 80,
Color.parseColor("#FFCCBB"), Color.parseColor("#FF0000"),
Shader.TileMode.MIRROR);
mTextPaint.setShader(shader2);
canvas.drawRect(x - 500, y - 80, x + 500, y + 80, mTextPaint);
}
效果图如下:
更多推荐



所有评论(0)