IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android绘图(一)基础篇 -> 正文阅读

[移动开发]Android绘图(一)基础篇

一、绘图入门

Canvas 翻译为“画布”,可以理解成画家作画时的宣纸。Canvas 供了若干方法用于绘制各种图形图案——点、线、圆等等。Paint 翻译为“画笔”,为绘图定义各种参数——颜色、线条样式、图案样式等等。通常的绘图思路是先定义 Paint 对象,指定绘图参数,再通过 Canvas 对象进行图 形绘制,绘图的结果因 Paint 的不同而不同。Paint 类用于定义绘图时的参数,主要包含颜色、文本、图形样式、位图模式、滤镜等几个方面。
颜色是指绘图时使用的颜色,在 Android 中颜色可以指定透明度,使用 16 进制来表示颜色时,格式通常为#AARRGGBB,其中,AA 表示透明度、RR 表示红色、GG 表示绿色、BB 表示蓝色,Color 类定义了颜色信息,内置了常用颜色的 int 型常量,比如 Color.RED 是红色,Color.BLUE 是蓝色……如果您习惯了 16 进制的颜色,Color 类的静态方法 parseColor(String colorString)可以将 16进制颜色转换成 Color 类型。
先来看看简单的使用

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ImageView iv = findViewById(R.id.iv1);
    // 创建空白的bitmap
    Bitmap bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888);
    // 新建画布,关联bitmap
    Canvas canvas = new Canvas(bitmap);
    // 绘制黑色背景
    canvas.drawColor(Color.BLACK);
    // 定义画笔
    Paint paint = new Paint();
    // 设置画笔颜色
    paint.setColor(Color.WHITE);
    paint.setAntiAlias(true);
    // 画笔实心
    paint.setStyle(Paint.Style.FILL);
    // 文本水平居左对齐
    paint.setTextAlign(Paint.Align.LEFT);
    // 字体大小设置
    paint.setTextSize(32f);
    paint.setTextSkewX(0.5f); // 设置文本倾斜度,取值0~1,正负表示倾斜方向
    paint.setUnderlineText(true);// 设置下划线
    paint.setFakeBoldText(true); // 设置粗体
    // 绘制文字
    canvas.drawText("hello 你好啊!", 10, 100, paint);

    // 绘制图形
    // 设置画笔空心样式
    paint.setStyle(Paint.Style.STROKE);
    paint.setColor(Color.RED);
    // 设置画笔描边的边框宽度
    paint.setStrokeWidth(20);
    // 设置画笔连接处的形状
    paint.setStrokeJoin(Paint.Join.BEVEL);
    // 绘制矩形
    canvas.drawRect(new Rect(20, 200, 350, 350), paint);

    //显示bitmap到ImageView中
    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

二、绘制位图

常用方法

//  bitmap 绘制在画布上,同时指定位图左上角相对于画布的坐标,大小与原位置相同,不进行任何缩放。
public void drawBitmap( Bitmap bitmap, float left, float top,  Paint paint)
//  下面两个方法从源bitmap中抠出一块大小区域为src的图片并绘制到canvas的dst处。src和ds 的大小与比例关系影响到最终的绘制效果,这个过程是自动缩放以适应dest区域的
public void drawBitmap(Bitmap bitmap,  Rect src, RectF dst, Paint paint)
public void drawBitmap(Bitmap bitmap,  Rect src, Rect dst, Paint paint)

ps:绘制位图时,除非需要进行位图运算,否则,并不需要指定 paint 对象,直接传递null 即可。

案例-使用drawBitmap方法在ImageView上绘制2个位图,一个按原始大小绘制,另一个则按2倍大小绘制

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ImageView iv = findViewById(R.id.iv);
    // 创建空白位图
    Bitmap bitmap = Bitmap.createBitmap(500, 800, Bitmap.Config.ARGB_8888);
    // 创建画布并关联位图
    Canvas canvas = new Canvas(bitmap);
    // 绘制背景色,便以区分
    canvas.drawColor(Color.BLACK);
    // 获取资源目录下的原图
    Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
    // 绘制原图
    canvas.drawBitmap(sourceBitmap, 0, 0, null);
    // 获取原图宽高
    int bmpWidth = sourceBitmap.getWidth();
    int bmpHeight = sourceBitmap.getHeight();
    // 创建原图的矩形
    Rect src = new Rect(0, 0, bmpWidth, bmpHeight);
    // 创建放大后的矩形,位于原图下方
    Rect dest = new Rect(0, bmpHeight, bmpWidth * 2, bmpHeight + bmpHeight * 2);
    // 绘制放大后的位图
    canvas.drawBitmap(sourceBitmap, src, dest, null);

    // 显示到ImageView上
    iv.setImageBitmap(bitmap);
}

效果图如下:
在这里插入图片描述

三、绘制点

点的大小取决于 setStrokeWidth()方法的参数,参数值越大,点也就越大。所以,不要以为一个点就是屏幕上的一个像素。如果将 stroke 的宽度设置为足够大,我们发现最终绘制出来的点其实是一个正方形。绘制点的方法一共有三个:

