当前位置: 首页 > news >正文

开源 java android app 开发(十五)绘图定义控件--仪表盘

 文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

 相关链接:

开源 java android app 开发(一)开发环境的搭建-CSDN博客

开源 java android app 开发(二)工程文件结构-CSDN博客

开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客

开源 java android app 开发(四)GUI界面重要组件-CSDN博客

开源 java android app 开发(五)文件和数据库存储-CSDN博客

开源 java android app 开发(六)多媒体使用-CSDN博客

开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客

开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客

开源 java android app 开发(九)后台之线程和服务-CSDN博客

开源 java android app 开发(十)广播机制-CSDN博客

开源 java android app 开发(十一)调试、发布-CSDN博客

开源 java android app 开发(十二)封库.aar-CSDN博客

推荐链接:

开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客

开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客

开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客

开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客

开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客

本章节主要内容是:自定义的仪表盘控件的使用,随机指向速度。

1.代码分析

2.所有源码

3.效果图

一、代码分析

DashboardView 自定义仪表盘视图详细分析
1. 类定义和常量声明
 

public class DashboardView extends View {// 默认颜色值常量private static final int DEFAULT_DIAL_COLOR = Color.parseColor("#3F51B5");private static final int DEFAULT_NEEDLE_COLOR = Color.parseColor("#FF4081");private static final int DEFAULT_TEXT_COLOR = Color.parseColor("#212121");private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFFFFF");


功能分析:

继承自View基类,实现自定义视图

定义4个默认颜色常量,使用Material Design配色方案

Color.parseColor()将十六进制颜色字符串转换为整型颜色值

2. 成员变量定义
2.1 绘制对象
 

private Paint dialPaint;        // 表盘圆弧画笔
private Paint needlePaint;      // 指针画笔  
private Paint textPaint;        // 文字画笔
private Paint bgPaint;          // 背景画笔


2.2 自定义属性
 

private String title = "仪表盘";     // 标题文本
private int dialColor;              // 表盘颜色
private int needleColor;            // 指针颜色
private int textColor;              // 文字颜色
private int backgroundColor;        // 背景颜色


2.3 数据范围
 

private float currentValue = 0;     // 当前显示值
private float minValue = 0;         // 最小值
private float maxValue = 100;       // 最大值
private float targetValue = 0;      // 动画目标值


2.4 动画控制
 

private boolean isAnimating = false;                    // 动画状态标志
private static final long ANIMATION_DURATION = 500;     // 动画时长500ms
private long animationStartTime = 0;                    // 动画开始时间戳


3. 构造函数
3.1 单参数构造函数
 

public DashboardView(Context context) {super(context);init(null);  // 调用初始化方法,无属性集
}


功能: 用于代码动态创建视图时的构造函数

3.2 双参数构造函数
 

public DashboardView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs);  // 传入XML属性集
}


功能: 用于XML布局文件中引用的标准构造函数

3.3 三参数构造函数
 

public DashboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);  // 包含默认样式属性
}


功能: 支持样式属性的构造函数

4. init() 初始化函数
 

private void init(AttributeSet attrs) {// 获取自定义属性if (attrs != null) {TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DashboardView);title = a.getString(R.styleable.DashboardView_title);dialColor = a.getColor(R.styleable.DashboardView_dialColor, DEFAULT_DIAL_COLOR);needleColor = a.getColor(R.styleable.DashboardView_needleColor, DEFAULT_NEEDLE_COLOR);textColor = a.getColor(R.styleable.DashboardView_textColor, DEFAULT_TEXT_COLOR);backgroundColor = a.getColor(R.styleable.DashboardView_backgroundColor, DEFAULT_BG_COLOR);a.recycle();  // 必须回收TypedArray} else {// 使用默认值dialColor = DEFAULT_DIAL_COLOR;// ... 其他颜色初始化}// 初始化表盘画笔dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  // 启用抗锯齿dialPaint.setColor(dialColor);dialPaint.setStyle(Paint.Style.STROKE);        // 描边模式dialPaint.setStrokeWidth(10f);                 // 线宽10像素// 初始化指针画笔needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);needlePaint.setColor(needleColor);needlePaint.setStyle(Paint.Style.FILL_AND_STROKE);  // 填充和描边needlePaint.setStrokeWidth(5f);                // 线宽5像素// 初始化文字画笔textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);textPaint.setColor(textColor);textPaint.setTextSize(40);                     // 文字大小40pxtextPaint.setTextAlign(Paint.Align.CENTER);    // 文字居中对齐// 初始化背景画笔bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);bgPaint.setColor(backgroundColor);bgPaint.setStyle(Paint.Style.FILL);            // 填充模式
}


