查看: 1421|回复: 0

[手机开发] Android自定义View实现仿GitHub的提交活跃表格

发表于 2017-6-24 08:00:01

说明

本文可能需要一些基础知识点,如Canvas,Paint,Path,Rect等类的基本使用,建议不熟悉的同学可以学习GcsSloop安卓自定义View教程目录,会帮助很大。

github.jpg

上图就是github的提交表格,直观来看可以分为几个部分进行绘制:

(1)各个月份的小方格子,并且色彩根据提交次数变化,由浅到深
(2)右下边的颜色标志,我们右对齐就可以了
(3)左边的星期,原图是从周日画到周六,我们从周一画到周日
(4)上面的月份,我们只画出1-12月
(5)点击时候弹出当天的提交情况,由一个小三角和圆角矩形组成

需要解决的计算问题:

(1)生成任意一年的所有天,包含年月日周,提交次数,色块颜色,坐标
(1)一年中所有的小方格子坐标
(2)右下边颜色标志坐标
(3)左边星期坐标
(4)上面月份坐标
(5)点击弹出的提示框和文字坐标

生成某年所有天数

每天的信息我们需要封装成一个类,代码如下:

  1. /**
  2. * Created by Administrator on 2017/1/13.
  3. * 封装每天的属性,方便在绘制的时候进行计算
  4. */
  5. public class Day implements Serializable{
  6. /**年**/
  7. public int year;
  8. /**月**/
  9. public int month;
  10. /**日**/
  11. public int date;
  12. /**周几**/
  13. public int week;
  14. /**贡献次数,默认0**/
  15. public int contribution = 0;
  16. /**默认颜色,根据提交次数改变**/
  17. public int colour = 0xFFEEEEEE;
  18. /**方格坐标,左上点,右下点,确定矩形范围**/
  19. public float startX;
  20. public float startY;
  21. public float endX;
  22. public float endY;
  23. @Override
  24. public String toString() {
  25. //这里直接在弹出框中显示
  26. return ""+year+"年"+month+"月"+date+"日周"+week+","+contribution+"次";
  27. }
  28. }
复制代码

要想先绘制表格,需要计算出所有的天,这里计算一年中所有的天,我们通过从当年1月1日算起,到12月31日,因为星期是连续的,所以我们需要我们提供某年的1月1日是周几,比如2016年1月1日是周5,这里必要的参数是2016和周5,那么我们用一个类来实现该方法,代码如下:

  1. public class DateFactory {
  2. /**平年map,对应月份和天数**/
  3. private static HashMap<Integer,Integer> monthMap = new LinkedHashMap<>(12);
  4. /**闰年map,对应月份和天数**/
  5. private static HashMap<Integer,Integer> leapMonthMap = new LinkedHashMap<>(12);
  6. static {
  7. //初始化map,只有2月份不同
  8. monthMap.put(1,31);leapMonthMap.put(1,31);
  9. monthMap.put(2,28);leapMonthMap.put(2,29);
  10. monthMap.put(3,31);leapMonthMap.put(3,31);
  11. monthMap.put(4,30);leapMonthMap.put(4,30);
  12. monthMap.put(5,31);leapMonthMap.put(5,31);
  13. monthMap.put(6,30);leapMonthMap.put(6,30);
  14. monthMap.put(7,31);leapMonthMap.put(7,31);
  15. monthMap.put(8,31);leapMonthMap.put(8,31);
  16. monthMap.put(9,30);leapMonthMap.put(9,30);
  17. monthMap.put(10,31);leapMonthMap.put(10,31);
  18. monthMap.put(11,30);leapMonthMap.put(11,30);
  19. monthMap.put(12,31);leapMonthMap.put(12,31);
  20. }
  21. /**
  22. * 输入年份和1月1日是周几
  23. * 闰年为366天,平年为365天
  24. * @param year 年份
  25. * @param weekday 该年1月1日为周几
  26. * @return 该年1月1日到12月31日所有的天数
  27. */
  28. public static List<Day> getDays(int year, int weekday) {
  29. List<Day> days = new ArrayList<>();
  30. boolean isLeapYear = isLeapYear(year);
  31. int dayNum = isLeapYear ? 366 : 365;
  32. Day day;
  33. int lastWeekday = weekday;
  34. for (int i = 1; i <= dayNum; i++) {
  35. day = new Day();
  36. day.year = year;
  37. //计算当天为周几,如果大于7就重置1
  38. day.week = lastWeekday<= 7 ? lastWeekday : 1;
  39. //计算当天为几月几号
  40. int[] monthAndDay = getMonthAndDay(isLeapYear, i);
  41. day.month = monthAndDay[0];
  42. day.date = monthAndDay[1];
  43. //记录下昨天是周几并+1
  44. lastWeekday = day.week;
  45. lastWeekday++;
  46. days.add(day);
  47. }
  48. checkDays(days);
  49. return days;
  50. }
  51. /**
  52. * 获取月和日
  53. * @param isLeapYear 是否闰年
  54. * @param currentDay 当前天数
  55. * @return 包含月和天的数组
  56. */
  57. public static int[] getMonthAndDay(boolean isLeapYear,int currentDay) {
  58. HashMap<Integer,Integer> maps = isLeapYear?leapMonthMap:monthMap;
  59. Set<Map.Entry<Integer,Integer>> set = maps.entrySet();
  60. int count = 0;
  61. Map.Entry<Integer, Integer> month = null;
  62. for (Map.Entry<Integer, Integer> entry : set) {
  63. count+=entry.getValue();
  64. if (currentDay<=count){
  65. month = entry;
  66. break;
  67. }
  68. }
  69. if (month == null){
  70. throw new IllegalStateException("未找到所在的月份");
  71. }
  72. int day = month.getValue()-(count-currentDay);
  73. return new int[]{month.getKey(),day};
  74. }
  75. /**
  76. * 判断是闰年还是平年
  77. * @param year 年份
  78. * @return true 为闰年
  79. */
  80. public static boolean isLeapYear(int year) {
  81. return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
  82. }
  83. /**
  84. * 检测生成的天数是否正常
  85. * @param days
  86. */
  87. private static void checkDays(List<Day> days) {
  88. if (days == null) {
  89. throw new IllegalArgumentException("天数为空");
  90. }
  91. if (days.size() != 365 && days.size() != 366) {
  92. throw new IllegalArgumentException("天数异常:" + days.size());
  93. }
  94. }
  95. public static void main(String[] args){
  96. //test
  97. List<Day> days = DateFactory.getDays(2016, 5);
  98. for (int i = 0; i < days.size(); i++) {
  99. System.out.println(days.get(i).toString());
  100. }
  101. }
  102. }
