需要做一个项目管理工具,其中使用到了甘特图。发现全网甘特图解决方案比较少,于是自动动手丰衣足食。
前面我用 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"><ScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"></ScrollView><HorizontalScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"><cn.netkiller.gantt.ui.GanttViewandroid: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;}@Overridepublic 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"));@Overrideprotected 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;}
}