// 该方法在(x,y)处绘制一个点。
public void drawPoint(float x, float y, Paint paint)
// 该方法的参数 pts 是一个数组,从下标 0 开始每 2 个数确定一个点,连续绘制多个点。多余的元素会忽略。
public void drawPoints(float[] pts, Paint paint)
// 从 pts 数组中的第 offset 处开始取出 count 个数字,以 2 个数为一组确实一个点,连 续绘制若干个点。忽略多余的元素
public void drawPoints(float[] pts, int offset, int count,	Paint paint)

示例如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    // 绘制背景
    canvas.drawColor(Color.BLACK);

    //画一个红色的点
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setStrokeWidth(10);
    canvas.drawPoint(120, 20, paint);

    // 画一组蓝色的点
    paint.setColor(Color.BLUE);
    float[] points = new float[]{10, 10, 50, 50, 50, 100, 50, 150};
    canvas.drawPoints(points, paint);

    // 画一组绿色的点
    paint.setColor(Color.GREEN);
    points = new float[]{20, 20, 60, 60, 60, 80, 60, 180};
    canvas.drawPoints(points, 3, 4, paint);

    // 显示在ImageView上
    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

四、绘制直线

两个点确定一条直线,所以,绘制线条时,需要指定两个点的坐标。同画点一样,绘制线条也有 3 个重载的方法:

// 在(startX,startY)和(stopX,stopY)两个点之间绘制一条直线。
public void drawLine(float	startX,	float startY, float stopX, float stopY,	Paint paint)
// pts 数组中每4个数一组绘制一条直线,多余的元素会忽略。	
public void drawLines(float[] pts,	Paint paint)	
// 从 pts 数组中的 offset 索引处开始,取出 count 个元素,并以 4 个数一组绘制直线,忽略多余的元素。
public void drawLines(float[] pts,	int offset,int count, Paint paint)

五、绘制矩形

绘制矩形时,参数分为两种:一种是指定 left、top、right、bottom 等 4 个参数,另一种直接指定一个 Rect 对象或 RectF 对象。绘制直角矩形的三个重载方法如下:

public void drawRect(float left,float top,	float right,float bottom,Paint paint)

public void drawRect(Rect r,Paint	paint)

public void drawRect(RectF	r,Paint	paint)

圆角矩形的几何形状比直角矩形相对复杂一些,我们需要指定 4 个拐角的弧度,4 个角的弧度不能单独设置,而是统一设置为相同的值。拐角弧度实际上是圆或椭圆的一段弧线,如图所示:
在这里插入图片描述
绘制圆角矩形一共有 2 个重载的方法:

// 该方法用于绘制一个圆角矩形,left、top、right、bottom 构建一个矩形,rx、ry 分别是圆角处的水平半径和垂直半径。rx 和 ry 不一定相同,如果不同,则是椭圆上的一段弧线。
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,Paint paint)
// 该方法是上面方法的另一种重载形式。
public void drawRoundRect(RectF rect,float	rx,float ry,Paint paint)

下面的代码绘制了三个矩形,一个直角矩形,一个为空心圆角矩形,另一个是有填充颜色的实心圆角矩形:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main5);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);

    Paint paint = new Paint();
    paint.setAntiAlias(true);
    // 该方法用于设置落笔时的样式,控制我们的画笔在离开画板时留下的最后一点图形
    paint.setStrokeCap(Paint.Cap.ROUND);
    // 当绘图样式为 STROKE 时,该方法用于指定线条连接处的拐角样式,能使绘制的图形更加平滑
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setColor(Color.RED);
    // 绘制红色的直角矩形
    canvas.drawRect(new Rect(0, 0, 100, 100), paint);

    // 绘制绿色的圆角矩形
    paint.setColor(Color.GREEN);
    canvas.drawRoundRect(new RectF(100, 100, 200, 200), 20, 20, paint);

    // 绘制蓝色的圆角矩形(空心)
    paint.setColor(Color.BLUE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(5);
    // 圆角的半径可以不一样
    canvas.drawRoundRect(new RectF(200, 200, 400, 400), 10, 20, paint);

    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

六、绘制圆

在对图形进行分类时,我将圆、椭圆、扇形、弧线统一归类到“圆”这一类中,扇形和弧线可以认为是圆或椭圆的一部分,椭圆的大小是由他的外切矩形来决定的,这实际上和几何学中的定义完全一致,如图所示:
在这里插入图片描述
绘制椭圆的方法如下:

public void drawOval(float	left, float top, float right, float bottom, Paint paint)

public void drawOval(RectF	oval, Paint paint)

绘制椭圆时,如果外切矩形的长和宽相等,即为正方形,绘制出来的图形就是一个正圆,但 是 Cavnas 类供了另一个更加简单实用的方法,供圆点的坐标和半径即可。

//(cx、cy)为圆心坐标,radius 为圆的半径
public void drawCircle(float cx, float cy,	float radius, Paint paint)

弧线和扇形本质上更是相似,弧线是椭圆上的一段,而扇形则是将弧线的两个端点和椭圆中心点使用线条连接形成的闭合区域。理解弧线和扇形的演变过程便很容易明白方法中的参数意义,如下图所示:
在这里插入图片描述
绘制弧线和扇形的方法如下:

// 参数oval是规定椭圆的范围, startAngle 表示起始角度,sweepAngle 表示扇形或弧线所占的角度,
// 正数表示顺时针,负数表示逆时针,useCenter 参数询问是否要使用中心点,为true 表示扇形,为 false 表示弧线
public void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint)