复制代码

具体的计算逻辑可以看看代码,不是很难,这样我们就能得到某年的所有天。

绘制天数格子

因为该view比较长,所以需要横屏显示,方便起见,这里我们也不再进行view的测量计算,也不再进行自定义属性,只关注其核心逻辑即可。

首先我们需要将需要的成员变量定义出来:

  1. /**灰色方格的默认颜色**/
  2. private final static int DEFAULT_BOX_COLOUR = 0xFFEEEEEE;
  3. /**提交次数颜色值**/
  4. private final static int[] COLOUR_LEVEL =
  5. new int[]{0xFF1E6823, 0xFF44A340, 0xFF8CC665, 0xFFD6E685, DEFAULT_BOX_COLOUR};
  6. /**星期**/
  7. private String[] weeks = new String[]{"Mon", "Wed", "Fri", "Sun"};
  8. /**月份**/
  9. private String[] months =
  10. new String[]{"Jan", "Feb", "Mar", "Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
  11. /**默认的padding,绘制的时候不贴边画**/
  12. private int padding = 24;
  13. /**小方格的默认边长**/
  14. private int boxSide = 8;
  15. /**小方格间的默认间隔**/
  16. private int boxInterval = 2;
  17. /**所有周的列数**/
  18. private int column = 0;
  19. private List<Day> mDays;//一年中所有的天
  20. private Paint boxPaint;//方格画笔
  21. private Paint textPaint;//文字画笔
  22. private Paint infoPaint;//弹出框画笔
  23. private Paint.FontMetrics metrics;//测量文字
  24. private float downX;//按下的点的X坐标
  25. private float downY;//按下的点的Y坐标
  26. private Day clickDay;//按下所对应的天
复制代码

这些提取的变量是慢慢增加的,在自定义的时候一下想不全的时候可以先写,等用到某些变量的时候就提取出来。
然后我们初始化一下数据:

  1. public GitHubContributionView(Context context, AttributeSet attrs, int defStyleAttr) {
  2. super(context, attrs, defStyleAttr);
  3. initView();
  4. }
  5. public void initView() {
  6. mDays = DateFactory.getDays(2016, 5);
  7. //方格画笔
  8. boxPaint = new Paint();
  9. boxPaint.setStyle(Paint.Style.FILL);
  10. boxPaint.setStrokeWidth(2);
  11. boxPaint.setColor(DEFAULT_BOX_COLOUR);
  12. boxPaint.setAntiAlias(true);
  13. //文字画笔
  14. textPaint = new Paint();
  15. textPaint.setStyle(Paint.Style.FILL);
  16. textPaint.setColor(Color.GRAY);
  17. textPaint.setTextSize(12);
  18. textPaint.setAntiAlias(true);
  19. //弹出的方格信息画笔
  20. infoPaint = new Paint();
  21. infoPaint.setStyle(Paint.Style.FILL);
  22. infoPaint.setColor(0xCC888888);
  23. infoPaint.setTextSize(12);
  24. infoPaint.setAntiAlias(true);
  25. //将默认值转换px
  26. padding = UI.dp2px(getContext(), padding);
  27. boxSide = UI.dp2px(getContext(), boxSide);
  28. metrics = textPaint.getFontMetrics();
  29. }
复制代码

这里我们以2016年来举例,mDays就是获取2016年的所有天的集合(参数可以当作自定义属性提取出来),相关的Paint也已经初始化好了,接下来就需要在onDraw方法里画,先画所有的方格子和月份标志:

  1. /**
  2. * 画出1-12月方格小块和上面的月份
  3. * @param canvas 画布
  4. */
  5. private void drawBox(Canvas canvas) {
  6. //方格的左上右下坐标
  7. float startX, startY, endX, endY;
  8. //起始月份为1月
  9. int month = 1;
  10. for (int i = 0; i < mDays.size(); i++) {
  11. Day day = mDays.get(i);
  12. if (i == 0){
  13. //画1月的文本标记,坐标应该是x=padding,y=padding-boxSide/2(间隙),y坐标在表格上面一点
  14. canvas.drawText(months[0],padding,padding-boxSide/2,textPaint);
  15. }
  16. if (day.week == 1 && i != 0) {
  17. //如果当天是周1,那么说明增加了一列
  18. column++;
  19. //如果列首的月份有变化,那么说明需要画月份
  20. if (day.month>month){
  21. month = day.month;
  22. //月份文本的坐标计算,x坐标在变化,而y坐标都是一样的,boxSide/2(间隙)
  23. canvas.drawText(months[month-1],padding+column*(boxSide+boxInterval),padding-boxSide/2,textPaint);
  24. }
  25. }
  26. //计算方格坐标点,x坐标随列数的增多而增加,y坐标随行数的增多而变化
  27. startX = padding + column * (boxSide + boxInterval);
  28. startY = padding + (day.week - 1) * (boxSide + boxInterval);
  29. endX = startX + boxSide;
  30. endY = startY + boxSide;
  31. //将该方格的坐标保存下来,这样可以在点击方格的时候计算弹框的坐标
  32. day.startX = startX;
  33. day.startY = startY;
  34. day.endX = endX;
  35. day.endY = endY;
  36. //给画笔设置当前天的颜色
  37. boxPaint.setColor(day.colour);
  38. canvas.drawRect(startX, startY, endX, endY, boxPaint);
  39. }
  40. boxPaint.setColor(DEFAULT_BOX_COLOUR);//恢复默认颜色
  41. }
复制代码

这里主要是注意下行数列数的变化和月份坐标的计算,格子画好了。

绘制星期文本

我们再画左边的星期文本:

  1. /**
  2. * 画左侧的星期
  3. * @param canvas 画布
  4. */
  5. private void drawWeek(Canvas canvas) {
  6. //文字是左对齐,所以找出最长的字
  7. float textLength = 0;
  8. for (String week : weeks) {
  9. float tempLength = textPaint.measureText(week);
  10. if (textLength < tempLength) {
  11. textLength = tempLength;
  12. }
  13. }
  14. //依次画出星期文本,坐标点x=padding-文本长度-文本和方格的间隙,y坐标随行数变化
  15. canvas.drawText(weeks[0], padding - textLength - 2, padding + boxSide - metrics.descent, textPaint);
  16. canvas.drawText(weeks[1], padding - textLength - 2, padding + 3 * (boxSide + boxInterval) - metrics.descent, textPaint);
  17. canvas.drawText(weeks[2], padding - textLength - 2, padding + 5 * (boxSide + boxInterval) - metrics.descent, textPaint);
  18. canvas.drawText(weeks[3], padding - textLength - 2, padding + 7 * (boxSide + boxInterval) - metrics.descent, textPaint);
  19. }
复制代码

绘制颜色深浅标志

然后根据表格的高度再画出右下边的颜色深浅标志:

  1. /**
  2. * 画出右下角的颜色深浅标志,因为是右对齐的所以需要从右往左画
  3. * @param canvas 画布
  4. */
  5. private void drawTag(Canvas canvas) {
  6. //首先计算出两个文本的长度
  7. float moreLength = textPaint.measureText("More");
  8. float lessLength = textPaint.measureText("Less");
  9. //画 More 文本,x坐标=padding+(列数+1)*(方格边长+方格间隙)-一个方格间隙-文本长度
  10. float moreX = padding + (column + 1) * (boxSide + boxInterval) - boxInterval - moreLength;
  11. //y坐标=padding+(方格行数+1,和表格底部有些距离)*(方格边长+方格间隙)+字体的ascent高度
  12. float moreY = padding + 8 * (boxSide + boxInterval) + Math.abs(metrics.ascent);
  13. canvas.drawText("More", moreX, moreY, textPaint);
  14. //画深浅色块,坐标根据上面的More依次计算就可以了
  15. float interval = boxSide - 2;//文字和色块间的距离
  16. float leftX = moreX - interval - boxSide;
  17. float topY = moreY - boxSide;
  18. float rightX = moreX - interval;
  19. float bottomY = moreY;//色块的Y坐标是一样的
  20. for (int i = 0; i < COLOUR_LEVEL.length; i++) {
  21. boxPaint.setColor(COLOUR_LEVEL[i]);
  22. canvas.drawRect(leftX - i * (boxSide + boxInterval), topY, rightX - i * (boxSide + boxInterval), bottomY, boxPaint);
  23. }
  24. //最后画 Less 文本,原理同上
  25. canvas.drawText("Less", leftX - 4 * (boxSide + boxInterval) - interval - lessLength, moreY, textPaint);
  26. }
复制代码

这样整个表格主体绘制完成。

处理点击事件

接下来要处理点击事件,判断点击的坐标如果在方格内,那么弹出对于的文本框,先处理点击事件:

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. //获取ACTION_DOWN的坐标,用来判断点在哪天,并弹出·
  4. if (MotionEvent.ACTION_DOWN == event.getAction()) {
  5. downX = event.getX();
  6. downY = event.getY();
  7. findClickBox();
  8. }
  9. //这里因为我们只是记录坐标点,不对事件进行拦截所以默认返回
  10. return super.onTouchEvent(event);
  11. }