功能分析:

属性解析: 使用TypedArray读取XML中定义的属性值

资源回收: 必须调用recycle()释放资源

画笔配置: 为不同绘制元素创建专用的Paint对象

抗锯齿: 所有画笔都启用抗锯齿以获得平滑效果5. onDraw() 核心绘制函数
 

@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);  // 调用父类绘制// 计算基本几何参数int width = getWidth();int height = getHeight();int centerX = width / 2;      // 中心点X坐标int centerY = height / 2;     // 中心点Y坐标  int radius = Math.min(width, height) / 2 - 20;  // 表盘半径(留出边距)// 1. 绘制背景圆形canvas.drawCircle(centerX, centerY, radius + 20, bgPaint);// 2. 绘制表盘圆弧RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);canvas.drawArc(oval, 150, 240, false, dialPaint);  // 从150°开始绘制240°圆弧// 3. 绘制刻度线和刻度值drawScale(canvas, centerX, centerY, radius);// 4. 绘制指针drawNeedle(canvas, centerX, centerY, radius);// 5. 绘制标题文字canvas.drawText(title, centerX, centerY + radius + 60, textPaint);// 6. 绘制当前数值(加大字号)textPaint.setTextSize(50);  // 临时调整字号canvas.drawText(String.format("%.1f", currentValue), centerX, centerY + 20, textPaint);textPaint.setTextSize(40);  // 恢复原字号// 7. 动画处理逻辑if (isAnimating) {long currentTime = System.currentTimeMillis();float elapsed = currentTime - animationStartTime;  // 已过去的时间if (elapsed < ANIMATION_DURATION) {float progress = elapsed / ANIMATION_DURATION;  // 动画进度(0-1)// 线性插值计算当前值currentValue = currentValue + (targetValue - currentValue) * progress;invalidate();  // 请求重绘,实现动画帧} else {currentValue = targetValue;  // 动画结束,直接跳到目标值isAnimating = false;         // 停止动画}}
}


6. drawScale() 刻度绘制函数
 

private void drawScale(Canvas canvas, int centerX, int centerY, int radius) {Paint scalePaint = new Paint(Paint.ANTI_ALIAS_FLAG);scalePaint.setColor(textColor);scalePaint.setTextSize(30);scalePaint.setTextAlign(Paint.Align.CENTER);// 绘制11个刻度(0-10,包含两端)for (int i = 0; i <= 10; i++) {// 计算刻度角度:起始150° + 每刻度24°(总240°范围)float angle = 150 + (i * 24);// 计算对应数值:最小值 + 等分比例float value = minValue + (maxValue - minValue) * i / 10;// 将角度转换为弧度double startAngleRad = Math.toRadians(angle);// 计算刻度线起点(向内偏移20px)int startX = centerX + (int) ((radius - 20) * Math.cos(startAngleRad));int startY = centerY + (int) ((radius - 20) * Math.sin(startAngleRad));// 计算刻度线终点(表盘边缘)int endX = centerX + (int) (radius * Math.cos(startAngleRad));int endY = centerY + (int) (radius * Math.sin(startAngleRad));// 绘制刻度线canvas.drawLine(startX, startY, endX, endY, scalePaint);// 计算刻度值文字位置(向内偏移50px)int textX = centerX + (int) ((radius - 50) * Math.cos(startAngleRad));int textY = centerY + (int) ((radius - 50) * Math.sin(startAngleRad)) + 10;// 绘制刻度数值(取整显示)canvas.drawText(String.valueOf((int)value), textX, textY, scalePaint);}
}


7. drawNeedle() 指针绘制函数
 

private void drawNeedle(Canvas canvas, int centerX, int centerY, int radius) {// 计算指针角度:150°对应minValue,390°对应maxValue(实际是150°+240°)float angle = 150 + (currentValue - minValue) / (maxValue - minValue) * 240;double angleRad = Math.toRadians(angle);  // 转换为弧度// 计算指针终点坐标(向内偏移30px)int endX = centerX + (int) ((radius - 30) * Math.cos(angleRad));int endY = centerY + (int) ((radius - 30) * Math.sin(angleRad));// 绘制指针线段(从中心到终点)canvas.drawLine(centerX, centerY, endX, endY, needlePaint);// 绘制中心圆点(半径10px)canvas.drawCircle(centerX, centerY, 10, needlePaint);
}


