条形图
首先我们可以创建一个自定义的BarChartView类。
- 在
onTouchEvent
方法中检测用户点击的位置,确定是否点击了某个柱条,并记录该柱条的索引。 - 在
onDraw
方法中,如果有选中的柱条,则在柱条上方绘制一个包含月份、数据1、数据2信息的提示框。 - 使用
drawRoundRect
方法绘制提示框背景。 - 使用
drawCircle
方法在提示框内绘制表示数据1和数据2颜色的圆点。 - 使用
drawColoredText
方法绘制带有颜色圆点的文本。
BarChartView.java代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.Nullable;
public class BarChartView extends View {
private Paint paintBar1;
private Paint paintBar2;
private Paint paintText;
private Paint paintLine;
private Paint paintBackground;
private Paint paintCircle;
private int[] data1 = {100, 140, 230, 100, 130};
private int[] data2 = {150, 100, 200, 140, 100};
private String[] months = {"1月", "2月", "3月", "4月", "5月"};
private int maxValue = 250;
private int selectedIndex = -1;
public BarChartView(Context context) {
super(context);
init();
}
public BarChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paintBar1 = new Paint();
paintBar1.setColor(Color.parseColor("#5087EC"));
paintBar1.setStyle(Paint.Style.FILL);
paintBar2 = new Paint();
paintBar2.setColor(Color.parseColor("#68BBC4"));
paintBar2.setStyle(Paint.Style.FILL);
paintText = new Paint();
paintText.setColor(Color.BLACK);
paintText.setTextSize(50);
paintText.setTextAlign(Paint.Align.CENTER);
paintLine = new Paint();
paintLine.setColor(Color.parseColor("#BBBBBB"));
paintLine.setStyle(Paint.Style.STROKE);
paintLine.setStrokeWidth(2);
paintBackground = new Paint();
paintBackground.setColor(Color.WHITE);
paintBackground.setStyle(Paint.Style.FILL);
paintCircle = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// barWidth = width / (data1.length * 5);// 调整直条宽度
// chartHeight = height * 0.8f; // 留出底部空间用于显示月份标签
int marginTop = 100;
int marginBottom = 100;
int marginLeft = 100;
int marginRight = 100;
int chartHeight = height - marginTop - marginBottom;
int chartWidth = width - marginLeft - marginRight;
int barCountPerMonth = 2; // 每个月的两个数据直条
int totalBars = data1.length * barCountPerMonth + (data1.length - 1); // 总直条数,包括月份间的间隔
int barWidth = chartWidth / totalBars;
// 绘制 Y 轴及其刻度线
for (int i = 0; i <= 5; i++) {
float y = marginTop + i * chartHeight / 5;
canvas.drawLine(barWidth, y, width - barWidth, y, paintLine);
canvas.drawText(String.valueOf(maxValue - maxValue * i / 5), barWidth / 2, y + paintText.getTextSize() / 2, paintText);
}
// 绘制 X 轴
// canvas.drawLine(barWidth, marginTop + chartHeight, width - barWidth, marginTop + chartHeight, paintLine);
// 绘制轴线
canvas.drawLine(marginLeft, marginTop, marginLeft, height - marginBottom, paintLine);
canvas.drawLine(marginLeft, height - marginBottom, width - marginRight, height - marginBottom, paintLine);
// 绘制数据直条,每个月份的两个数据直条(data1 和 data2)挨在一起,并且月份与月份之间隔开一个直条的距离。
for (int i = 0; i < data1.length; i++) {
// 第一个数据直条 (Data1)
int left1 = marginLeft + i * (barCountPerMonth * barWidth + barWidth); // 考虑到月份间的间隔
int right1 = left1 + barWidth;
int top1 = height - marginBottom - (data1[i] * chartHeight / maxValue);
canvas.drawRect(left1, top1, right1, height - marginBottom, paintBar1);
// 第二个数据直条 (Data2)
int left2 = right1;
int right2 = left2 + barWidth;
int top2 = height - marginBottom - (data2[i] * chartHeight / maxValue);
canvas.drawRect(left2, top2, right2, height - marginBottom, paintBar2);
// 绘制月份标签
float x = (left1 + right2) / 2.0f;
float y = height - marginBottom + 40;
canvas.drawText(months[i], x, y, paintText);
// if (selectedIndex ==i) {
// String text = months[selectedIndex] + " | Data1: " + data1[selectedIndex] + " | Data2: " + data2[selectedIndex];
// canvas.drawText(text, width / 2, marginTop + chartHeight + 80, paintText);
// }
}
if (selectedIndex != -1) {
int left1 = marginLeft + selectedIndex * (barCountPerMonth * barWidth + barWidth);
int right1 = left1 + barWidth;
int left2 = right1;
int right2 = left2 + barWidth;
float centerX = (left1 + right2) / 2.0f;
float centerY = Math.min(height - marginBottom - (data1[selectedIndex] * chartHeight / maxValue),
height - marginBottom - (data2[selectedIndex] * chartHeight / maxValue)) - 20;
String text1 = months[selectedIndex];
String text2 = "数据1: " + data1[selectedIndex];
String text3 = "数据2: " + data2[selectedIndex];
RectF rect = new RectF(centerX - 150, centerY - 120, centerX + 150, centerY);
canvas.drawRoundRect(rect, 20, 20, paintBackground);
canvas.drawText(text1, centerX, centerY - 80, paintText);
drawColoredText(canvas, text2, centerX, centerY - 40, Color.parseColor("#5087EC"));
drawColoredText(canvas, text3, centerX, centerY, Color.parseColor("#68BBC4"));
}
}
private void drawColoredText(Canvas canvas, String text, float x, float y, int color) {
paintCircle.setColor(color);
canvas.drawCircle(x - 50, y - 10, 10, paintCircle);
canvas.drawText(text, x, y, paintText);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
float touchX = event.getX();
int width = getWidth();
int height = getHeight();
int marginLeft = 100;
int marginRight = 100;
int chartWidth = width - marginLeft - marginRight;
int barCountPerMonth = 2;
int totalBars = data1.length * barCountPerMonth + (data1.length - 1);
int barWidth = chartWidth / totalBars;
for (int i = 0; i < data1.length; i++) {
int left1 = marginLeft + i * (barCountPerMonth * barWidth + barWidth);
int right1 = left1 + barWidth;
int left2 = right1;
int right2 = left2 + barWidth;
if ((touchX >= left1 && touchX <= right1) || (touchX >= left2 && touchX <= right2)) {
selectedIndex = i;
invalidate(); // 触发重新绘制
return true;
}
}
selectedIndex = -1;
invalidate();
}
return super.onTouchEvent(event);
}
}
这段代码定义了两个 Paint
对象 paintText
和 paintLine
,它们分别用于绘制文本和线条。以下是每个属性的详细解释:
paintText
这是一个用于绘制文本的 Paint
对象。
-
paintText = new Paint();
- 创建一个新的
Paint
对象paintText
,它用于绘制文本。
- 创建一个新的
-
paintText.setColor(Color.BLACK);
- 设置
paintText
的颜色为黑色 (Color.BLACK
)。这意味着用这个Paint
绘制的所有文本都会是黑色的。
- 设置
-
paintText.setTextSize(30);
- 设置
paintText
的字体大小为 30 像素。这会影响用该Paint
绘制的文本的大小。
- 设置
-
paintText.setTextAlign(Paint.Align.CENTER);
- 设置
paintText
的对齐方式为居中对齐 (Paint.Align.CENTER
)。这意味着当绘制文本时,文本的中心点将在指定的 x 坐标位置上。
- 设置
paintLine
这是一个用于绘制线条的 Paint
对象。
-
paintLine = new Paint();
创建一个新的Paint
对象paintLine
,它用于绘制线条。 -
paintLine.setColor(Color.BLACK);
设置paintLine
的颜色为黑色 (Color.BLACK
)。这意味着用这个Paint
绘制的所有线条都会是黑色的。 -
paintLine.setStyle(Paint.Style.STROKE);
设置paintLine
的样式为描边 (Paint.Style.STROKE
)。这意味着只绘制线条的轮廓,而不是填充线条内部。 -
paintLine.setStrokeWidth(2);
设置paintLine
的线宽为 2 像素。这会影响用该Paint
绘制的线条的粗细。
总结起来,这段代码配置了两个 Paint
对象,一个用于绘制文本,另一个用于绘制线条,分别设置了它们的颜色、文本大小、对齐方式、样式和线宽。
在页面布局中添加代码
确保在 activity.xml
中已经正确引用了自定义 View
<!-- ConstraintLayout26 should be defined in your activity_record.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout26"
android:layout_width="match_parent"
android:layout_height="360dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.example.barchart.BarChartView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.example.barchart.BarChartView中example是BarChartView的路径,要根据文件路径进行修改。
这样,当用户点击某个月份的柱条时,会在柱条上方显示所需的提示信息。图表中的数据可以在数组中进行修改。
折线图
我们可以创建一个自定义的LineChartView
类,该类继承自View
。我们需要在onDraw
方法中绘制折线图,并在onTouchEvent
方法中处理鼠标悬停事件。
LineChartView.java代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
//#DCE7FB蓝
//#E3F5F7绿
public class LineChartView extends View {
private Paint paintLine;
private Paint paintLine1;
private Paint paintLine2;
private Paint paintText;
private Paint paintCircle1;
private Paint paintCircle2;
;
private Paint paintFill1;
private Paint paintFill2;
private String[] months = {"1月", "2月", "3月", "4月", "5月"};
private int[] data1 = {100, 140, 230, 100, 130};
private int[] data2 = {150, 100, 200, 140, 100};
private int maxValue = 250;
private int yStep = 50;
private int xStep;
private int selectedIndex = -1;
public LineChartView(Context context) {
super(context);
init();
}
public LineChartView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 创建画笔对象
paintLine = new Paint();
paintLine.setColor(Color.parseColor("#BBBBBB"));
paintLine.setStrokeWidth(3);
paintLine1 = new Paint();
paintLine1.setColor(Color.parseColor("#5087EC")); // 数据1的折线颜色
paintLine1.setStrokeWidth(3);
paintLine1.setStyle(Paint.Style.STROKE); // 仅绘制线条
paintLine2 = new Paint();
paintLine2.setColor(Color.parseColor("#68BBC4")); // 数据2的折线颜色
paintLine2.setStrokeWidth(3);
paintLine2.setStyle(Paint.Style.STROKE); // 仅绘制线条
paintText = new Paint();
paintText.setColor(Color.BLACK);
paintText.setTextSize(50);
paintText.setTextAlign(Paint.Align.CENTER);
paintCircle1 = new Paint();
paintCircle1.setColor(Color.parseColor("#5087EC"));
paintCircle2 = new Paint();
paintCircle2.setColor(Color.parseColor("#68BBC4"));
paintFill1 = new Paint();
paintFill1.setColor(Color.parseColor("#DCE7FB"));
paintFill1.setStyle(Paint.Style.FILL);
paintFill1.setAlpha(128); // 设置透明度(0-255,0为完全透明,255为不透明)
paintFill2 = new Paint();
paintFill2.setColor(Color.parseColor("#E3F5F7"));
paintFill2.setStyle(Paint.Style.FILL);
paintFill2.setAlpha(128); // 设置透明度(0-255,0为完全透明,255为不透明)
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int marginTop = 100;
int marginBottom = 100;
int marginLeft = 100;
int marginRight = 100;
int chartHeight = height - marginTop - marginBottom;
int chartWidth = width - marginLeft - marginRight;
xStep = chartWidth / (months.length - 1);
// 绘制横坐标轴及刻度
canvas.drawLine(marginLeft, height - marginBottom, width - marginRight, height - marginBottom, paintLine);
for (int i = 0; i < months.length; i++) {
float x = marginLeft + i * xStep;
float y = height - marginBottom + 50;
canvas.drawText(months[i], x, y, paintText);
}
// 绘制纵坐标轴及刻度
canvas.drawLine(marginLeft, marginTop, marginLeft, height - marginBottom, paintLine);
for (int i = 0; i <= maxValue; i += yStep) {
float x = marginLeft - 40;
float y = height - marginBottom - i * chartHeight / maxValue;
canvas.drawText(String.valueOf(i), x, y, paintText);
canvas.drawLine(marginLeft, y, width - marginRight, y, paintLine);
}
// 绘制数据1折线及填充区域
Path path1 = new Path();
path1.moveTo(marginLeft, height - marginBottom);
for (int i = 0; i < data1.length; i++) {
float x = marginLeft + i * xStep;
float y = height - marginBottom - data1[i] * chartHeight / maxValue;
// if (i == 0) {
// path1.moveTo(x, y);
// } else {
path1.lineTo(x, y);
// }
// canvas.drawCircle(x, y, 10, paintCircle1);
}
path1.lineTo(marginLeft + (data1.length - 1) * xStep, height - marginBottom);
path1.close();
canvas.drawPath(path1, paintFill1);
// canvas.drawPath(path1, paintLine);
Path linePath1 = new Path();
for (int i = 0; i < data1.length; i++) {
float x = marginLeft + i * xStep;
float y = height - marginBottom - data1[i] * chartHeight / maxValue;
if (i == 0) {
linePath1.moveTo(x, y);
} else {
linePath1.lineTo(x, y);
}
canvas.drawCircle(x, y, 10, paintCircle1);
}
canvas.drawPath(linePath1, paintLine1);
// 绘制数据2折线及填充区域
Path path2 = new Path();
path2.moveTo(marginLeft, height - marginBottom);
for (int i = 0; i < data2.length; i++) {
float x = marginLeft + i * xStep;
float y = height - marginBottom - data2[i] * chartHeight / maxValue;
// if (i == 0) {
// path2.moveTo(x, y);
// } else {
path2.lineTo(x, y);
// }
// canvas.drawCircle(x, y, 10, paintCircle2);
}
path2.lineTo(marginLeft + (data2.length - 1) * xStep, height - marginBottom);
path2.close();
canvas.drawPath(path2, paintFill2);
// canvas.drawPath(path2, paintLine);
Path linePath2 = new Path();
for (int i = 0; i < data2.length; i++) {
float x = marginLeft + i * xStep;
float y = height - marginBottom - data2[i] * chartHeight / maxValue;
if (i == 0) {
linePath2.moveTo(x, y);
} else {
linePath2.lineTo(x, y);
}
canvas.drawCircle(x, y, 10, paintCircle2);
}
canvas.drawPath(linePath2, paintLine2);
// 绘制鼠标悬停时的提示信息
if (selectedIndex != -1) {
float x = marginLeft + selectedIndex * xStep;
float y1 = height - marginBottom - data1[selectedIndex] * chartHeight / maxValue - 40;
float y2 = height - marginBottom - data2[selectedIndex] * chartHeight / maxValue - 80;
canvas.drawText(months[selectedIndex], x, y1, paintText);
canvas.drawText("数据1:" + data1[selectedIndex], x, y1 + 40, paintText);
canvas.drawText("数据2:" + data2[selectedIndex], x, y2, paintText);
}
// 绘制触摸点位置的提示框和文本
if (selectedIndex != -1) {
float x = getPaddingLeft() + selectedIndex * selectedIndex;
float y = getHeight() - getPaddingBottom();
// 绘制提示框
Paint boxPaint = new Paint();
boxPaint.setColor(Color.WHITE);
boxPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(x - 100, y - 150, x + 100, y - 30, boxPaint);
// 绘制文本
Paint textPaint = new Paint();
textPaint.setColor(Color.BLACK);
textPaint.setTextSize(30);
textPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(months[selectedIndex], x, y - 120, textPaint); // 绘制月份
// 绘制数据1的折线颜色和数值
Paint dotPaint1 = new Paint();
dotPaint1.setColor(Color.parseColor("#5087EC"));
canvas.drawCircle(x, y - 80, 8, dotPaint1); // 绘制圆点
canvas.drawText("Data1: " + data1[selectedIndex], x, y - 60, textPaint);
// 绘制数据2的折线颜色和数值
Paint dotPaint2 = new Paint();
dotPaint2.setColor(Color.parseColor("#68BBC4"));
canvas.drawCircle(x, y - 40, 8, dotPaint2); // 绘制圆点
canvas.drawText("Data2: " + data2[selectedIndex], x, y - 20, textPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
float touchX = event.getX();
int width = getWidth();
int height = getHeight();
int marginLeft = 100;
int marginRight = 100;
int chartWidth = width - marginLeft - marginRight;
for (int i = 0; i < data1.length; i++) {
float x = marginLeft + i * xStep;
if (touchX >= x - xStep / 2 && touchX <= x + xStep / 2) {
selectedIndex = i;
invalidate(); // 触发重新绘制
return true;
}
}
selectedIndex = -1;
invalidate();
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
float x = event.getX();
float y = event.getY();
// 根据触摸点的位置计算对应的月份索引
int index = calculateIndex(x);
if (index >= 0 && index < months.length) {
selectedIndex = index;
invalidate(); // 刷新视图,触发重绘
}
break;
case MotionEvent.ACTION_UP:
selectedIndex = -1;
invalidate();
break;
}
return super.onTouchEvent(event);
}
private int calculateIndex(float x) {
// 获取视图的宽度,减去左右的 Padding
float viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
// 计算每个标签之间的间隔
float labelSpacing = viewWidth / (months.length - 1);
// 计算触摸点相对于第一个标签的偏移量
float offsetX = x - getPaddingLeft();
// 计算索引
int index = Math.round(offsetX / labelSpacing);
// 确保索引在有效范围内
if (index < 0) {
index = 0;
} else if (index >= months.length) {
index = months.length - 1;
}
return index;
}
}
我们定义了一个自定义视图 CustomChartView
,通过 init()
方法初始化了 Paint
对象,并在 onDraw()
方法中绘制两个数据集的填充区域。
为了实现绘制两条不同颜色的折线,并且确保它们围起来的空间不被填充,在 onDraw
方法中绘制折线。
步骤:
- 创建两个
Paint
对象:分别为数据1和数据2设置不同的颜色。 - 绘制折线:使用这两个
Paint
对象绘制数据1和数据2的折线。
paintLine1
和paintLine2
是两个Paint
对象,分别用于绘制数据1和数据2的折线。我们分别设置了它们的颜色和线条宽度。setStyle(Paint.Style.STROKE)
确保只绘制折线,不填充内部区域。- 使用
Path
对象来绘制折线。moveTo
方法将画笔移动到起始点,lineTo
方法绘制连线。 canvas.drawPath
方法使用指定的Paint
对象绘制路径。
通过这样的方式,你可以绘制两条不同颜色的折线,并且不会填充它们围起来的空间。你可以根据需要调整数据点和其他属性,以适应你的具体需求。
页面布局代码:
然后在activity_record.xml
文件的ConstraintLayout
中添加一个LineChartView
视图:
<com.example.LineChartView
android:id="@+id/lineChartView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
记得修改文件路径。
最后,在Activity
中找到该视图并设置相关属性:
LineChartView lineChartView = findViewById(R.id.lineChartView);
lineChartView.setBackgroundColor(Color.WHITE);
这样就完成了复式折线统计图的实现。当鼠标悬停在某个月份的折线上时,会在折线上方显示相应的提示信息。