复制代码

判断是否在方格内:

  1. /**
  2. * 判断是否点击在方格内
  3. */
  4. private void findClickBox() {
  5. for (Day day : mDays) {
  6. //检测点击的坐标如果在方格内,则弹出信息提示
  7. if (downX >= day.startX && downX <= day.endX && downY >= day.startY && downY <= day.endY) {
  8. clickDay = day;//纪录点击的哪天
  9. break;
  10. }
  11. }
  12. //点击完要刷新,这样每次点击不同的方格,弹窗就可以在相应的位置显示
  13. refreshView();
  14. }
  15. /**
  16. * 点击弹出文字提示
  17. */
  18. private void refreshView() {
  19. invalidate();
  20. }
复制代码

绘制弹出文本框

然后看看弹出文本框的绘制:

  1. /**
  2. * 画方格上的文字弹框
  3. * @param canvas 画布
  4. */
  5. private void drawPopupInfo(Canvas canvas) {
  6. if (clickDay != null) {//点击的天不为null时候才画
  7. //先根据方格来画出一个小三角形,坐标就是方格的中间
  8. Path infoPath = new Path();
  9. //先从方格中心
  10. infoPath.moveTo(clickDay.startX + boxSide / 2, clickDay.startY + boxSide / 2);
  11. //然后是方格的左上点
  12. infoPath.lineTo(clickDay.startX, clickDay.startY);
  13. //然后是方格的右上点
  14. infoPath.lineTo(clickDay.endX, clickDay.startY);
  15. //画出三角
  16. canvas.drawPath(infoPath,infoPaint);
  17. //画三角上的圆角矩形
  18. textPaint.setColor(Color.WHITE);
  19. //得到当天的文本信息
  20. String popupInfo = clickDay.toString();
  21. System.out.println(popupInfo);
  22. //计算文本的高度和长度用以确定矩形的大小
  23. float infoHeight = metrics.descent - metrics.ascent;
  24. float infoLength = textPaint.measureText(popupInfo);
  25. Log.e("height",infoHeight+"");
  26. Log.e("length",infoLength+"");
  27. //矩形左上点应该是x=当前天的x+边长/2-(文本长度/2+文本和框的间隙)
  28. float leftX = (clickDay.startX + boxSide / 2 ) - (infoLength / 2 + boxSide);
  29. //矩形左上点应该是y=当前天的y+边长/2-(文本高度+上下文本和框的间隙)
  30. float topY = clickDay.startY-(infoHeight+2*boxSide);
  31. //矩形的右下点应该是x=leftX+文本长度+文字两边和矩形的间距
  32. float rightX = leftX+infoLength+2*boxSide;
  33. //矩形的右下点应该是y=当前天的y
  34. float bottomY = clickDay.startY;
  35. System.out.println(""+leftX+"/"+topY+"/"+rightX+"/"+bottomY);
  36. RectF rectF = new RectF(leftX, topY, rightX, bottomY);
  37. canvas.drawRoundRect(rectF,4,4,infoPaint);
  38. //绘制文字,x=leftX+文字和矩形间距,y=topY+文字和矩形上面间距+文字顶到基线高度
  39. canvas.drawText(popupInfo,leftX+boxSide,topY+boxSide+Math.abs(metrics.ascent),textPaint);
  40. clickDay = null;//重新置空,保证点击方格外信息消失
  41. textPaint.setColor(Color.GRAY);//恢复画笔颜色
  42. }
  43. }