public void drawArc(float left, float top, float right, float bottom, 
					float startAngle, float sweepAngle,boolean useCenter,Paint paint)

下面的代码演示了弧线和扇形的绘制方法,采用了 Style.STROKE 的图形模式,如果将 Style设置为 Style.FILL,不管是弧线还是扇形,都可以使用颜色进行填充

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main6);
    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);

    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    // 空心模式
    paint.setStyle(Paint.Style.STROKE);

    RectF oval = new RectF(100, 100, 400, 200);
    // 绘制椭圆
    paint.setColor(Color.GRAY);
    canvas.drawOval(oval, paint);
    
    // 绘制圆弧,起始角度90度(也就是y轴原点),扫过45度(也就是8点中方向)
    paint.setColor(Color.RED);
    canvas.drawArc(oval,90,45,false,paint);

    // 绘制扇形,起始角度是0度(也就是在x轴原点),扫过-45度(也就是3点钟方向,因为Android坐标系是向下的)
    paint.setColor(Color.GREEN);
    canvas.drawArc(oval,0,-45,true,paint);

    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

七、绘制路径

Path 是 Graphics2D 中一个非常重要的概念,表示“路径”,理解该概念时保持“路径”的本色就好。路径可以是直的、也可以是弯的,可以是闭合的、也可以是非闭合的,可以是圆形的、也可以是方形的,可以是单个的、也可以是多个的,可以是简单的、也可以是复杂的……总的来说,路径是基于普通图形但是功能比普通图形更强的一种复杂图形。

Path 是一个类,用于绘制复杂图形,创建之初什么也没有,只有往 Path 中添加了具体的形状,Path 才会清晰可见。绘制 Path 时,所有信息都存储在 Path 对象中,Canvas 根据 Path 对象来绘制相应的图形。

我们将 Path 的功能归纳成以下几类:

7.1 往 Path 中添加线条

通过 Path 可以绘制出奇形怪状的线条,并能将线条组合在一起变成折线,闭合后就是一个多边形了。这就是 Path 的厉害之处。为此,Path 类中定义了 5 个方法

// 将画笔移动到点(x,y)的位置,使用的是绝对定位
public void moveTo(float x,float y)
// 将画笔移动到一个新点,新点在上一个点的基础上偏移(dx,dy),也就是说,新点的坐标为(x+dx,y+dy)。这里使用的是相对定位。首字母“r”就是“relative(相对)”的意思
public void rMoveTo(float dx,float	dy)
// 将画笔连接到点(x,y)的位置,并在上一个点与当前点之前画一条直线。使用的是绝对定位。
public void lineTo(float x,float y)
// 将画笔移动到一个新点,新点在上一个点的基础上偏移(dx,dy),新点的坐标为(x+dx,y+dy),同时,在新点与上一个点之间画一条直线。这里使用的是相对定位。
public void rLineTo(float dx,float dy)
// 在第一个点和最后一个点之前画一条直线,形成闭合区域。
public void close()

下面的案例使用 Path 绘制了一个五角星,这不是一个完美的五角星几何图形,因为五个点的坐标并没有正确计算出来,只是算了个大概。首先调用了 moveTo(0, 150)定义好这次绘图的起 点,接下来调用 rLineTo()方法通过相对定位计算出下一个点的坐标,并使用直线连接,最后,调用 close()方法连接最后一点和第一个点以形成闭合区域。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main7);
    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);

    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(5);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeJoin(Paint.Join.ROUND);

    // 创建路径
    Path path = new Path();
    path.moveTo(0, 150);
    path.rLineTo(300, 0);
    path.rLineTo(-300, 150);
    path.rLineTo(150, -300);
    path.rLineTo(150, 300);
    path.close();// 连接最后一点和第一个点以形成闭合区域

    // 绘制路径
    canvas.drawPath(path, paint);

    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

7.2 往 Path 中添加矩形、椭圆、弧

如果要往 Path 对象中添加矩形、椭圆、圆和弧,需要调用 Path 类中定义的一组以“add”开 头的方法,这组方法有些需要传递一个类型为 Path.Direction 的参数,这是一个枚举类型,枚举值 CW 表示顺时针,CCW 表示逆时针,下一节内容沿着图形绘制文字时,我们可以清晰地感受到方向对绘图带来的影响。

