欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义View实现仿GitHub的提交活跃表格

程序员文章站 2024-02-21 17:22:04
说明 本文可能需要一些基础知识点,如canvas,paint,path,rect等类的基本使用,建议不熟悉的同学可以学习gcssloop安卓自定义view教程目录,会帮助...

说明

本文可能需要一些基础知识点,如canvas,paint,path,rect等类的基本使用,建议不熟悉的同学可以学习gcssloop安卓自定义view教程目录,会帮助很大。

Android自定义View实现仿GitHub的提交活跃表格

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

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

需要解决的计算问题:

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

生成某年所有天数

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

/**
 * created by administrator on 2017/1/13.
 * 封装每天的属性,方便在绘制的时候进行计算
 */
public class day implements serializable{
 /**年**/
 public int year;
 /**月**/
 public int month;
 /**日**/
 public int date;
 /**周几**/
 public int week;
 /**贡献次数,默认0**/
 public int contribution = 0;
 /**默认颜色,根据提交次数改变**/
 public int colour = 0xffeeeeee;
 /**方格坐标,左上点,右下点,确定矩形范围**/
 public float startx;
 public float starty;
 public float endx;
 public float endy;
 @override
 public string tostring() {
  //这里直接在弹出框中显示
  return ""+year+"年"+month+"月"+date+"日周"+week+","+contribution+"次";
 }
}

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

public class datefactory {
 /**平年map,对应月份和天数**/
 private static hashmap<integer,integer> monthmap = new linkedhashmap<>(12);
 /**闰年map,对应月份和天数**/
 private static hashmap<integer,integer> leapmonthmap = new linkedhashmap<>(12);
 static {
  //初始化map,只有2月份不同
  monthmap.put(1,31);leapmonthmap.put(1,31);
  monthmap.put(2,28);leapmonthmap.put(2,29);
  monthmap.put(3,31);leapmonthmap.put(3,31);
  monthmap.put(4,30);leapmonthmap.put(4,30);
  monthmap.put(5,31);leapmonthmap.put(5,31);
  monthmap.put(6,30);leapmonthmap.put(6,30);
  monthmap.put(7,31);leapmonthmap.put(7,31);
  monthmap.put(8,31);leapmonthmap.put(8,31);
  monthmap.put(9,30);leapmonthmap.put(9,30);
  monthmap.put(10,31);leapmonthmap.put(10,31);
  monthmap.put(11,30);leapmonthmap.put(11,30);
  monthmap.put(12,31);leapmonthmap.put(12,31);
 }
 /**
  * 输入年份和1月1日是周几
  * 闰年为366天,平年为365天
  * @param year 年份
  * @param weekday 该年1月1日为周几
  * @return 该年1月1日到12月31日所有的天数
  */
 public static list<day> getdays(int year, int weekday) {
  list<day> days = new arraylist<>();
  boolean isleapyear = isleapyear(year);
  int daynum = isleapyear ? 366 : 365;
  day day;
  int lastweekday = weekday;
  for (int i = 1; i <= daynum; i++) {
   day = new day();
   day.year = year;
   //计算当天为周几,如果大于7就重置1
   day.week = lastweekday<= 7 ? lastweekday : 1;
   //计算当天为几月几号
   int[] monthandday = getmonthandday(isleapyear, i);
   day.month = monthandday[0];
   day.date = monthandday[1];
   //记录下昨天是周几并+1
   lastweekday = day.week;
   lastweekday++;
   days.add(day);
  }
  checkdays(days);
  return days;
 }
 /**
  * 获取月和日
  * @param isleapyear 是否闰年
  * @param currentday 当前天数
  * @return 包含月和天的数组
  */
 public static int[] getmonthandday(boolean isleapyear,int currentday) {
  hashmap<integer,integer> maps = isleapyear?leapmonthmap:monthmap;
  set<map.entry<integer,integer>> set = maps.entryset();
  int count = 0;
  map.entry<integer, integer> month = null;
  for (map.entry<integer, integer> entry : set) {
   count+=entry.getvalue();
   if (currentday<=count){
    month = entry;
    break;
   }
  }
  if (month == null){
   throw new illegalstateexception("未找到所在的月份");
  }
  int day = month.getvalue()-(count-currentday);
  return new int[]{month.getkey(),day};
 }
 /**
  * 判断是闰年还是平年
  * @param year 年份
  * @return true 为闰年
  */
 public static boolean isleapyear(int year) {
  return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
 }
 /**
  * 检测生成的天数是否正常
  * @param days
  */
 private static void checkdays(list<day> days) {
  if (days == null) {
   throw new illegalargumentexception("天数为空");
  }
  if (days.size() != 365 && days.size() != 366) {
   throw new illegalargumentexception("天数异常:" + days.size());
  }
 }
 public static void main(string[] args){
  //test
  list<day> days = datefactory.getdays(2016, 5);
  for (int i = 0; i < days.size(); i++) {
   system.out.println(days.get(i).tostring());
  }
 }
}

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