复制代码

这样主体逻辑完成,但需要开放设置某天提交次数的方法:

  1. /**
  2. * 设置某天的次数
  3. * @param year 年
  4. * @param month 月
  5. * @param day 日
  6. * @param contribution 次数
  7. */
  8. public void setData(int year,int month,int day,int contribution){
  9. //先找到是第几天,为了方便不做参数检测了
  10. for (Day d : mDays) {
  11. if (d.year == year && d.month == month && d.date == day){
  12. d.contribution = contribution;
  13. d.colour = getColour(contribution);
  14. break;
  15. }
  16. }
  17. refreshView();
  18. }
  19. /**
  20. * 根据提交次数来获取颜色值
  21. * @param contribution 提交的次数
  22. * @return 颜色值
  23. */
  24. private int getColour(int contribution){
  25. int colour = 0;
  26. if (contribution <= 0){
  27. colour = COLOUR_LEVEL[4];
  28. }
  29. if (contribution == 1){
  30. colour = COLOUR_LEVEL[3];
  31. }
  32. if (contribution == 2){
  33. colour = COLOUR_LEVEL[2];
  34. }
  35. if (contribution == 3){
  36. colour = COLOUR_LEVEL[1];
  37. }
  38. if (contribution >= 4){
  39. colour = COLOUR_LEVEL[0];
  40. }
  41. return colour;
  42. }
