需要做一个项目管理工具,其中使用到了甘特图。发现全网甘特图解决方案比较少,于是自动动手丰衣足食。
前面我用 Python和 Node.js 前端都做过,这次仅仅是移植到 Android上面。
其实甘特图非常简单,开发也不难,如果我专职去做,能做出一个非常棒产品。我写这个只是消遣,玩玩,闲的蛋痛,所以不怎么上心,就搞成下面这德行吧。仅仅供大家学习,参考。
那天心情好了,完善一下。
屏幕布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
</ScrollView>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<cn.netkiller.gantt.ui.GanttView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#DEDEDE"
android:keepScreenOn="true"
android:padding="15dp"
android:text="TextView" />
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
View 代码
package cn.netkiller.gantt.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* TODO: document your custom view class.
*/
public class GanttView extends View {
private final String TAG = GanttView.class.getSimpleName();
private Drawable mExampleDrawable;
private int contentWidth, contentHeight;
private int paddingLeft, paddingTop, paddingRight, paddingBottom;
private int canvasLeft, canvasTop, canvasRight, canvasBottom;
private Canvas canvas;
private int textSize;
private Map<Date, Coordinate> coordinates = new HashMap<Date, Coordinate>();
public static class Coordinate {
public int x, y;
public Coordinate(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public String toString() {
return "Coordinate{" +
"x=" + x +
", y=" + y +
'}';
}
}
public GanttView(Context context) {
super(context);
init(null, 0);
}
public GanttView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public GanttView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
paddingLeft = getPaddingLeft();
paddingTop = getPaddingTop();
paddingRight = getPaddingRight();
paddingBottom = getPaddingBottom();
contentWidth = getWidth() - paddingLeft - paddingRight;
contentHeight = getHeight() - paddingTop - paddingBottom;
// Load attributes
// final TypedArray a = getContext().obtainStyledAttributes(
// attrs, R.styleable.MyView, defStyle, 0);
//
mExampleString = a.getString(
R.styleable.MyView_exampleString);
// mExampleString = "AAAA";
// mExampleColor = a.getColor(
// R.styleable.MyView_exampleColor,
// mExampleColor);
// // Use getDimensionPixelSize or getDimensionPixelOffset when dealing with
// // values that should fall on pixel boundaries.
// mExampleDimension = a.getDimension(
// R.styleable.MyView_exampleDimension,
// mExampleDimension);
//
// if (a.hasValue(R.styleable.MyView_exampleDrawable)) {
// mExampleDrawable = a.getDrawable(
// R.styleable.MyView_exampleDrawable);
// mExampleDrawable.setCallback(this);
// }
//
// a.recycle();
}
// @Override
// protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// super.onSizeChanged(w, h, oldw, oldh);
//
if (h < computeVerticalScrollRange()) {
canScroll = true;
} else {
canScroll = false;
}
// }
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(contentWidth, contentHeight);
}
//
// private boolean canScroll = true;
//
// @Override
// protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// super.onSizeChanged(w, h, oldw, oldh);
//
// if (h < computeVerticalScrollRange()) {
// canScroll = true;
// } else {
// canScroll = false;
// }
// }
List<Data> taskList = List.of(
new Data("AAA", "2024-06-28", "2024-07-04", "1", "Neo"),
new Data("AAABBB", "2024-06-15", "2024-06-20", "10", "Neo"),
new Data("AAACC", "2024-06-25", "2024-06-27", "1", "Neo"),
new Data("AAABBCCD", "2024-06-25", "2024-06-28", "1", "Neo"),
new Data("消息推送", "2024-01-15", "2024-06-30", "1", "Neo"),
new Data("AAA", "2024-06-05", "2024-07-12", "1", "Neo"),
new Data("AAA", "2024-06-05", "2024-07-17", "1", "Neo"),
new Data("AAA", "2024-06-15", "2024-07-07", "1", "Neo"),
new Data("AAA", "2024-06-18", "2024-07-02", "1", "Neo"),
new Data("AAA", "2024-06-05", "2024-07-05", "1", "Neo")
);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
canvasTop = paddingTop;
canvasLeft = paddingLeft;
canvasRight = canvas.getWidth() - paddingRight;
canvasBottom = canvas.getHeight() - paddingBottom;
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
paint.setColor(Color.DKGRAY);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setAlpha(30);
canvas.drawRect(canvasLeft, canvasTop, canvasRight, canvasBottom, paint);
title("canvas");
try {
calendar("2024-06-12", "2024-07-20");
tasks(taskList);
} catch (Exception e) {
}
// Draw the example drawable on top of the text.
if (mExampleDrawable != null) {
mExampleDrawable.setBounds(paddingLeft, paddingTop,
paddingLeft + contentWidth, paddingTop + contentHeight);
mExampleDrawable.draw(canvas);
}
}
public Map<String, Float> colume(List<Data> taskList) {
TextPaint textPaint = new TextPaint();
textPaint.setTextSize(sp2px(20));
float name = textPaint.measureText("任务");
float start = textPaint.measureText("开始日期");
float finish = textPaint.measureText("截止日期");
float day = textPaint.measureText("工时");
float resource = textPaint.measureText("资源");
for (Data data : taskList) {
float textWidth = textPaint.measureText(data.name);
if (textWidth > name) {
name = textWidth;
}
if (textPaint.measureText(data.start) > start) {
start = textPaint.measureText(data.start);
}
if (textPaint.measureText(data.finish) > finish) {
finish = textPaint.measureText(data.finish);
}
if (textPaint.measureText(data.day) > day) {
day = textPaint.measureText(data.day);
}
if (textPaint.measureText(data.resource) > resource) {
resource = textPaint.measureText(data.resource);
}
}
float finalName = name;
float finalStart = start;
float finalResource = resource;
float finalFinish = finish;
float finalDay = day;
return new LinkedHashMap<String, Float>() {{
put("任务", finalName);
put("开始日期", finalStart);
put("截止日期", finalFinish);
put("工时", finalDay);
put("资源", finalResource);
}};
}
private int titleHeight;
public float sp2px(float spValue) {
//fontScale (DisplayMetrics类中属性scaledDensity)
final float fontScale = getResources().getDisplayMetrics().scaledDensity;
return (spValue * fontScale + 0.5f);
}
private void title(String value) {
// Draw the text.
TextPaint textPaint = new TextPaint();
textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextSize(sp2px(25));
float textWidth = textPaint.measureText(value);
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
// float textHeight = fontMetrics.bottom;
float textHeight = textPaint.getFontSpacing();
canvas.drawText(value,
canvasLeft + (getWidth() - textWidth) / 2,
canvasTop + textHeight,
textPaint);
textPaint.setTextSize(sp2px(18));
String copyright = "https://www.netkiller.cn - design by netkiller";
textWidth = textPaint.measureText(copyright);
textHeight += textPaint.getFontSpacing();
canvas.drawText(copyright,
getWidth() - textWidth - canvasLeft,
canvasTop + textHeight,
textPaint);
titleHeight = (int) textHeight;
}
private void process(String string, int x, int y, int size) {
TextPaint mTextPaint = new TextPaint();
mTextPaint.setTextSize(sp2px(size));
// mTextWidth = mTextPaint.measureText(string);
// Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
// mTextHeight = fontMetrics.bottom;
}
private int tableEnd = 0;
// private void table() {
//
//
TextPaint textPaint = new TextPaint();
// calendarTextPaint.setTextSize(sp2px(20));
//
// Paint.FontMetrics fontMetrics = calendarTextPaint.getFontMetrics();
// float fontSpacing = calendarTextPaint.getFontSpacing();
mTextHeight = fontMetrics.bottom;
// int textX = canvasLeft + (int) fontSpacing / 2;
// int textY = canvasTop + (int) fontSpacing * 3 + titleHeight;
//
// int startX = 0;
// int startY = (int) (canvasTop + titleHeight + fontSpacing * 2);
// int stopX = startX;
// int stopY = canvasBottom;
//
// for (String text : List.of("任务", "开始日期", "截止日期", "工时", "资源")) {
// canvas.drawText(text, textX, textY, calendarTextPaint);
// textX += (int) (calendarTextPaint.measureText(text) + fontSpacing);
// startX = stopX = textX;
// canvas.drawLine(startX - (fontSpacing / 2), startY, stopX - (fontSpacing / 2), stopY, calendarPaint);
// }
// tableEnd = (int) (startX - (int) fontSpacing - fontSpacing / 2);
//
// }
private void table() {
// TextPaint textPaint = new TextPaint();
calendarTextPaint.setTextSize(sp2px(20));
// Paint.FontMetrics fontMetrics = calendarTextPaint.getFontMetrics();
float fontSpacing = calendarTextPaint.getFontSpacing();
// mTextHeight = fontMetrics.bottom;
int tableLeft = canvasLeft;
int tableTop = canvasTop + (int) fontSpacing * 2 + titleHeight;
int tableRight = canvasRight;
int tableBottom = canvasBottom;
int textX = tableLeft;
int textY = tableTop + calendarFontSpacing;
int startX = tableLeft;
int startY = tableTop;
int stopX = startX;
int stopY = tableBottom;
for (Map.Entry<String, Float> entry : colume(taskList).entrySet()) {
String text = entry.getKey();
Float textWidth = entry.getValue();
canvas.drawText(text, textX + (int) fontSpacing / 2, textY, calendarTextPaint);
textX += (int) (textWidth + calendarFontSpacing);
startX += (int) (textWidth + calendarFontSpacing);
stopX = startX;
canvas.drawLine(startX, startY, stopX, stopY, calendarPaint);
}
tableEnd = tableRight = (int) (startX - calendarFontSpacing / 2);
}
private void tasks(List<Data> taskList) throws ParseException {
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
// TextPaint textPaint = new TextPaint();
// calendarPaint.setColor(Color.DKGRAY);
calendarPaint.setColor(Color.GRAY);
calendarTextPaint.setTextSize(sp2px(20));
int taskTop = calendarTop + calendarFontSpacing * 3;
int taskLeft = canvasLeft;
int taskRight = tableEnd;
int taskBottom = canvasBottom;
// Paint.FontMetrics fontMetrics = calendarTextPaint.getFontMetrics();
// float fontSpacing = calendarTextPaint.getFontSpacing();
int textX = 0;
int textY = taskTop + calendarFontSpacing;
int startX = 0;
int startY = taskTop;
int stopX = startX;
int stopY = taskTop + calendarFontSpacing;
// canvas.drawLine(startX, calendarTop + calendarFontSpacing * 1, calendarRight, calendarTop + calendarFontSpacing * 1, calendarPaint);
// canvas.drawLine(startX, startY - calendarFontSpacing, startX, stopY, calendarTextPaint);
Map<String, Float> col = colume(taskList);
Log.d(TAG, col.toString());
for (Data task : taskList) {
textX = taskLeft + (int) calendarFontSpacing / 2;
Iterator<Float> aa = col.values().iterator();
for (String text : List.of(task.name, task.start, task.finish, task.day, task.resource)) {
Float textWidth = aa.next();
canvas.drawText(text, textX, textY, calendarTextPaint);
textX += (int) (textWidth + calendarFontSpacing);
startX = stopX = textX;
}
textY += (int) (calendarFontSpacing);
try {
Date startData = new SimpleDateFormat("yyyy-MM-dd").parse(task.start);
Date finishData = new SimpleDateFormat("yyyy-MM-dd").parse(task.finish);
Log.e(TAG, "Start: " + String.valueOf(startData) + " Finish: " + finishData);
Coordinate startCoordinates = coordinates.get(startData);
Coordinate finishCoordinates = coordinates.get(finishData);
Log.e(TAG, "Start: " + startCoordinates.toString() + "Finish: " + finishCoordinates);
canvas.drawRect(startCoordinates.x + 5, startY + 5, finishCoordinates.x + calendarFontSpacing - 5, stopY - 5, paint);
} catch (Exception e) {
}
// canvas.drawText(task.name, textX, textY, calendarTextPaint);
startY += (int) (calendarFontSpacing);
canvas.drawLine(canvasLeft, startY, calendarRight, stopY, calendarPaint);
stopY += (int) (calendarFontSpacing);
}
}
private Paint calendarPaint = new Paint();
private TextPaint calendarTextPaint = new TextPaint();
private int calendarFontSpacing;
private int calendarLeft, calendarTop, calendarRight, calendarBottom;
private void calendar(String startDate, String endDate) throws ParseException {
calendarPaint.setStyle(Paint.Style.STROKE);
calendarPaint.setColor(Color.DKGRAY);
calendarPaint.setStrokeWidth(2);
// calendarPaint.setTextSize(sp2px(20));
// paint.setAlpha(50);
calendarTextPaint.setTextSize(sp2px(20));
calendarFontSpacing = (int) calendarTextPaint.getFontSpacing();
//
// Paint paint = new Paint();
// paint.setTypeface(Typeface.DEFAULT);
// paint.setTextSize(getTextSize());
// Paint.FontMetrics fontMetrics = paint.getFontMetrics();
// float textHeight = fm.getAscent() + fm.getDescent();
// float calendarFontSpacing = fontMetrics.descent - fontMetrics.ascent;
// float calendarFontSpacing = fontMetrics.bottom - fontMetrics.top;
// Paint.FontMetrics fontMetrics = calendarPaint.getFontMetrics();
// float textHeight = fontMetrics.getAscent() + fontMetrics.getDescent();
// 边框
canvas.drawRect(canvasLeft, canvasTop + titleHeight, canvasRight, canvasBottom, calendarPaint);
table();
calendarLeft = canvasLeft + tableEnd;
calendarTop = canvasTop + titleHeight;
calendarRight = canvasRight;
calendarBottom = canvasBottom;
int textX = calendarLeft;
int textY = calendarTop + (int) calendarFontSpacing * 2;
int startX = calendarLeft;
int startY = calendarTop + (int) calendarFontSpacing * 1;
int stopX = 0;
int stopY = calendarBottom;
canvas.drawLine(startX, calendarTop + calendarFontSpacing * 1, calendarRight, calendarTop + calendarFontSpacing * 1, calendarPaint);
canvas.drawLine(startX, startY - calendarFontSpacing, startX, stopY, calendarTextPaint);
canvas.drawLine(canvasLeft, calendarTop + calendarFontSpacing * 2, calendarRight, calendarTop + calendarFontSpacing * 2, calendarPaint);
canvas.drawLine(canvasLeft, calendarTop + calendarFontSpacing * 3, canvasRight, calendarTop + calendarFontSpacing * 3, calendarPaint);
// Paint paint = new Paint();
calendarPaint.setStyle(Paint.Style.FILL);
calendarPaint.setColor(Color.BLUE);
calendarPaint.setStrokeWidth(2);
startY = calendarTop + (int) calendarFontSpacing * 2;
int measureWeek = (int) calendarTextPaint.measureText("六");
int measureDay = (int) calendarTextPaint.measureText("30");
int measureText = measureWeek > measureDay ? measureWeek : measureDay;
List<String> weeks = List.of("日", "一", "二", "三", "四", "五", "六");
Calendar calendar = Calendar.getInstance();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM");
Date d1 = new SimpleDateFormat("yyyy-MM-dd").parse(startDate);//定义起始日期
Date d2 = new SimpleDateFormat("yyyy-MM-dd").parse(endDate);//定义结束日期
calendar.setTime(d2);
calendar.add(Calendar.DATE, 1);
d2 = calendar.getTime();
calendar.setTime(d1);//设置日期起始时间
while (calendar.getTime().before(d2)) {//判断是否到结束日期
String month = new SimpleDateFormat("yyyy-MM").format(calendar.getTime());
String day = new SimpleDateFormat("d").format(calendar.getTime());
coordinates.put(calendar.getTime(), new Coordinate(startX, startY));
// if (dateRange.containsKey(month)) {
// List<Date> tmp = dateRange.get(month);
// tmp.add(calendar.getTime());
// } else {
// dateRange.put(month, List.of(calendar.getTime()));
// }
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
String week = weeks.get(dayOfWeek - 1);
if (Set.of("六", "日").contains(week)) {
calendarPaint.setColor(Color.WHITE);
} else {
calendarPaint.setColor(Color.GRAY);
}
// Log.d(TAG, String.valueOf());
// Log.d(TAG, String.valueOf());
stopX = (int) (startX + measureText);
canvas.drawRect(startX, startY, stopX, stopY, calendarPaint);
canvas.drawText(week, textX, textY, calendarTextPaint);
if (calendarTextPaint.measureText(day) < calendarTextPaint.measureText(week)) {
canvas.drawText(day, textX + calendarTextPaint.measureText(day) / 2, textY + calendarFontSpacing, calendarTextPaint);
} else {
canvas.drawText(day, textX, textY + calendarFontSpacing, calendarTextPaint);
}
if (day.equals("1")) {
canvas.drawText(month, textX, textY - calendarFontSpacing, calendarTextPaint);
canvas.drawLine(startX, startY - calendarFontSpacing * 2, startX, stopY, calendarTextPaint);
}
if (week.equals("日")) {
canvas.drawLine(stopX, startY - calendarFontSpacing, stopX, stopY, calendarTextPaint);
}
textX += measureText + 2;
startX = textX;
calendar.add(Calendar.DATE, 1);//进行当前日期月份加1
}
calendarPaint.setColor(Color.GRAY);
canvas.drawLine(canvasLeft, calendarTop + calendarFontSpacing * 2, calendarRight, calendarTop + calendarFontSpacing * 2, calendarPaint);
canvas.drawLine(canvasLeft, calendarTop + calendarFontSpacing * 3, canvasRight, calendarTop + calendarFontSpacing * 3, calendarPaint);
calendarLeft = stopX;
}
public class Data {
public Data(String name, String start, String finish, String day, String resource) {
this.name = name;
this.start = start;
this.finish = finish;
this.day = day;
this.resource = resource;
}
public String name;
public String start;
public String finish;
public String day;
public String resource;
}
// public class Coordinate{
//
// }
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public int getTextSize() {
return textSize;
}
/**
* Gets the example dimension attribute value.
*
* @return The example dimension attribute value.
*/
// public float getExampleDimension() {
// return mExampleDimension;
// }
/**
* Sets the view"s example dimension attribute value. In the example view, this dimension
* is the font size.
*
* @param exampleDimension The example dimension attribute value to use.
*/
// public void setExampleDimension(float exampleDimension) {
// mExampleDimension = exampleDimension;
// invalidateTextPaintAndMeasurements();
// }
/**
* Gets the example drawable attribute value.
*
* @return The example drawable attribute value.
*/
// public Drawable getExampleDrawable() {
// return mExampleDrawable;
// }
/**
* Sets the view"s example drawable attribute value. In the example view, this drawable is
* drawn above the text.
*
* @param exampleDrawable The example drawable attribute value to use.
*/
// public void setExampleDrawable(Drawable exampleDrawable) {
// mExampleDrawable = exampleDrawable;
// }
private void week() {
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
// Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
// float fontSpacing = calendarTextPaint.getFontSpacing();
int textX = calendarLeft;
int textY = calendarTop + (int) calendarFontSpacing * 2;
int startX = calendarLeft;
int startY = calendarTop + (int) calendarFontSpacing * 2;
int stopX = 0;
int stopY = calendarBottom;
int measureWeek = (int) calendarTextPaint.measureText("六");
int measureDay = (int) calendarTextPaint.measureText("30");
int measureText = measureWeek > measureDay ? measureWeek : measureDay;
List<String> weeks = List.of("一", "二", "三", "四", "五", "六", "日");
int w = 0;
for (int i = 1; i <= 31; i++) {
// for (String week : List.of("一", "二", "三", "四", "五", "六", "日")) {
String week = weeks.get(w);
w++;
if (w >= weeks.size()) {
w = 0;
}
String day = String.valueOf(i);
if (Set.of("六", "日").contains(week)) {
paint.setColor(Color.WHITE);
} else {
paint.setColor(Color.GRAY);
}
// Log.d(TAG, String.valueOf());
// Log.d(TAG, String.valueOf());
stopX = (int) (startX + measureText);
canvas.drawRect(startX, startY, stopX, stopY, paint);
canvas.drawText(week, textX, textY, calendarTextPaint);
if (calendarTextPaint.measureText(day) < calendarTextPaint.measureText(week)) {
canvas.drawText(day, textX + calendarTextPaint.measureText(day) / 2, textY + calendarFontSpacing, calendarTextPaint);
} else {
canvas.drawText(day, textX, textY + calendarFontSpacing, calendarTextPaint);
}
// if (week.equals("一")) {
// canvas.drawLine(startX, startY - calendarFontSpacing, startX, stopY, calendarTextPaint);
// }
if (week.equals("日")) {
canvas.drawLine(stopX, startY - calendarFontSpacing, stopX, stopY, calendarTextPaint);
}
textX += measureText + 2;
startX = textX;
// stopX = (int) (startX + fontSpacing);
}
calendarLeft = stopX;
}
}