绘制天数格子

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

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

 /**灰色方格的默认颜色**/
 private final static int default_box_colour = 0xffeeeeee;
 /**提交次数颜色值**/
 private final static int[] colour_level =
   new int[]{0xff1e6823, 0xff44a340, 0xff8cc665, 0xffd6e685, default_box_colour};
 /**星期**/
 private string[] weeks = new string[]{"mon", "wed", "fri", "sun"};
 /**月份**/
 private string[] months =
   new string[]{"jan", "feb", "mar", "apr","may","jun","jul","aug","sep","oct","nov","dec"};
 /**默认的padding,绘制的时候不贴边画**/
 private int padding = 24;
 /**小方格的默认边长**/
 private int boxside = 8;
 /**小方格间的默认间隔**/
 private int boxinterval = 2;
 /**所有周的列数**/
 private int column = 0;
 private list<day> mdays;//一年中所有的天
 private paint boxpaint;//方格画笔
 private paint textpaint;//文字画笔
 private paint infopaint;//弹出框画笔
 private paint.fontmetrics metrics;//测量文字
 private float downx;//按下的点的x坐标
 private float downy;//按下的点的y坐标
 private day clickday;//按下所对应的天

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

public githubcontributionview(context context, attributeset attrs, int defstyleattr) {
  super(context, attrs, defstyleattr);
  initview();
 }
 public void initview() {
  mdays = datefactory.getdays(2016, 5);
  //方格画笔
  boxpaint = new paint();
  boxpaint.setstyle(paint.style.fill);
  boxpaint.setstrokewidth(2);
  boxpaint.setcolor(default_box_colour);
  boxpaint.setantialias(true);
  //文字画笔
  textpaint = new paint();
  textpaint.setstyle(paint.style.fill);
  textpaint.setcolor(color.gray);
  textpaint.settextsize(12);
  textpaint.setantialias(true);
  //弹出的方格信息画笔
  infopaint = new paint();
  infopaint.setstyle(paint.style.fill);
  infopaint.setcolor(0xcc888888);
  infopaint.settextsize(12);
  infopaint.setantialias(true);
  //将默认值转换px
  padding = ui.dp2px(getcontext(), padding);
  boxside = ui.dp2px(getcontext(), boxside);
  metrics = textpaint.getfontmetrics();
 }

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

 /**
  * 画出1-12月方格小块和上面的月份
  * @param canvas 画布
  */
 private void drawbox(canvas canvas) {
  //方格的左上右下坐标
  float startx, starty, endx, endy;
  //起始月份为1月
  int month = 1;
  for (int i = 0; i < mdays.size(); i++) {
   day day = mdays.get(i);
   if (i == 0){
    //画1月的文本标记,坐标应该是x=padding,y=padding-boxside/2(间隙),y坐标在表格上面一点
    canvas.drawtext(months[0],padding,padding-boxside/2,textpaint);
   }
   if (day.week == 1 && i != 0) {
    //如果当天是周1,那么说明增加了一列
    column++;
    //如果列首的月份有变化,那么说明需要画月份
    if (day.month>month){
     month = day.month;
     //月份文本的坐标计算,x坐标在变化,而y坐标都是一样的,boxside/2(间隙)
     canvas.drawtext(months[month-1],padding+column*(boxside+boxinterval),padding-boxside/2,textpaint);
    }
   }
   //计算方格坐标点,x坐标随列数的增多而增加,y坐标随行数的增多而变化
   startx = padding + column * (boxside + boxinterval);
   starty = padding + (day.week - 1) * (boxside + boxinterval);
   endx = startx + boxside;
   endy = starty + boxside;
   //将该方格的坐标保存下来,这样可以在点击方格的时候计算弹框的坐标
   day.startx = startx;
   day.starty = starty;
   day.endx = endx;
   day.endy = endy;
   //给画笔设置当前天的颜色
   boxpaint.setcolor(day.colour);
   canvas.drawrect(startx, starty, endx, endy, boxpaint);
  }
  boxpaint.setcolor(default_box_colour);//恢复默认颜色
 }

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