8. setValue() 数值设置函数(带动画)
 

public void setValue(float value) {// 边界检查if (value < minValue) value = minValue;if (value > maxValue) value = maxValue;// 设置动画参数this.targetValue = value;          // 目标值this.isAnimating = true;           // 启动动画标志this.animationStartTime = System.currentTimeMillis();  // 记录开始时间invalidate();  // 触发重绘,启动动画
}


9. 其他设置函数
 

// 设置数值范围
public void setRange(float min, float max) {this.minValue = min;this.maxValue = max;invalidate();  // 重绘视图
}// 设置标题
public void setTitle(String title) {this.title = title;invalidate();
}// 颜色设置函数(都会更新对应画笔颜色并重绘)
public void setDialColor(int color) {this.dialColor = color;dialPaint.setColor(color);invalidate();
}// 获取当前值
public float getCurrentValue() {return currentValue;
}


10. 关键设计特点
10.1 动画实现机制
线性插值: 使用currentValue + (targetValue - currentValue) * progress计算过渡值

时间控制: 基于系统时间戳计算动画进度

连续重绘: 通过invalidate()在动画期间持续刷新视图

10.2 几何计算
角度映射: 将数值线性映射到150°-390°的角度范围

坐标转换: 使用三角函数计算圆周上的点坐标

自适应尺寸: 基于视图实际尺寸计算绘制参数

10.3 绘制层次
背景圆形

表盘圆弧

刻度线和数值

指针和中心点

标题和当前值文字

这个自定义视图实现了完整的仪表盘功能,具有良好的可定制性和平滑的动画效果

二、所有源码

文件结构,

DashboardView.java

MainActivity.java

activity_main.xml

attrs.xml

colors.xml

1.DashboardView.java源码