// 往 Path 对象中添加一个矩形
public void addRect(RectF rect,Path.Direction dir)

public void addRect(float left,float top,float right,float	bottom,	Path.Direction	dir)
// Path 对象中添加一个圆角矩形。该方法和前面绘制圆角矩形相比在定义四个角的弧线大小时功能更强,能对四个角分别定义不同的弧线弧度
public void addRoundRect(RectF rect,float[] radii,	Path.Direction dir)

public void addRoundRect(RectF rect,float rx,float	ry,Path.Direction dir)

public void addRoundRect(float left, float top, float right, float	bottom, float[] radii,Path.Direction dir)
// Path 对象中添加一个椭圆。
public void addOval(RectF oval, Path.Direction dir)

public void addOval(float left, float top,	float right, float bottom, Path.Direction dir)
// Path 对象中添加一个圆。
public void addCircle(float x, float y, float radius, Path.Direction dir)
// Path 对象中添加一段弧。本方法并没有指定方向,因为角度的正负已经代表了方向,正数为顺时针,负数为逆时针。
public void addArc(RectF oval, float startAngle, float sweepAngle)
public void addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)

我们在下面的代码中绘制了一个 Path 对象,对象中同时包含了矩形、圆角矩形、椭圆、圆、弧线等图形,显然,Path 对象绘制出来的图形更加复杂了。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main8);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 620, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);
    paint.setColor(Color.RED);

    Path path = new Path();
    // 绘制矩形
    path.addRect(new RectF(10, 10, 300, 100), Path.Direction.CCW);
    // 绘制圆角矩形,4个角的弧度都不一样,2个数确定一个弧度
    path.addRoundRect(new RectF(10, 120, 300, 220),
            new float[]{10, 20, 20, 10, 30, 40, 40, 30},
            Path.Direction.CCW);
    // 椭圆
    path.addOval(new RectF(10, 240, 300, 340), Path.Direction.CCW);
    // 圆
    path.addCircle(60, 390, 50, Path.Direction.CCW);
    // 弧线
    path.addArc(new RectF(10, 500, 300, 600), -30, -60);
    // 绘制路径
    canvas.drawPath(path,paint);

    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

7.3 往 Path 中添加曲线和贝塞尔曲线

曲线包括弧线和贝塞尔曲线,与前面讲的矩形、圆或弧线不同,绘制曲线时需要确定一个起点,绘制的曲线会与该起点进行连接,形成一个更加复杂的图形。
贝塞尔曲线(Bézier curve)是图形开发中的一个重要工具,通过三个点的,确定一条平滑的曲线,又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具。贝塞尔曲线又分为一阶贝塞尔曲线、二阶贝塞尔曲线、三阶贝塞尔曲线和高阶贝塞尔曲线,一阶贝塞尔曲线就是一条线段,Path 类支持二阶贝塞尔曲线和三阶贝塞尔曲线。如下图所示分别是: 一阶、二阶、三阶
在这里插入图片描述
前面提到,贝塞尔曲线通过 3 个点来绘制一条平滑的曲线,这 3 个点分别是起点、控制点和终点。比如,如果要绘制一条二阶贝塞尔曲线,必须调用 moveTo()方法定义起点,再调用如下方法绘制贝塞尔曲线

// (x1,y1)是控制点,(x2,y2)是终点。
public void quadTo(float x1,float y1,float x2,float y2)

我们通过一段代码演示如何绘制贝塞尔曲线

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main9);
    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 400, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);
    paint.setColor(Color.RED);

    Path path = new Path();
    path.moveTo(100, 100);
    path.quadTo(200, 50, 300, 300);
    canvas.drawPath(path, paint);

    // 画点(起点100,100,控制点200,50,终点300,300)
    paint.setColor(Color.GREEN);
    canvas.drawPoints(new float[]{100, 100, 200, 50, 300, 300}, paint);
    
    // 添加文案
    paint.setStrokeWidth(2);
    paint.setTextSize(22);
    canvas.drawText("起点",90,140,paint);
    canvas.drawText("控制点",220,55,paint);
    canvas.drawText("终点",280,340,paint);
    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述
三阶贝塞尔曲线有 1 个起点,2 个控制点,1 个终点,Path 类中通过下面方法进行绘制

// (x1、y1)、(x2、y2)是控制点,(x3、y3)是终点
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

quadTo()和 cubicTo()的控制点和终点利用绝对定位来进行确定,其实还有另外两个方法,通过相对定位对各点进行定义:

public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

添加曲线可以使用arcTo()方法,arcTo()方法可以和 moveTo()配合使用,通过 moveTo()确定一个起点,再通过 arcTo()绘制弧线。弧线是基于矩形的内切圆上的一段,该弧线的起始点会和 moveTo()方法定义的点进行连接。

public void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
public void arcTo(RectF oval, float startAngle, float sweepAngle)
public void arcTo(float left, float top, float right, float bottom, 
				  float startAngle, float sweepAngle, boolean forceMoveTo)