绘制星期文本

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

/**
  * 画左侧的星期
  * @param canvas 画布
  */
 private void drawweek(canvas canvas) {
  //文字是左对齐,所以找出最长的字
  float textlength = 0;
  for (string week : weeks) {
   float templength = textpaint.measuretext(week);
   if (textlength < templength) {
    textlength = templength;
   }
  }
  //依次画出星期文本,坐标点x=padding-文本长度-文本和方格的间隙,y坐标随行数变化
  canvas.drawtext(weeks[0], padding - textlength - 2, padding + boxside - metrics.descent, textpaint);
  canvas.drawtext(weeks[1], padding - textlength - 2, padding + 3 * (boxside + boxinterval) - metrics.descent, textpaint);
  canvas.drawtext(weeks[2], padding - textlength - 2, padding + 5 * (boxside + boxinterval) - metrics.descent, textpaint);
  canvas.drawtext(weeks[3], padding - textlength - 2, padding + 7 * (boxside + boxinterval) - metrics.descent, textpaint);
 }

绘制颜色深浅标志

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

/**
  * 画出右下角的颜色深浅标志,因为是右对齐的所以需要从右往左画
  * @param canvas 画布
  */
 private void drawtag(canvas canvas) {
  //首先计算出两个文本的长度
  float morelength = textpaint.measuretext("more");
  float lesslength = textpaint.measuretext("less");
  //画 more 文本,x坐标=padding+(列数+1)*(方格边长+方格间隙)-一个方格间隙-文本长度
  float morex = padding + (column + 1) * (boxside + boxinterval) - boxinterval - morelength;
  //y坐标=padding+(方格行数+1,和表格底部有些距离)*(方格边长+方格间隙)+字体的ascent高度
  float morey = padding + 8 * (boxside + boxinterval) + math.abs(metrics.ascent);
  canvas.drawtext("more", morex, morey, textpaint);
  //画深浅色块,坐标根据上面的more依次计算就可以了
  float interval = boxside - 2;//文字和色块间的距离
  float leftx = morex - interval - boxside;
  float topy = morey - boxside;
  float rightx = morex - interval;
  float bottomy = morey;//色块的y坐标是一样的
  for (int i = 0; i < colour_level.length; i++) {
   boxpaint.setcolor(colour_level[i]);
   canvas.drawrect(leftx - i * (boxside + boxinterval), topy, rightx - i * (boxside + boxinterval), bottomy, boxpaint);
  }
  //最后画 less 文本,原理同上
  canvas.drawtext("less", leftx - 4 * (boxside + boxinterval) - interval - lesslength, morey, textpaint);
 }

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

处理点击事件

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

 @override
 public boolean ontouchevent(motionevent event) {
  //获取action_down的坐标,用来判断点在哪天,并弹出·
  if (motionevent.action_down == event.getaction()) {
   downx = event.getx();
   downy = event.gety();
   findclickbox();
  }
  //这里因为我们只是记录坐标点,不对事件进行拦截所以默认返回
  return super.ontouchevent(event);
 }