package com.example.dashboard;import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;public class DashboardView extends View {// 默认颜色值private static final int DEFAULT_DIAL_COLOR = Color.parseColor("#3F51B5");private static final int DEFAULT_NEEDLE_COLOR = Color.parseColor("#FF4081");private static final int DEFAULT_TEXT_COLOR = Color.parseColor("#212121");private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFFFFF");// 绘制相关对象private Paint dialPaint;private Paint needlePaint;private Paint textPaint;private Paint bgPaint;// 自定义属性private String title = "仪表盘";private int dialColor;private int needleColor;private int textColor;private int backgroundColor;// 数据相关private float currentValue = 0;private float minValue = 0;private float maxValue = 100;private float targetValue = 0;// 动画相关private boolean isAnimating = false;private static final long ANIMATION_DURATION = 500; // 动画持续时间(ms)private long animationStartTime = 0;public DashboardView(Context context) {super(context);init(null);}public DashboardView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(attrs);}public DashboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);}private void init(AttributeSet attrs) {// 获取自定义属性if (attrs != null) {TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.DashboardView);title = a.getString(R.styleable.DashboardView_title);dialColor = a.getColor(R.styleable.DashboardView_dialColor, DEFAULT_DIAL_COLOR);needleColor = a.getColor(R.styleable.DashboardView_needleColor, DEFAULT_NEEDLE_COLOR);textColor = a.getColor(R.styleable.DashboardView_textColor, DEFAULT_TEXT_COLOR);backgroundColor = a.getColor(R.styleable.DashboardView_backgroundColor, DEFAULT_BG_COLOR);a.recycle();} else {dialColor = DEFAULT_DIAL_COLOR;needleColor = DEFAULT_NEEDLE_COLOR;textColor = DEFAULT_TEXT_COLOR;backgroundColor = DEFAULT_BG_COLOR;}// 初始化表盘画笔dialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);dialPaint.setColor(dialColor);dialPaint.setStyle(Paint.Style.STROKE);dialPaint.setStrokeWidth(10f);// 初始化指针画笔needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);needlePaint.setColor(needleColor);needlePaint.setStyle(Paint.Style.FILL_AND_STROKE);needlePaint.setStrokeWidth(5f);// 初始化文字画笔textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);textPaint.setColor(textColor);textPaint.setTextSize(40);textPaint.setTextAlign(Paint.Align.CENTER);// 初始化背景画笔bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);bgPaint.setColor(backgroundColor);bgPaint.setStyle(Paint.Style.FILL);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);int width = getWidth();int height = getHeight();int centerX = width / 2;int centerY = height / 2;int radius = Math.min(width, height) / 2 - 20;// 绘制背景canvas.drawCircle(centerX, centerY, radius + 20, bgPaint);// 绘制表盘RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);canvas.drawArc(oval, 150, 240, false, dialPaint);// 绘制刻度drawScale(canvas, centerX, centerY, radius);// 绘制指针drawNeedle(canvas, centerX, centerY, radius);// 绘制标题canvas.drawText(title, centerX, centerY + radius + 60, textPaint);// 绘制当前值textPaint.setTextSize(50);canvas.drawText(String.format("%.1f", currentValue), centerX, centerY + 20, textPaint);textPaint.setTextSize(40);// 如果需要动画,继续重绘if (isAnimating) {long currentTime = System.currentTimeMillis();float elapsed = currentTime - animationStartTime;if (elapsed < ANIMATION_DURATION) {float progress = elapsed / ANIMATION_DURATION;currentValue = currentValue + (targetValue - currentValue) * progress;invalidate();} else {currentValue = targetValue;isAnimating = false;}}}private void drawScale(Canvas canvas, int centerX, int centerY, int radius) {Paint scalePaint = new Paint(Paint.ANTI_ALIAS_FLAG);scalePaint.setColor(textColor);scalePaint.setTextSize(30);scalePaint.setTextAlign(Paint.Align.CENTER);for (int i = 0; i <= 10; i++) {float angle = 150 + (i * 24);float value = minValue + (maxValue - minValue) * i / 10;// 计算刻度起点和终点double startAngleRad = Math.toRadians(angle);int startX = centerX + (int) ((radius - 20) * Math.cos(startAngleRad));int startY = centerY + (int) ((radius - 20) * Math.sin(startAngleRad));int endX = centerX + (int) (radius * Math.cos(startAngleRad));int endY = centerY + (int) (radius * Math.sin(startAngleRad));// 绘制刻度线canvas.drawLine(startX, startY, endX, endY, scalePaint);// 绘制刻度值int textX = centerX + (int) ((radius - 50) * Math.cos(startAngleRad));int textY = centerY + (int) ((radius - 50) * Math.sin(startAngleRad)) + 10;canvas.drawText(String.valueOf((int)value), textX, textY, scalePaint);}}private void drawNeedle(Canvas canvas, int centerX, int centerY, int radius) {// 计算指针角度 (150°到390°对应minValue到maxValue)float angle = 150 + (currentValue - minValue) / (maxValue - minValue) * 240;double angleRad = Math.toRadians(angle);// 计算指针终点int endX = centerX + (int) ((radius - 30) * Math.cos(angleRad));int endY = centerY + (int) ((radius - 30) * Math.sin(angleRad));// 绘制指针canvas.drawLine(centerX, centerY, endX, endY, needlePaint);// 绘制中心圆点canvas.drawCircle(centerX, centerY, 10, needlePaint);}// 设置当前值(带动画效果)public void setValue(float value) {if (value < minValue) value = minValue;if (value > maxValue) value = maxValue;this.targetValue = value;this.isAnimating = true;this.animationStartTime = System.currentTimeMillis();invalidate();}// 设置数值范围public void setRange(float min, float max) {this.minValue = min;this.maxValue = max;invalidate();}// 设置标题public void setTitle(String title) {this.title = title;invalidate();}// 设置表盘颜色public void setDialColor(int color) {this.dialColor = color;dialPaint.setColor(color);invalidate();}// 设置指针颜色public void setNeedleColor(int color) {this.needleColor = color;needlePaint.setColor(color);invalidate();}// 设置文字颜色public void setTextColor(int color) {this.textColor = color;textPaint.setColor(color);invalidate();}// 设置背景颜色public void setBackgroundColor(int color) {this.backgroundColor = color;bgPaint.setColor(color);invalidate();}public float getCurrentValue() {return currentValue;}
}

2.MainActivity.java源码

package com.example.dashboard;import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.widget.TextView;
import java.util.Random;public class MainActivity extends AppCompatActivity {private DashboardView dashboard;private TextView valueText;private Handler handler = new Handler();private Random random = new Random();private float minValue = 0;private float maxValue = 100;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);dashboard = findViewById(R.id.dashboard);valueText = findViewById(R.id.valueText);// 设置仪表盘属性dashboard.setTitle("速度仪表盘");dashboard.setDialColor(getResources().getColor(R.color.dialColor));dashboard.setNeedleColor(getResources().getColor(R.color.needleColor));dashboard.setTextColor(getResources().getColor(R.color.textColor));dashboard.setBackgroundColor(getResources().getColor(R.color.backgroundColor));dashboard.setRange(minValue, maxValue);// 开始定时更新数据startDataUpdates();}private void startDataUpdates() {handler.postDelayed(new Runnable() {@Overridepublic void run() {// 生成随机值float value = minValue + random.nextFloat() * (maxValue - minValue);// 更新仪表盘dashboard.setValue(value);// 更新文本显示valueText.setText(String.format("当前值: %.1f", value));// 1秒后再次更新handler.postDelayed(this, 1000);}}, 1000);}@Overrideprotected void onDestroy() {super.onDestroy();// 移除所有回调,防止内存泄漏handler.removeCallbacksAndMessages(null);}
}