上面三个方法的参数在前面都有说明,不再赘述,参数 forceMoveTo 为 true 时,表示开始一个新的图形,不和上一个点进行连接,为 false 时才和上一个点连接
我们通过一小段代码来演示 artTo()方法的使用技巧:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main10);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);

    // 绘制椭圆
    RectF oval1 = new RectF(150, 100, 350, 250);
    RectF oval2 = new RectF(150, 250, 350, 400);
    paint.setColor(Color.GRAY);
    canvas.drawOval(oval1, paint);
    canvas.drawOval(oval2, paint);

    Path path = new Path();
    paint.setColor(Color.GREEN);
    path.moveTo(100, 100);
    // true表示不会连接到上一个点(这里上一个点刚好是起始点)
    path.arcTo(oval1, -30, 60, true);
    //forceMoveTo不传默认是false,表示会和上一个点进行连接
    path.arcTo(oval2, 90, -45);
    canvas.drawPath(path, paint);

    iv.setImageBitmap(bitmap);

}

效果图:
在这里插入图片描述
从图中可以看出下面的弧线和上一个点是连接起来的,它的上一个点刚好就是上一个圆弧的终点

7.4 将 Path 中的图形进行运算

我们还可以将多个 Path 进行图形运算,得到更加复杂和不规则的图形。Path 有一个静态内部类 Op,定义了 5 种运算规则:

Path.Op. DIFFERENCE:差集,图形 A 减去与图形 B 重叠的区域后 A 余下的区域。
Path.Op. INTERSECT:交集,图形 A 和图形 B 的重叠区域。
Path.Op. REVERSE_DIFFERENCE:反差集,图形 B 减去与图形 A 重叠的区域后 B 余下的区域。
Path.Op. UNION:并集,包含了图形 A 和图形 B 的所有区域。
Path.Op.XOR:补集,即图形 A 和图形 B 的所有区域减去他们的重叠区域后余下的区域。
我们通过以下的表格来比较这 5 种图形运算的不同效果:
图形A代表黑色正方形,图形B代表红色圆形
在这里插入图片描述
下面通过代码演示,先绘制原图查看效果:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main11);
    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);

    // 绘制白色矩形
    paint.setStyle(Paint.Style.FILL);  // 修改此参数可以查看填充模式还是边框模式
    paint.setColor(Color.WHITE);
    Path path1 = new Path();
    path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);
    canvas.drawPath(path1, paint);

    // 绘制红色圆
    paint.setColor(Color.RED);
    Path path2 = new Path();
    path2.addCircle(100, 100, 50, Path.Direction.CCW);
    canvas.drawPath(path2, paint);

    iv.setImageBitmap(bitmap);

}

效果图如下:
在这里插入图片描述
然后对path进行5种效果运算演示

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main11);
    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(800, 500, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStrokeWidth(5);
    //============原图==========================
    // 绘制白色矩形
    paint.setStyle(Paint.Style.FILL);  // 修改此参数可以查看填充模式还是边框模式
    paint.setColor(Color.WHITE);
    Path path1 = new Path();
    path1.addRect(new RectF(10, 10, 110, 110), Path.Direction.CCW);
    canvas.drawPath(path1, paint);

    // 绘制红色圆
    paint.setColor(Color.RED);
    Path path2 = new Path();
    path2.addCircle(100, 100, 50, Path.Direction.CCW);
    canvas.drawPath(path2, paint);
    //============原图==========================


    // =============差集计算==============
    // path1 差 path2 ,也就是:path1-path2
    paint.setColor(Color.WHITE);
    path1.reset();
    path2.reset();
    path1.addRect(new RectF(10, 170, 110, 270), Path.Direction.CCW);

    path2.addCircle(100, 260, 50, Path.Direction.CCW);
    // 差集运算
    path1.op(path2, Path.Op.DIFFERENCE);
    canvas.drawPath(path1,paint);
    // =============差集计算==============

    // =============交集==============
    // path1 交 path2 ,也就是:path1&path2
    paint.setColor(Color.WHITE);
    path1.reset();
    path2.reset();
    path1.addRect(new RectF(160, 170, 260, 270), Path.Direction.CCW);

    path2.addCircle(250, 260, 50, Path.Direction.CCW);
    // 交集运算
    path1.op(path2, Path.Op.INTERSECT);
    canvas.drawPath(path1,paint);
    // =============差集计算==============

    // =============反差集==============
    // path1 反差集 path2 ,也就是:path2-path1
    paint.setColor(Color.WHITE);
    path1.reset();
    path2.reset();
    path1.addRect(new RectF(260, 170, 360, 270), Path.Direction.CCW);

    path2.addCircle(350, 260, 50, Path.Direction.CCW);
    // 反差集运算
    path1.op(path2, Path.Op.REVERSE_DIFFERENCE);
    canvas.drawPath(path1,paint);
    // =============差集计算==============

    // =============并集==============
    // path1 并集 path2 ,也就是:path1|path2
    paint.setColor(Color.WHITE);
    path1.reset();
    path2.reset();
    path1.addRect(new RectF(450, 170, 550, 270), Path.Direction.CCW);

    path2.addCircle(540, 260, 50, Path.Direction.CCW);
    // 并集运算
    path1.op(path2, Path.Op.UNION);
    canvas.drawPath(path1,paint);
    // =============差集计算==============

    // =============补集==============
    // path1 补集 path2 ,也就是:path1|path2 - path1&path2
    paint.setColor(Color.WHITE);
    path1.reset();
    path2.reset();
    path1.addRect(new RectF(650, 170, 750, 270), Path.Direction.CCW);

    path2.addCircle(740, 260, 50, Path.Direction.CCW);
    // 补集运算
    path1.op(path2, Path.Op.XOR);
    canvas.drawPath(path1,paint);
    // =============差集计算==============

    iv.setImageBitmap(bitmap);

}