判断是否在方格内:

 /**
  * 判断是否点击在方格内
  */
 private void findclickbox() {
  for (day day : mdays) {
   //检测点击的坐标如果在方格内,则弹出信息提示
   if (downx >= day.startx && downx <= day.endx && downy >= day.starty && downy <= day.endy) {
    clickday = day;//纪录点击的哪天
    break;
   }
  }
  //点击完要刷新,这样每次点击不同的方格,弹窗就可以在相应的位置显示
  refreshview();
 }
 /**
  * 点击弹出文字提示
  */
 private void refreshview() {
  invalidate();
 }

绘制弹出文本框

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

/**
  * 画方格上的文字弹框
  * @param canvas 画布
  */
 private void drawpopupinfo(canvas canvas) {
  if (clickday != null) {//点击的天不为null时候才画
   //先根据方格来画出一个小三角形,坐标就是方格的中间
   path infopath = new path();
   //先从方格中心
   infopath.moveto(clickday.startx + boxside / 2, clickday.starty + boxside / 2);
   //然后是方格的左上点
   infopath.lineto(clickday.startx, clickday.starty);
   //然后是方格的右上点
   infopath.lineto(clickday.endx, clickday.starty);
   //画出三角
   canvas.drawpath(infopath,infopaint);
   //画三角上的圆角矩形
   textpaint.setcolor(color.white);
   //得到当天的文本信息
   string popupinfo = clickday.tostring();
   system.out.println(popupinfo);
   //计算文本的高度和长度用以确定矩形的大小
   float infoheight = metrics.descent - metrics.ascent;
   float infolength = textpaint.measuretext(popupinfo);
   log.e("height",infoheight+"");
   log.e("length",infolength+"");
   //矩形左上点应该是x=当前天的x+边长/2-(文本长度/2+文本和框的间隙)
   float leftx = (clickday.startx + boxside / 2 ) - (infolength / 2 + boxside);
   //矩形左上点应该是y=当前天的y+边长/2-(文本高度+上下文本和框的间隙)
   float topy = clickday.starty-(infoheight+2*boxside);
   //矩形的右下点应该是x=leftx+文本长度+文字两边和矩形的间距
   float rightx = leftx+infolength+2*boxside;
   //矩形的右下点应该是y=当前天的y
   float bottomy = clickday.starty;
   system.out.println(""+leftx+"/"+topy+"/"+rightx+"/"+bottomy);
   rectf rectf = new rectf(leftx, topy, rightx, bottomy);
   canvas.drawroundrect(rectf,4,4,infopaint);
   //绘制文字,x=leftx+文字和矩形间距,y=topy+文字和矩形上面间距+文字顶到基线高度
   canvas.drawtext(popupinfo,leftx+boxside,topy+boxside+math.abs(metrics.ascent),textpaint);
   clickday = null;//重新置空,保证点击方格外信息消失
   textpaint.setcolor(color.gray);//恢复画笔颜色
  }
 }

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

/**
  * 设置某天的次数
  * @param year 年
  * @param month 月
  * @param day 日
  * @param contribution 次数
  */
 public void setdata(int year,int month,int day,int contribution){
  //先找到是第几天,为了方便不做参数检测了
  for (day d : mdays) {
   if (d.year == year && d.month == month && d.date == day){
    d.contribution = contribution;
    d.colour = getcolour(contribution);
    break;
   }
  }
  refreshview();
 }
 /**
  * 根据提交次数来获取颜色值
  * @param contribution 提交的次数
  * @return 颜色值
  */
 private int getcolour(int contribution){
  int colour = 0;
  if (contribution <= 0){
   colour = colour_level[4];
  }
  if (contribution == 1){
   colour = colour_level[3];
  }
  if (contribution == 2){
   colour = colour_level[2];
  }
  if (contribution == 3){
   colour = colour_level[1];
  }
  if (contribution >= 4){
   colour = colour_level[0];
  }
  return colour;
 }

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

/**
 * created by administrator on 2017/1/13.
 * 仿github的提交活跃表
 * 横屏使用
 */