复制代码

好了,所有逻辑完成,主要涉及到一些计算,完整代码:

  1. /**
  2. * Created by Administrator on 2017/1/13.
  3. * 仿GitHub的提交活跃表
  4. * 横屏使用
  5. */
  6. public class GitHubContributionView extends View {
  7. /**灰色方格的默认颜色**/
  8. private final static int DEFAULT_BOX_COLOUR = 0xFFEEEEEE;
  9. /**提交次数颜色值**/
  10. private final static int[] COLOUR_LEVEL =
  11. new int[]{0xFF1E6823, 0xFF44A340, 0xFF8CC665, 0xFFD6E685, DEFAULT_BOX_COLOUR};
  12. /**星期**/
  13. private String[] weeks = new String[]{"Mon", "Wed", "Fri", "Sun"};
  14. /**月份**/
  15. private String[] months =
  16. new String[]{"Jan", "Feb", "Mar", "Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
  17. /**默认的padding,绘制的时候不贴边画**/
  18. private int padding = 24;
  19. /**小方格的默认边长**/
  20. private int boxSide = 8;
  21. /**小方格间的默认间隔**/
  22. private int boxInterval = 2;
  23. /**所有周的列数**/
  24. private int column = 0;
  25. private List<Day> mDays;//一年中所有的天
  26. private Paint boxPaint;//方格画笔
  27. private Paint textPaint;//文字画笔
  28. private Paint infoPaint;//弹出框画笔
  29. private Paint.FontMetrics metrics;//测量文字
  30. private float downX;//按下的点的X坐标
  31. private float downY;//按下的点的Y坐标
  32. private Day clickDay;//按下所对应的天
  33. public GitHubContributionView(Context context) {
  34. this(context, null);
  35. }
  36. public GitHubContributionView(Context context, AttributeSet attrs) {
  37. this(context, attrs, 0);
  38. }
  39. public GitHubContributionView(Context context, AttributeSet attrs, int defStyleAttr) {
  40. super(context, attrs, defStyleAttr);
  41. initView();
  42. }
  43. public void initView() {
  44. mDays = DateFactory.getDays(2016, 5);
  45. //方格画笔
  46. boxPaint = new Paint();
  47. boxPaint.setStyle(Paint.Style.FILL);
  48. boxPaint.setStrokeWidth(2);
  49. boxPaint.setColor(DEFAULT_BOX_COLOUR);
  50. boxPaint.setAntiAlias(true);
  51. //文字画笔
  52. textPaint = new Paint();
  53. textPaint.setStyle(Paint.Style.FILL);
  54. textPaint.setColor(Color.GRAY);
  55. textPaint.setTextSize(12);
  56. textPaint.setAntiAlias(true);
  57. //弹出的方格信息画笔
  58. infoPaint = new Paint();
  59. infoPaint.setStyle(Paint.Style.FILL);
  60. infoPaint.setColor(0xCC888888);
  61. infoPaint.setTextSize(12);
  62. infoPaint.setAntiAlias(true);
  63. //将默认值转换px
  64. padding = UI.dp2px(getContext(), padding);
  65. boxSide = UI.dp2px(getContext(), boxSide);
  66. metrics = textPaint.getFontMetrics();
  67. }
  68. @Override
  69. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  70. super.onSizeChanged(w, h, oldw, oldh);
  71. }
  72. @Override
  73. protected void onDraw(Canvas canvas) {
  74. super.onDraw(canvas);
  75. column = 0;
  76. canvas.save();
  77. drawBox(canvas);
  78. drawWeek(canvas);
  79. drawTag(canvas);
  80. drawPopupInfo(canvas);
  81. canvas.restore();
  82. }
  83. /**
  84. * 画出1-12月方格小块和上面的月份
  85. * @param canvas 画布
  86. */
  87. private void drawBox(Canvas canvas) {
  88. //方格的左上右下坐标
  89. float startX, startY, endX, endY;
  90. //起始月份为1月
  91. int month = 1;
  92. for (int i = 0; i < mDays.size(); i++) {
  93. Day day = mDays.get(i);
  94. if (i == 0){
  95. //画1月的文本标记,坐标应该是x=padding,y=padding-boxSide/2(间隙),y坐标在表格上面一点
  96. canvas.drawText(months[0],padding,padding-boxSide/2,textPaint);
  97. }
  98. if (day.week == 1 && i != 0) {
  99. //如果当天是周1,那么说明增加了一列
  100. column++;
  101. //如果列首的月份有变化,那么说明需要画月份
  102. if (day.month>month){
  103. month = day.month;
  104. //月份文本的坐标计算,x坐标在变化,而y坐标都是一样的,boxSide/2(间隙)
  105. canvas.drawText(months[month-1],padding+column*(boxSide+boxInterval),padding-boxSide/2,textPaint);
  106. }
  107. }
  108. //计算方格坐标点,x坐标一致随列数的增多而增加,y坐标随行数的增多而变化
  109. startX = padding + column * (boxSide + boxInterval);
  110. startY = padding + (day.week - 1) * (boxSide + boxInterval);
  111. endX = startX + boxSide;
  112. endY = startY + boxSide;
  113. //将该方格的坐标保存下来,这样可以在点击方格的时候计算弹框的坐标
  114. day.startX = startX;
  115. day.startY = startY;
  116. day.endX = endX;
  117. day.endY = endY;
  118. //给画笔设置当前天的颜色
  119. boxPaint.setColor(day.colour);
  120. canvas.drawRect(startX, startY, endX, endY, boxPaint);
  121. }
  122. boxPaint.setColor(DEFAULT_BOX_COLOUR);//恢复默认颜色
  123. }
  124. /**
  125. * 画左侧的星期
  126. * @param canvas 画布
  127. */
  128. private void drawWeek(Canvas canvas) {
  129. //文字是左对齐,所以找出最长的字
  130. float textLength = 0;
  131. for (String week : weeks) {
  132. float tempLength = textPaint.measureText(week);
  133. if (textLength < tempLength) {
  134. textLength = tempLength;
  135. }
  136. }
  137. //依次画出星期文本,坐标点x=padding-文本长度-文本和方格的间隙,y坐标随行数变化
  138. canvas.drawText(weeks[0], padding - textLength - 2, padding + boxSide - metrics.descent, textPaint);
  139. canvas.drawText(weeks[1], padding - textLength - 2, padding + 3 * (boxSide + boxInterval) - metrics.descent, textPaint);
  140. canvas.drawText(weeks[2], padding - textLength - 2, padding + 5 * (boxSide + boxInterval) - metrics.descent, textPaint);
  141. canvas.drawText(weeks[3], padding - textLength - 2, padding + 7 * (boxSide + boxInterval) - metrics.descent, textPaint);
  142. }
  143. /**
  144. * 画出右下角的颜色深浅标志,因为是右对齐的所以需要从右往左画
  145. * @param canvas 画布
  146. */
  147. private void drawTag(Canvas canvas) {
  148. //首先计算出两个文本的长度
  149. float moreLength = textPaint.measureText("More");
  150. float lessLength = textPaint.measureText("Less");
  151. //画 More 文本,x坐标=padding+(列数+1)*(方格边长+方格间隙)-一个方格间隙-文本长度
  152. float moreX = padding + (column + 1) * (boxSide + boxInterval) - boxInterval - moreLength;
  153. //y坐标=padding+(方格行数+1,和表格底部有些距离)*(方格边长+方格间隙)+字体的ascent高度
  154. float moreY = padding + 8 * (boxSide + boxInterval) + Math.abs(metrics.ascent);
  155. canvas.drawText("More", moreX, moreY, textPaint);
  156. //画深浅色块,坐标根据上面的More依次计算就可以了
  157. float interval = boxSide - 2;//文字和色块间的距离
  158. float leftX = moreX - interval - boxSide;
  159. float topY = moreY - boxSide;
  160. float rightX = moreX - interval;
  161. float bottomY = moreY;//色块的Y坐标是一样的
  162. for (int i = 0; i < COLOUR_LEVEL.length; i++) {
  163. boxPaint.setColor(COLOUR_LEVEL[i]);
  164. canvas.drawRect(leftX - i * (boxSide + boxInterval), topY, rightX - i * (boxSide + boxInterval), bottomY, boxPaint);
  165. }
  166. //最后画 Less 文本,原理同上
  167. canvas.drawText("Less", leftX - 4 * (boxSide + boxInterval) - interval - lessLength, moreY, textPaint);
  168. }
  169. @Override
  170. public boolean onTouchEvent(MotionEvent event) {
  171. //获取点击时候的坐标,用来判断点在哪天,并弹出·
  172. if (MotionEvent.ACTION_DOWN == event.getAction()) {
  173. downX = event.getX();
  174. downY = event.getY();
  175. findClickBox();
  176. }
  177. return super.onTouchEvent(event);
  178. }
  179. /**
  180. * 判断是否点击在方格内
  181. */
  182. private void findClickBox() {
  183. for (Day day : mDays) {
  184. //检测点击的坐标如果在方格内,则弹出信息提示
  185. if (downX >= day.startX && downX <= day.endX && downY >= day.startY && downY <= day.endY) {
  186. clickDay = day;//纪录点击的哪天
  187. break;
  188. }
  189. }
  190. //点击完要刷新,这样每次点击不同的方格,弹窗就可以在相应的位置显示
  191. refreshView();
  192. }
  193. /**
  194. * 点击弹出文字提示
  195. */
  196. private void refreshView() {
  197. invalidate();
  198. }
  199. /**
  200. * 画方格上的文字弹框
  201. * @param canvas 画布
  202. */
  203. private void drawPopupInfo(Canvas canvas) {
  204. if (clickDay != null) {
  205. //先根据方格来画出一个小三角形,坐标就是方格的中间
  206. Path infoPath = new Path();
  207. //先从方格中心
  208. infoPath.moveTo(clickDay.startX + boxSide / 2, clickDay.startY + boxSide / 2);
  209. //然后是方格的左上点
  210. infoPath.lineTo(clickDay.startX, clickDay.startY);
  211. //然后是方格的右上点
  212. infoPath.lineTo(clickDay.endX, clickDay.startY);
  213. //画出三角
  214. canvas.drawPath(infoPath,infoPaint);
  215. //画三角上的圆角矩形
  216. textPaint.setColor(Color.WHITE);
  217. //得到当天的文本信息
  218. String popupInfo = clickDay.toString();
  219. System.out.println(popupInfo);
  220. //计算文本的高度和长度用以确定矩形的大小
  221. float infoHeight = metrics.descent - metrics.ascent;
  222. float infoLength = textPaint.measureText(popupInfo);
  223. Log.e("height",infoHeight+"");
  224. Log.e("length",infoLength+"");
  225. //矩形左上点应该是x=当前天的x+边长/2-(文本长度/2+文本和框的间隙)
  226. float leftX = (clickDay.startX + boxSide / 2 ) - (infoLength / 2 + boxSide);
  227. //矩形左上点应该是y=当前天的y+边长/2-(文本高度+上下文本和框的间隙)
  228. float topY = clickDay.startY-(infoHeight+2*boxSide);
  229. //矩形的右下点应该是x=leftX+文本长度+文字两边和矩形的间距
  230. float rightX = leftX+infoLength+2*boxSide;
  231. //矩形的右下点应该是y=当前天的y
  232. float bottomY = clickDay.startY;
  233. System.out.println(""+leftX+"/"+topY+"/"+rightX+"/"+bottomY);
  234. RectF rectF = new RectF(leftX, topY, rightX, bottomY);
  235. canvas.drawRoundRect(rectF,4,4,infoPaint);
  236. //绘制文字,x=leftX+文字和矩形间距,y=topY+文字和矩形上面间距+文字顶到基线高度
  237. canvas.drawText(popupInfo,leftX+boxSide,topY+boxSide+Math.abs(metrics.ascent),textPaint);
  238. clickDay = null;//重新置空,保证点击方格外信息消失
  239. textPaint.setColor(Color.GRAY);//恢复画笔颜色
  240. }
  241. }
  242. /**
  243. * 设置某天的次数
  244. * @param year 年
  245. * @param month 月
  246. * @param day 日
  247. * @param contribution 次数
  248. */
  249. public void setData(int year,int month,int day,int contribution){
  250. //先找到是第几天,为了方便不做参数检测了
  251. for (Day d : mDays) {
  252. if (d.year == year && d.month == month && d.date == day){
  253. d.contribution = contribution;
  254. d.colour = getColour(contribution);
  255. break;
  256. }
  257. }
  258. refreshView();
  259. }
  260. /**
  261. * 根据提交次数来获取颜色值
  262. * @param contribution 提交的次数
  263. * @return 颜色值
  264. */
  265. private int getColour(int contribution){
  266. int colour = 0;
  267. if (contribution <= 0){
  268. colour = COLOUR_LEVEL[4];
  269. }
  270. if (contribution == 1){
  271. colour = COLOUR_LEVEL[3];
  272. }
  273. if (contribution == 2){
  274. colour = COLOUR_LEVEL[2];
  275. }
  276. if (contribution == 3){
  277. colour = COLOUR_LEVEL[1];
  278. }
  279. if (contribution >= 4){
  280. colour = COLOUR_LEVEL[0];
  281. }
  282. return colour;
  283. }
  284. }
复制代码

这样弄个布局测试下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:gravity="center"
  7. android:orientation="vertical"
  8. >
  9. <com.franky.custom.view.GitHubContributionView
  10. android:id="@+id/cc_chart"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent"
  13. />
  14. </LinearLayout>
复制代码

随机弄些数据:

  1. public class MainActivity extends AppCompatActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. GitHubContributionView github = (GitHubContributionView) findViewById(R.id.cc_chart);
  7. github.setData(2016,12,9,2);
  8. github.setData(2016,11,9,1);
  9. github.setData(2016,10,5,10);
  10. github.setData(2016,8,9,3);
  11. github.setData(2016,4,20,2);
  12. github.setData(2016,12,13,3);
  13. github.setData(2016,12,14,3);
  14. github.setData(2016,2,15,4);
  15. }
  16. }
复制代码

效果

gif没有录好,看看图片效果:

效果.png

查看源码

以上所述是小编给大家介绍的Android自定义View实现仿GitHub的提交活跃表格,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对程序员之家网站的支持!



回复

使用道具 举报