效果图:
在这里插入图片描述

7.5 绘制文字

Canvas为我们供了两组方法,一组直接从指定的位置开始绘制文字,另一组沿着 Path 绘制文字:

public void drawText(char[] text, int index, int count, float x, float y, Paint paint)
public void drawText(String text, float x, float y, Paint paint)
public void drawText(String text, int start, int end, float x, float y, Paint paint)
public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint)

上面这一组方法是从指定的位置(坐标)开始绘制文字,虽然都是字符串,但是供了三种形式:char[]、String 和 CharSequence,本质上并没有什么不同,参数 index 和count、start 和 end 可以从字符串中取出子串,而参数 x、y 就是文字绘制的坐标位置,其中 y 是文字的 baseline 的值

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)

上面这两个重载的 drawTextOnPath()方法用于沿着 Path 定义好的路径绘制文字,这是一个很在趣的功能,文字在 Path 的带领下龙飞凤舞,灵活多变。参数 hOffset 和 vOffset 用于定义文字离 Path 的水平偏移量和垂直偏移量,正数和负数影响文字与路径的相对位 置。同样的,也支持绘制从字符数组中截取的子串,index 表示起始索引,count 表示要截取的长度。

下面的案例中绘制了 4 个字符串,一个绘制所有的字符串,中间两个截取子串进行绘制,最后一个沿着 Path 绘制出所有的文字,为了更好的理解文字与路径的关系,所以把对应的路径也绘制出来了

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main12);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(800, 450, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);
    Paint paint = new Paint();
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    paint.setTextSize(30);
    String text = "天王盖地虎,宝塔镇河妖;野鸡闷头钻,哪能上天王山";

    // 1.直接绘制
    canvas.drawText(text, 10, 50, paint);
    // 2.截取数量来绘制
    paint.setColor(Color.RED);
    // start从6开始,end=11,表示取[6,11)范围的数量
    canvas.drawText(text, 6, 11, 10, 100, paint);
    // 截取方式二
    paint.setColor(Color.BLUE);
    // index从12开始,count=5,表示从索引12开始取5个字符
    canvas.drawText(text.toCharArray(), 12, 5, 10, 150, paint);

    // 3.通过路径绘制
    // 先创建路径
    Path path = new Path();
    path.moveTo(10, 300);
    path.quadTo(100, 100, 700, 400);
    paint.setColor(Color.GREEN);
    // 将文字绘制到路径上
    canvas.drawTextOnPath(text, path, 10, 30, paint);
    // 绘制路径,方便查看
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(2);
    paint.setColor(Color.RED);
    canvas.drawPath(path, paint);
    // 绘制到ImageView上
    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

7.5.1 在指定位置绘制文本

下面2个方法都是标记了过时的方法

public void drawPosText(@NonNull String text, @NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);
public void drawPosText(@NonNull char[] text, int index, int count,@NonNull @Size(multiple = 2) float[] pos,@NonNull Paint paint);

通过这2个方法可以按自定的坐标集绘制字符串或者字符数组,例如要实现一个垂直方向的文本绘制可以这样实现

class MyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, def: Int = 0) : View(context, attrs, def) {

    private val paint = Paint().apply {
        isAntiAlias = true
        textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22f, resources.displayMetrics)
        color = Color.RED
    }

    private val text = "月黑风高"

    // 每个字的x和y坐标集
    private val array = FloatArray(text.length * 2).apply {
        for (i in 0 until size step 2) {
            set(i, 100f) // x,水平方向每个字的x相同,都是100px
            set(i + 1, (i + 1) * paint.textSize) // y,垂直方向每个字间隔一个字的大小
        }
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.drawColor(Color.BLACK)
        // 绘制文本
        canvas?.drawPosText(text, array, paint)
    }
}

效果图:
在这里插入图片描述

八、Paint的FontMetrics使用

在这里插入图片描述
粉红色就是TextView的背景色, 可以看到在Ascent和Descent之外分别还有一点距离才到TextView的边缘, 也就是右侧使用橙色方块标出的fontPadding