public class githubcontributionview extends view {
 /**灰色方格的默认颜色**/
 private final static int default_box_colour = 0xffeeeeee;
 /**提交次数颜色值**/
 private final static int[] colour_level =
   new int[]{0xff1e6823, 0xff44a340, 0xff8cc665, 0xffd6e685, default_box_colour};
 /**星期**/
 private string[] weeks = new string[]{"mon", "wed", "fri", "sun"};
 /**月份**/
 private string[] months =
   new string[]{"jan", "feb", "mar", "apr","may","jun","jul","aug","sep","oct","nov","dec"};
 /**默认的padding,绘制的时候不贴边画**/
 private int padding = 24;
 /**小方格的默认边长**/
 private int boxside = 8;
 /**小方格间的默认间隔**/
 private int boxinterval = 2;
 /**所有周的列数**/
 private int column = 0;
 private list<day> mdays;//一年中所有的天
 private paint boxpaint;//方格画笔
 private paint textpaint;//文字画笔
 private paint infopaint;//弹出框画笔
 private paint.fontmetrics metrics;//测量文字
 private float downx;//按下的点的x坐标
 private float downy;//按下的点的y坐标
 private day clickday;//按下所对应的天
 public githubcontributionview(context context) {
  this(context, null);
 }
 public githubcontributionview(context context, attributeset attrs) {
  this(context, attrs, 0);
 }
 public githubcontributionview(context context, attributeset attrs, int defstyleattr) {
  super(context, attrs, defstyleattr);
  initview();
 }
 public void initview() {
  mdays = datefactory.getdays(2016, 5);
  //方格画笔
  boxpaint = new paint();
  boxpaint.setstyle(paint.style.fill);
  boxpaint.setstrokewidth(2);
  boxpaint.setcolor(default_box_colour);
  boxpaint.setantialias(true);
  //文字画笔
  textpaint = new paint();
  textpaint.setstyle(paint.style.fill);
  textpaint.setcolor(color.gray);
  textpaint.settextsize(12);
  textpaint.setantialias(true);
  //弹出的方格信息画笔
  infopaint = new paint();
  infopaint.setstyle(paint.style.fill);
  infopaint.setcolor(0xcc888888);
  infopaint.settextsize(12);
  infopaint.setantialias(true);
  //将默认值转换px
  padding = ui.dp2px(getcontext(), padding);
  boxside = ui.dp2px(getcontext(), boxside);
  metrics = textpaint.getfontmetrics();
 }
 @override
 protected void onsizechanged(int w, int h, int oldw, int oldh) {
  super.onsizechanged(w, h, oldw, oldh);
 }
 @override
 protected void ondraw(canvas canvas) {
  super.ondraw(canvas);
  column = 0;
  canvas.save();
  drawbox(canvas);
  drawweek(canvas);
  drawtag(canvas);
  drawpopupinfo(canvas);
  canvas.restore();
 }
 /**
  * 画出1-12月方格小块和上面的月份
  * @param canvas 画布
  */
 private void drawbox(canvas canvas) {
  //方格的左上右下坐标
  float startx, starty, endx, endy;
  //起始月份为1月
  int month = 1;
  for (int i = 0; i < mdays.size(); i++) {
   day day = mdays.get(i);
   if (i == 0){
    //画1月的文本标记,坐标应该是x=padding,y=padding-boxside/2(间隙),y坐标在表格上面一点
    canvas.drawtext(months[0],padding,padding-boxside/2,textpaint);
   }
   if (day.week == 1 && i != 0) {
    //如果当天是周1,那么说明增加了一列
    column++;
    //如果列首的月份有变化,那么说明需要画月份
    if (day.month>month){
     month = day.month;
     //月份文本的坐标计算,x坐标在变化,而y坐标都是一样的,boxside/2(间隙)
     canvas.drawtext(months[month-1],padding+column*(boxside+boxinterval),padding-boxside/2,textpaint);
    }
   }
   //计算方格坐标点,x坐标一致随列数的增多而增加,y坐标随行数的增多而变化
   startx = padding + column * (boxside + boxinterval);
   starty = padding + (day.week - 1) * (boxside + boxinterval);
   endx = startx + boxside;
   endy = starty + boxside;
   //将该方格的坐标保存下来,这样可以在点击方格的时候计算弹框的坐标
   day.startx = startx;
   day.starty = starty;
   day.endx = endx;
   day.endy = endy;
   //给画笔设置当前天的颜色
   boxpaint.setcolor(day.colour);
   canvas.drawrect(startx, starty, endx, endy, boxpaint);
  }
  boxpaint.setcolor(default_box_colour);//恢复默认颜色
 }
 /**
  * 画左侧的星期
  * @param canvas 画布
  */
 private void drawweek(canvas canvas) {
  //文字是左对齐,所以找出最长的字
  float textlength = 0;
  for (string week : weeks) {
   float templength = textpaint.measuretext(week);
   if (textlength < templength) {
    textlength = templength;
   }
  }
  //依次画出星期文本,坐标点x=padding-文本长度-文本和方格的间隙,y坐标随行数变化
  canvas.drawtext(weeks[0], padding - textlength - 2, padding + boxside - metrics.descent, textpaint);
  canvas.drawtext(weeks[1], padding - textlength - 2, padding + 3 * (boxside + boxinterval) - metrics.descent, textpaint);
  canvas.drawtext(weeks[2], padding - textlength - 2, padding + 5 * (boxside + boxinterval) - metrics.descent, textpaint);
  canvas.drawtext(weeks[3], padding - textlength - 2, padding + 7 * (boxside + boxinterval) - metrics.descent, textpaint);
 }
 /**
  * 画出右下角的颜色深浅标志,因为是右对齐的所以需要从右往左画
  * @param canvas 画布
  */
 private void drawtag(canvas canvas) {
  //首先计算出两个文本的长度
  float morelength = textpaint.measuretext("more");
  float lesslength = textpaint.measuretext("less");
  //画 more 文本,x坐标=padding+(列数+1)*(方格边长+方格间隙)-一个方格间隙-文本长度
  float morex = padding + (column + 1) * (boxside + boxinterval) - boxinterval - morelength;
  //y坐标=padding+(方格行数+1,和表格底部有些距离)*(方格边长+方格间隙)+字体的ascent高度
  float morey = padding + 8 * (boxside + boxinterval) + math.abs(metrics.ascent);
  canvas.drawtext("more", morex, morey, textpaint);
  //画深浅色块,坐标根据上面的more依次计算就可以了
  float interval = boxside - 2;//文字和色块间的距离
  float leftx = morex - interval - boxside;
  float topy = morey - boxside;
  float rightx = morex - interval;
  float bottomy = morey;//色块的y坐标是一样的
  for (int i = 0; i < colour_level.length; i++) {
   boxpaint.setcolor(colour_level[i]);
   canvas.drawrect(leftx - i * (boxside + boxinterval), topy, rightx - i * (boxside + boxinterval), bottomy, boxpaint);
  }
  //最后画 less 文本,原理同上
  canvas.drawtext("less", leftx - 4 * (boxside + boxinterval) - interval - lesslength, morey, textpaint);
 }
 @override
 public boolean ontouchevent(motionevent event) {
  //获取点击时候的坐标,用来判断点在哪天,并弹出·
  if (motionevent.action_down == event.getaction()) {
   downx = event.getx();
   downy = event.gety();
   findclickbox();
  }
  return super.ontouchevent(event);
 }
 /**
  * 判断是否点击在方格内
  */
 private void findclickbox() {
  for (day day : mdays) {
   //检测点击的坐标如果在方格内,则弹出信息提示
   if (downx >= day.startx && downx <= day.endx && downy >= day.starty && downy <= day.endy) {
    clickday = day;//纪录点击的哪天
    break;
   }
  }
  //点击完要刷新,这样每次点击不同的方格,弹窗就可以在相应的位置显示
  refreshview();
 }
 /**
  * 点击弹出文字提示
  */
 private void refreshview() {
  invalidate();
 }
 /**
  * 画方格上的文字弹框
  * @param canvas 画布
  */
 private void drawpopupinfo(canvas canvas) {
  if (clickday != null) {
   //先根据方格来画出一个小三角形,坐标就是方格的中间
   path infopath = new path();
   //先从方格中心
   infopath.moveto(clickday.startx + boxside / 2, clickday.starty + boxside / 2);
   //然后是方格的左上点
   infopath.lineto(clickday.startx, clickday.starty);
   //然后是方格的右上点
   infopath.lineto(clickday.endx, clickday.starty);
   //画出三角
   canvas.drawpath(infopath,infopaint);
   //画三角上的圆角矩形
   textpaint.setcolor(color.white);
   //得到当天的文本信息
   string popupinfo = clickday.tostring();
   system.out.println(popupinfo);
   //计算文本的高度和长度用以确定矩形的大小
   float infoheight = metrics.descent - metrics.ascent;
   float infolength = textpaint.measuretext(popupinfo);
   log.e("height",infoheight+"");
   log.e("length",infolength+"");
   //矩形左上点应该是x=当前天的x+边长/2-(文本长度/2+文本和框的间隙)
   float leftx = (clickday.startx + boxside / 2 ) - (infolength / 2 + boxside);
   //矩形左上点应该是y=当前天的y+边长/2-(文本高度+上下文本和框的间隙)
   float topy = clickday.starty-(infoheight+2*boxside);
   //矩形的右下点应该是x=leftx+文本长度+文字两边和矩形的间距
   float rightx = leftx+infolength+2*boxside;
   //矩形的右下点应该是y=当前天的y
   float bottomy = clickday.starty;
   system.out.println(""+leftx+"/"+topy+"/"+rightx+"/"+bottomy);
   rectf rectf = new rectf(leftx, topy, rightx, bottomy);
   canvas.drawroundrect(rectf,4,4,infopaint);
   //绘制文字,x=leftx+文字和矩形间距,y=topy+文字和矩形上面间距+文字顶到基线高度
   canvas.drawtext(popupinfo,leftx+boxside,topy+boxside+math.abs(metrics.ascent),textpaint);
   clickday = null;//重新置空,保证点击方格外信息消失
   textpaint.setcolor(color.gray);//恢复画笔颜色
  }
 }
 /**
  * 设置某天的次数
  * @param year 年
  * @param month 月
  * @param day 日
  * @param contribution 次数
  */
 public void setdata(int year,int month,int day,int contribution){
  //先找到是第几天,为了方便不做参数检测了
  for (day d : mdays) {
   if (d.year == year && d.month == month && d.date == day){
    d.contribution = contribution;
    d.colour = getcolour(contribution);
    break;
   }
  }
  refreshview();
 }
 /**
  * 根据提交次数来获取颜色值
  * @param contribution 提交的次数
  * @return 颜色值
  */
 private int getcolour(int contribution){
  int colour = 0;
  if (contribution <= 0){
   colour = colour_level[4];
  }
  if (contribution == 1){
   colour = colour_level[3];
  }
  if (contribution == 2){
   colour = colour_level[2];
  }
  if (contribution == 3){
   colour = colour_level[1];
  }
  if (contribution >= 4){
   colour = colour_level[0];
  }
  return colour;
 }
}

这样弄个布局测试下:

<?xml version="1.0" encoding="utf-8"?>
<linearlayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:gravity="center"
 android:orientation="vertical"
 >
 <com.franky.custom.view.githubcontributionview
  android:id="@+id/cc_chart"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  />
</linearlayout>

随机弄些数据:

public class mainactivity extends appcompatactivity {
 @override
 protected void oncreate(bundle savedinstancestate) {
  super.oncreate(savedinstancestate);
  setcontentview(r.layout.activity_main);
  githubcontributionview github = (githubcontributionview) findviewbyid(r.id.cc_chart);
  github.setdata(2016,12,9,2);
  github.setdata(2016,11,9,1);
  github.setdata(2016,10,5,10);
  github.setdata(2016,8,9,3);
  github.setdata(2016,4,20,2);
  github.setdata(2016,12,13,3);
  github.setdata(2016,12,14,3);
  github.setdata(2016,2,15,4);
 }
}

效果

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

Android自定义View实现仿GitHub的提交活跃表格

查看源码

以上所述是小编给大家介绍的android自定义view实现仿github的提交活跃表格,希望对大家有所帮助