3.activity_main.xml源码

<?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"android:gravity="center"android:padding="16dp"tools:context=".MainActivity"><com.example.dashboard.DashboardViewandroid:id="@+id/dashboard"android:layout_width="300dp"android:layout_height="300dp"app:title="速度仪表盘"app:dialColor="#3F51B5"app:needleColor="#FF4081"app:textColor="#212121"app:backgroundColor="#FFFFFF" /><TextViewandroid:id="@+id/valueText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="当前值: 0.0"android:textSize="18sp" /></LinearLayout>

4.attrs.xml源码用于主活动设置控件的属性

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="DashboardView"><attr name="title" format="string" /><attr name="dialColor" format="color" /><attr name="needleColor" format="color" /><attr name="textColor" format="color" /><attr name="backgroundColor" format="color" /></declare-styleable>
</resources>

5.colors.xml源码用途添加的各种颜色

<?xml version="1.0" encoding="utf-8"?>
<resources><color name="colorPrimary">#008577</color><color name="colorPrimaryDark">#00574B</color><color name="colorAccent">#D81B60</color><color name="purple_200">#FFBB86FC</color><color name="purple_500">#FF6200EE</color><color name="purple_700">#FF3700B3</color><color name="teal_200">#FF03DAC5</color><color name="teal_700">#FF018786</color><color name="black">#FF000000</color><color name="white">#FFFFFFFF</color><!-- 仪表盘自定义颜色 --><color name="dialColor">#3F51B5</color><color name="needleColor">#FF4081</color><color name="textColor">#212121</color><color name="backgroundColor">#FFFFFF</color></resources>

三、效果演示

主活动随机写入数据,指针指向仪表盘的相应位置。

http://www.dtcms.com/a/397901.html

相关文章:

  • Android如何自动弹出软键盘?
  • Linux Shell 脚本:从零到进阶的实战笔记
  • MR 一体机市场报告:2031全球规模突破 1.98亿美元,中国 40.8% 市占率成核心增长极
  • 网站管理员权限权重高的网站有哪些
  • 【Spark+Hive+hadoop】基于spark+hadoop基于大数据的全球用水量数据可视化分析系统大数据毕设
  • 07.【Linux系统编程】进程控制(进程创建fork、进程终止exit等、进程等待waitwaitpid、进程替换execl等)
  • 百度Qianfan-VL系列上线:推出3B/8B/70B三款视觉理解模型,覆盖不同算力需求
  • 基于 Python Keras 实现 猫狗图像的精准分类
  • 点云-标注-分类-航线规划软件 (一)点云自动分类
  • 挑战用R语言硬干一百万单细胞数据分析
  • 如何自己弄个免费网站wordpress前端登陆
  • npm install 时包库找不到报错解决
  • 【开题答辩实录分享】以《城市网约车服务预约与管理小程序的设计与实现》为例进行答辩实录分享
  • 网站建设软件哪个最好wordpress转发插件
  • C#异步协同常用例子
  • Flutter 中使用 Color 的最优方案
  • 一半都有哪些做影视外包的网站怎么做网站的签约编辑
  • Qt QEventLoop的使用的一个问题讨论
  • 保定网站seo技术wordpress主题左目录
  • net网站开发做手工简笔上海手机网站建设
  • 做地图的网站湖北专业网站建设市面价
  • 拜师做网站网站短链接生成
  • 用狐狸做logo的网站虾皮跨境电商注册多少钱
  • 东莞化工网站建设网站的思维导图怎么做
  • 公司网站制作银川微信投票网站怎么做
  • 英国零售电商网站开发好点的开发网站的公司
  • 韶关最新消息厦门关键词排名优化
  • 家居网站建设流程企业网站是怎么建站的
  • 国外的域名注册网站哪个好wordpress的使用方法
  • 网站建设包涵哪些领域可以做推广的网站