FontMetrics提供了如下属性:
top: 即上边界, 因为在Android中, y轴正方向是向下的, 而基准线(base line)是y=0, 所以这个值是一个负数.它的值等于它到base line距离的负数
ascent: 字体文件中设置的Ascent值也是负数, 理由同上,它的值是ascent到base line的距离的负数
descent: 字体文件中设置的Descent值正数,因为在基准线下面,它的值等于它到base line的距离
bottom: 下边界, 正数,理由同上,它的值等于它到base line的距离
leading: 两行之间, 上一行的bottom和下一行的top的间距, 然而这个值总是0, 可以忽略,用下图来描述leading

在这里插入图片描述

8.1 行距

行距就是相邻两行的基线之间的距离.默认行距的实际值等于字体设置中的|Descent| + |Aescent|,在Android的TextView中, 可以通过android:lineSpacingExtra和android:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默认值为0, lineSpacingMultiplier默认值为1, 有以下公式

行距=默认行距 * lineSpacingMultiplier + lineSpacingExtra

8.2 计算fontPadding

顶部的fontPadding= |top - ascent |,底部的font padding= bottom - descent,通过android:includeFontPadding可以决定字体的高度是否包含fontPadding

Android中的字体高度是|bottom| + |top|, 而普通软件(例如word, Sketch或者其他设计软件)中, 字体高度使用的是|descent| + |ascent|, 所以Android中的字体在垂直方向上总是比设计稿的多占一点空间.
对于普通的字体, 要完美复刻设计稿的字体高度, 应该把android:includeFontPadding设置为false,默认是true

8.3文本在控件水平方向居中显示

原理很简单,只需要计算绘制文字的x坐标 = 控件宽度的一半 - 文字宽度的一半即可, 代码如下:

private val mPaint: Paint = Paint().apply {
    isAntiAlias = true
    color = Color.RED
    textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    // 绘制的文本
    val txt = "hello go yes!!!"
    // 控件的宽度和高度的一半
    val x = measuredWidth / 2f
    val y = measuredHeight / 2f

    // 绘制中间水平线
    canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)
    // 绘制中间垂直线
    canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)

    // 测量文字大小
    val rect = Rect()
    mPaint.getTextBounds(txt, 0, txt.length, rect)

    // 文字水平居中的x坐标计算
    val finalX = x - rect.width() / 2f
    // 绘制文本
    canvas?.drawText(txt, finalX, y, mPaint)
}

效果图:
在这里插入图片描述
由上图可见文本在水平方向是居中了的,但是垂直方向并没有居中(看感叹号的位置就能对比出来了),这是因为我们将绘制文本的y坐标(baseline)定为了控件高度的一半,这是错误的,我们需要将文本处于控件居中位置, 而不应该是y坐标(baseline), 这就需要向下移动一个baseline的距离,这个距离的计算可以看下面介绍.

8.4 文本在控件垂直方向居中显示

  1. 计算文字的高度一半到baseLine的距离
    Baseline在文本的垂直方向很重要,只有先确定了Baseline的位置,换句话说就是y坐标的值,我们才能准确的将文字绘制在我们想要的位置上。Baseline的概念在我们使用TextView等系统控件直接设置文字内容时是用不到的,但是如果我们想要在Canvas画布上面绘制文字时,Baseline的概念就必不可少了。以4个参数的drawText方法为例:
/**
 * canvas的drawText方法
 * @param text:待绘制的文本
 * @param x:从画布上开始绘制的x坐标(Canvas是一个原点在左上角的平面坐标系)
 * @param y:baseLine所在的y坐标,不少人一开始以为y是绘制文字区域的底部坐标,其实是不正确的,这是两个概念
 * @param paint: 画笔
 */
public void drawText(String text, float x, float y, Paint paint)

计算原理:

/* 
如下所示:
---------------------top---------------------负数2 (相对原点baseline的距离)
---------------------ascent------------------ 负数1 (相对原点baseline的距离)
--------------------文本正中----------------- 距离top和bottom都等于(fontMetrics.bottom - fontMetrics.top)/2
---------------------baseline---------------- 0 (原点)
---------------------descent-----------------正数1 (相对原点baseline的距离)
---------------------bottom------------------正数2 (相对原点baseline的距离)
 */
// 文字的高度 = (fontMetrics.bottom - fontMetrics.top)
// 文字正中 =  文字高度的一半 = (fontMetrics.bottom - fontMetrics.top)/2
// 那么文字的正中到baseline的距离 = 文字高度的一半 - fontMetrics.bottom,即:
int distanceY = (int) (((fontMetrics.bottom - fontMetrics.top) / 2) - fontMetrics.bottom);

注意: 文本的正中的"值"并不是控件的getHeight()/2,也就是说getHeight() != (fontMetrics.bottom - fontMetrics.top)因为fontMetrics的值是相对于baseline的.而控件是相对左上角top来计算的.所以如果要在自定义View的正中央绘制文本,那么可以这么做

private val mPaint: Paint = Paint().apply {
    isAntiAlias = true
    color = Color.RED
    textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
}

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

    val txt = "hello go yes!!!"
    // 控件的宽度和高度的一半
    val x = measuredWidth / 2f
    val y = measuredHeight / 2f
    // 绘制中间水平线
    canvas?.drawLine(0f, y, measuredWidth.toFloat(), y, mPaint)
    // 绘制中间垂直线
    canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)
    
    //=======================水平居中处理===============================
    // 测量文字大小
    val rect = Rect()
    mPaint.getTextBounds(txt, 0, txt.length, rect)
    // 文字水平居中的x坐标计算
    val finalX = x - rect.width() / 2f
    
    //=======================垂直居中处理===============================
    // 计算文本高度一半到baseline的距离
    val metrics = mPaint.fontMetrics
    val distance = (metrics.bottom - metrics.top) / 2f - metrics.bottom

    // 矫正y坐标(我们预想的就是让文本正中的位置处于控件居中的位置,而文本正中距离baseline的距离就是我们计算y坐标的偏移量)
    val finalY = y + distance
    // 绘制文本
    canvas?.drawText(txt, finalX, finalY, mPaint)
}

效果图:
在这里插入图片描述
注意看感叹号的位置是居中的, 换个中文就很明显了
在这里插入图片描述

8.5 Paint的setTextAlign使用

该方法用于控制文本在水平方向的对齐方式, 可以理解成该字符串与起点的相对位置, 常用有3种:
1)Align.LEFT: 居左绘制,即通过drawText函数指定的起点在最左侧,文字从起点位置开始绘制
2)Align.CENTER:居中绘制,即通过drawText函数指定的起点在文字中间位置
3)Align.Right: 居右绘制,即通过drawText函数指定的起点在文字右侧位置.
如下图所示:
在这里插入图片描述
因此如果需要将文本绘制在控件的水平居中的位置,可以这么做

private val mPaint: Paint = Paint().apply {
    isAntiAlias = true
    color = Color.RED
    textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23f, resources.displayMetrics)
    textAlign = Paint.Align.CENTER // 文字水平居中对齐
}

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

    val txt = "我是胜哥"

    // 控件的宽度的一半
    val x = measuredWidth / 2f

    // 绘制中间垂直线
    canvas?.drawLine(x, 0f, x, measuredHeight.toFloat(), mPaint)

    // 水平居中绘制文本, 可以看到此时计算x坐标的时候就不需要计算文本的宽度了
    canvas?.drawText(txt, x, 100f, mPaint)
}

效果如下:
在这里插入图片描述

九、绘制验证码

在 ImageView 绘制一个空心矩形,随机产生 100 条干扰线,并随机生成 4 个数,字显示在矩形框内。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ImageView iv = findViewById(R.id.iv);
    Bitmap bitmap = Bitmap.createBitmap(500, 300, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawColor(Color.BLACK);

    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.FILL);
    // 先绘制长方型
    canvas.drawRect(new Rect(100, 100, 370, 200), paint);
    // 绘制随机文字
    char[] text = new char[]{'我', '你', '他', '它', '东', '西', '南', '北', '中'};
    Random random = new Random();
    // 生成随机数
    char[] randomText = new char[4];
    int index = 0;
    while (index < 4 && randomText[index] == 0) {
        char getChar = text[random.nextInt(text.length)];
        boolean contains = false;
        for (int i = 0; i < randomText.length; i++) {
            if (getChar == randomText[i]) {
                contains = true;
                break;
            }
        }
        if (!contains) {
            randomText[index] = getChar;
            index++;
        }
    }
    paint.setColor(Color.BLACK);
    paint.setTextSize(30);
    paint.setFakeBoldText(true);
    // 设置字间距为一个字的宽度
    paint.setLetterSpacing(1);
    // 绘制文本
    canvas.drawText(randomText, 0, 4, 110, 160, paint);

    // 绘制线条
    paint.setStyle(Paint.Style.STROKE);
    for (int i = 0; i < 100; i++) {
        // 随机颜色
        int color = Color.argb(150,
                55 + random.nextInt(200),
                55 + random.nextInt(200),
                55 + random.nextInt(200));
        paint.setColor(color);
        // 绘制随机直线条
        /*int startY = 100 + random.nextInt(100);
        int endX = 100 + random.nextInt(270);
        canvas.drawLine(100, startY, endX, startY, paint);*/

        // 绘制随机贝塞尔曲线
        Path path = new Path();
        // 控制点
        int controlX = 100 + random.nextInt(100);
        int controlY = 100 + random.nextInt(100);
        // 起点
        path.moveTo(100, 100 + random.nextInt(100));
        // 终点
        int endX = 370;
        int endY = 100 + random.nextInt(100);
        path.quadTo(controlX, controlY, endX, endY);
        canvas.drawPath(path, paint);
    }
    iv.setImageBitmap(bitmap);
}

效果图:
在这里插入图片描述

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-01-16 13:11:02  更:2022-01-16 13:12:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:32:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码