欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android自定義View實(shí)現(xiàn)仿GitHub的提交活躍表格

 更新時(shí)間:2017年01月18日 10:14:17   作者:franky814  
這篇文章主要介紹了Android自定義View實(shí)現(xiàn)仿GitHub的提交活躍表格,非常不錯(cuò),具有參考借鑒價(jià)值,需要的的朋友參考下吧

說(shuō)明

本文可能需要一些基礎(chǔ)知識(shí)點(diǎn),如Canvas,Paint,Path,Rect等類的基本使用,建議不熟悉的同學(xué)可以學(xué)習(xí)GcsSloop安卓自定義View教程目錄,會(huì)幫助很大。

github.jpg

上圖就是github的提交表格,直觀來(lái)看可以分為幾個(gè)部分進(jìn)行繪制:

(1)各個(gè)月份的小方格子,并且色彩根據(jù)提交次數(shù)變化,由淺到深
(2)右下邊的顏色標(biāo)志,我們右對(duì)齊就可以了
(3)左邊的星期,原圖是從周日畫到周六,我們從周一畫到周日
(4)上面的月份,我們只畫出1-12月
(5)點(diǎn)擊時(shí)候彈出當(dāng)天的提交情況,由一個(gè)小三角和圓角矩形組成

需要解決的計(jì)算問(wèn)題:

(1)生成任意一年的所有天,包含年月日周,提交次數(shù),色塊顏色,坐標(biāo)
(1)一年中所有的小方格子坐標(biāo)
(2)右下邊顏色標(biāo)志坐標(biāo)
(3)左邊星期坐標(biāo)
(4)上面月份坐標(biāo)
(5)點(diǎn)擊彈出的提示框和文字坐標(biāo)

生成某年所有天數(shù)

每天的信息我們需要封裝成一個(gè)類,代碼如下:

/**
 * Created by Administrator on 2017/1/13.
 * 封裝每天的屬性,方便在繪制的時(shí)候進(jìn)行計(jì)算
 */
public class Day implements Serializable{
 /**年**/
 public int year;
 /**月**/
 public int month;
 /**日**/
 public int date;
 /**周幾**/
 public int week;
 /**貢獻(xiàn)次數(shù),默認(rèn)0**/
 public int contribution = 0;
 /**默認(rèn)顏色,根據(jù)提交次數(shù)改變**/
 public int colour = 0xFFEEEEEE;
 /**方格坐標(biāo),左上點(diǎn),右下點(diǎn),確定矩形范圍**/
 public float startX;
 public float startY;
 public float endX;
 public float endY;
 @Override
 public String toString() {
  //這里直接在彈出框中顯示
  return ""+year+"年"+month+"月"+date+"日周"+week+","+contribution+"次";
 }
}

要想先繪制表格,需要計(jì)算出所有的天,這里計(jì)算一年中所有的天,我們通過(guò)從當(dāng)年1月1日算起,到12月31日,因?yàn)樾瞧谑沁B續(xù)的,所以我們需要我們提供某年的1月1日是周幾,比如2016年1月1日是周5,這里必要的參數(shù)是2016和周5,那么我們用一個(gè)類來(lái)實(shí)現(xiàn)該方法,代碼如下:

public class DateFactory {
 /**平年map,對(duì)應(yīng)月份和天數(shù)**/
 private static HashMap<Integer,Integer> monthMap = new LinkedHashMap<>(12);
 /**閏年map,對(duì)應(yīng)月份和天數(shù)**/
 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日所有的天數(shù)
  */
 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;
   //計(jì)算當(dāng)天為周幾,如果大于7就重置1
   day.week = lastWeekday<= 7 ? lastWeekday : 1;
   //計(jì)算當(dāng)天為幾月幾號(hào)
   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 當(dāng)前天數(shù)
  * @return 包含月和天的數(shù)組
  */
 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;
 }
 /**
  * 檢測(cè)生成的天數(shù)是否正常
  * @param days
  */
 private static void checkDays(List<Day> days) {
  if (days == null) {
   throw new IllegalArgumentException("天數(shù)為空");
  }
  if (days.size() != 365 && days.size() != 366) {
   throw new IllegalArgumentException("天數(shù)異常:" + 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());
  }
 }
}

具體的計(jì)算邏輯可以看看代碼,不是很難,這樣我們就能得到某年的所有天。

繪制天數(shù)格子

因?yàn)樵搗iew比較長(zhǎng),所以需要橫屏顯示,方便起見(jiàn),這里我們也不再進(jìn)行view的測(cè)量計(jì)算,也不再進(jìn)行自定義屬性,只關(guān)注其核心邏輯即可。

首先我們需要將需要的成員變量定義出來(lái):

 /**灰色方格的默認(rèn)顏色**/
 private final static int DEFAULT_BOX_COLOUR = 0xFFEEEEEE;
 /**提交次數(shù)顏色值**/
 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"};
 /**默認(rèn)的padding,繪制的時(shí)候不貼邊畫**/
 private int padding = 24;
 /**小方格的默認(rèn)邊長(zhǎng)**/
 private int boxSide = 8;
 /**小方格間的默認(rèn)間隔**/
 private int boxInterval = 2;
 /**所有周的列數(shù)**/
 private int column = 0;
 private List<Day> mDays;//一年中所有的天
 private Paint boxPaint;//方格畫筆
 private Paint textPaint;//文字畫筆
 private Paint infoPaint;//彈出框畫筆
 private Paint.FontMetrics metrics;//測(cè)量文字
 private float downX;//按下的點(diǎn)的X坐標(biāo)
 private float downY;//按下的點(diǎn)的Y坐標(biāo)
 private Day clickDay;//按下所對(duì)應(yīng)的天

這些提取的變量是慢慢增加的,在自定義的時(shí)候一下想不全的時(shí)候可以先寫,等用到某些變量的時(shí)候就提取出來(lái)。
然后我們初始化一下數(shù)據(jù):

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);
  //將默認(rèn)值轉(zhuǎn)換px
  padding = UI.dp2px(getContext(), padding);
  boxSide = UI.dp2px(getContext(), boxSide);
  metrics = textPaint.getFontMetrics();
 }

這里我們以2016年來(lái)舉例,mDays就是獲取2016年的所有天的集合(參數(shù)可以當(dāng)作自定義屬性提取出來(lái)),相關(guān)的Paint也已經(jīng)初始化好了,接下來(lái)就需要在onDraw方法里畫,先畫所有的方格子和月份標(biāo)志:

 /**
  * 畫出1-12月方格小塊和上面的月份
  * @param canvas 畫布
  */
 private void drawBox(Canvas canvas) {
  //方格的左上右下坐標(biāo)
  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月的文本標(biāo)記,坐標(biāo)應(yīng)該是x=padding,y=padding-boxSide/2(間隙),y坐標(biāo)在表格上面一點(diǎn)
    canvas.drawText(months[0],padding,padding-boxSide/2,textPaint);
   }
   if (day.week == 1 && i != 0) {
    //如果當(dāng)天是周1,那么說(shuō)明增加了一列
    column++;
    //如果列首的月份有變化,那么說(shuō)明需要畫月份
    if (day.month>month){
     month = day.month;
     //月份文本的坐標(biāo)計(jì)算,x坐標(biāo)在變化,而y坐標(biāo)都是一樣的,boxSide/2(間隙)
     canvas.drawText(months[month-1],padding+column*(boxSide+boxInterval),padding-boxSide/2,textPaint);
    }
   }
   //計(jì)算方格坐標(biāo)點(diǎn),x坐標(biāo)隨列數(shù)的增多而增加,y坐標(biāo)隨行數(shù)的增多而變化
   startX = padding + column * (boxSide + boxInterval);
   startY = padding + (day.week - 1) * (boxSide + boxInterval);
   endX = startX + boxSide;
   endY = startY + boxSide;
   //將該方格的坐標(biāo)保存下來(lái),這樣可以在點(diǎn)擊方格的時(shí)候計(jì)算彈框的坐標(biāo)
   day.startX = startX;
   day.startY = startY;
   day.endX = endX;
   day.endY = endY;
   //給畫筆設(shè)置當(dāng)前天的顏色
   boxPaint.setColor(day.colour);
   canvas.drawRect(startX, startY, endX, endY, boxPaint);
  }
  boxPaint.setColor(DEFAULT_BOX_COLOUR);//恢復(fù)默認(rèn)顏色
 }

這里主要是注意下行數(shù)列數(shù)的變化和月份坐標(biāo)的計(jì)算,格子畫好了。

繪制星期文本

我們?cè)佼嬜筮叺男瞧谖谋荆?/p>

/**
  * 畫左側(cè)的星期
  * @param canvas 畫布
  */
 private void drawWeek(Canvas canvas) {
  //文字是左對(duì)齊,所以找出最長(zhǎng)的字
  float textLength = 0;
  for (String week : weeks) {
   float tempLength = textPaint.measureText(week);
   if (textLength < tempLength) {
    textLength = tempLength;
   }
  }
  //依次畫出星期文本,坐標(biāo)點(diǎn)x=padding-文本長(zhǎng)度-文本和方格的間隙,y坐標(biāo)隨行數(shù)變化
  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);
 }

繪制顏色深淺標(biāo)志

然后根據(jù)表格的高度再畫出右下邊的顏色深淺標(biāo)志:

/**
  * 畫出右下角的顏色深淺標(biāo)志,因?yàn)槭怯覍?duì)齊的所以需要從右往左畫
  * @param canvas 畫布
  */
 private void drawTag(Canvas canvas) {
  //首先計(jì)算出兩個(gè)文本的長(zhǎng)度
  float moreLength = textPaint.measureText("More");
  float lessLength = textPaint.measureText("Less");
  //畫 More 文本,x坐標(biāo)=padding+(列數(shù)+1)*(方格邊長(zhǎng)+方格間隙)-一個(gè)方格間隙-文本長(zhǎng)度
  float moreX = padding + (column + 1) * (boxSide + boxInterval) - boxInterval - moreLength;
  //y坐標(biāo)=padding+(方格行數(shù)+1,和表格底部有些距離)*(方格邊長(zhǎng)+方格間隙)+字體的ascent高度
  float moreY = padding + 8 * (boxSide + boxInterval) + Math.abs(metrics.ascent);
  canvas.drawText("More", moreX, moreY, textPaint);
  //畫深淺色塊,坐標(biāo)根據(jù)上面的More依次計(jì)算就可以了
  float interval = boxSide - 2;//文字和色塊間的距離
  float leftX = moreX - interval - boxSide;
  float topY = moreY - boxSide;
  float rightX = moreX - interval;
  float bottomY = moreY;//色塊的Y坐標(biāo)是一樣的
  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);
 }

這樣整個(gè)表格主體繪制完成。

處理點(diǎn)擊事件

接下來(lái)要處理點(diǎn)擊事件,判斷點(diǎn)擊的坐標(biāo)如果在方格內(nèi),那么彈出對(duì)于的文本框,先處理點(diǎn)擊事件:

 @Override
 public boolean onTouchEvent(MotionEvent event) {
  //獲取ACTION_DOWN的坐標(biāo),用來(lái)判斷點(diǎn)在哪天,并彈出·
  if (MotionEvent.ACTION_DOWN == event.getAction()) {
   downX = event.getX();
   downY = event.getY();
   findClickBox();
  }
  //這里因?yàn)槲覀冎皇怯涗涀鴺?biāo)點(diǎn),不對(duì)事件進(jìn)行攔截所以默認(rèn)返回
  return super.onTouchEvent(event);
 }

判斷是否在方格內(nèi):

 /**
  * 判斷是否點(diǎn)擊在方格內(nèi)
  */
 private void findClickBox() {
  for (Day day : mDays) {
   //檢測(cè)點(diǎn)擊的坐標(biāo)如果在方格內(nèi),則彈出信息提示
   if (downX >= day.startX && downX <= day.endX && downY >= day.startY && downY <= day.endY) {
    clickDay = day;//紀(jì)錄點(diǎn)擊的哪天
    break;
   }
  }
  //點(diǎn)擊完要刷新,這樣每次點(diǎn)擊不同的方格,彈窗就可以在相應(yīng)的位置顯示
  refreshView();
 }
 /**
  * 點(diǎn)擊彈出文字提示
  */
 private void refreshView() {
  invalidate();
 }

繪制彈出文本框

然后看看彈出文本框的繪制:

/**
  * 畫方格上的文字彈框
  * @param canvas 畫布
  */
 private void drawPopupInfo(Canvas canvas) {
  if (clickDay != null) {//點(diǎn)擊的天不為null時(shí)候才畫
   //先根據(jù)方格來(lái)畫出一個(gè)小三角形,坐標(biāo)就是方格的中間
   Path infoPath = new Path();
   //先從方格中心
   infoPath.moveTo(clickDay.startX + boxSide / 2, clickDay.startY + boxSide / 2);
   //然后是方格的左上點(diǎn)
   infoPath.lineTo(clickDay.startX, clickDay.startY);
   //然后是方格的右上點(diǎn)
   infoPath.lineTo(clickDay.endX, clickDay.startY);
   //畫出三角
   canvas.drawPath(infoPath,infoPaint);
   //畫三角上的圓角矩形
   textPaint.setColor(Color.WHITE);
   //得到當(dāng)天的文本信息
   String popupInfo = clickDay.toString();
   System.out.println(popupInfo);
   //計(jì)算文本的高度和長(zhǎng)度用以確定矩形的大小
   float infoHeight = metrics.descent - metrics.ascent;
   float infoLength = textPaint.measureText(popupInfo);
   Log.e("height",infoHeight+"");
   Log.e("length",infoLength+"");
   //矩形左上點(diǎn)應(yīng)該是x=當(dāng)前天的x+邊長(zhǎng)/2-(文本長(zhǎng)度/2+文本和框的間隙)
   float leftX = (clickDay.startX + boxSide / 2 ) - (infoLength / 2 + boxSide);
   //矩形左上點(diǎn)應(yīng)該是y=當(dāng)前天的y+邊長(zhǎng)/2-(文本高度+上下文本和框的間隙)
   float topY = clickDay.startY-(infoHeight+2*boxSide);
   //矩形的右下點(diǎn)應(yīng)該是x=leftX+文本長(zhǎng)度+文字兩邊和矩形的間距
   float rightX = leftX+infoLength+2*boxSide;
   //矩形的右下點(diǎn)應(yīng)該是y=當(dāng)前天的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+文字和矩形上面間距+文字頂?shù)交€高度
   canvas.drawText(popupInfo,leftX+boxSide,topY+boxSide+Math.abs(metrics.ascent),textPaint);
   clickDay = null;//重新置空,保證點(diǎn)擊方格外信息消失
   textPaint.setColor(Color.GRAY);//恢復(fù)畫筆顏色
  }
 }

這樣主體邏輯完成,但需要開(kāi)放設(shè)置某天提交次數(shù)的方法:

/**
  * 設(shè)置某天的次數(shù)
  * @param year 年
  * @param month 月
  * @param day 日
  * @param contribution 次數(shù)
  */
 public void setData(int year,int month,int day,int contribution){
  //先找到是第幾天,為了方便不做參數(shù)檢測(cè)了
  for (Day d : mDays) {
   if (d.year == year && d.month == month && d.date == day){
    d.contribution = contribution;
    d.colour = getColour(contribution);
    break;
   }
  }
  refreshView();
 }
 /**
  * 根據(jù)提交次數(shù)來(lái)獲取顏色值
  * @param contribution 提交的次數(shù)
  * @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;
 }

好了,所有邏輯完成,主要涉及到一些計(jì)算,完整代碼:

/**
 * Created by Administrator on 2017/1/13.
 * 仿GitHub的提交活躍表
 * 橫屏使用
 */
public class GitHubContributionView extends View {
 /**灰色方格的默認(rèn)顏色**/
 private final static int DEFAULT_BOX_COLOUR = 0xFFEEEEEE;
 /**提交次數(shù)顏色值**/
 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"};
 /**默認(rèn)的padding,繪制的時(shí)候不貼邊畫**/
 private int padding = 24;
 /**小方格的默認(rèn)邊長(zhǎng)**/
 private int boxSide = 8;
 /**小方格間的默認(rèn)間隔**/
 private int boxInterval = 2;
 /**所有周的列數(shù)**/
 private int column = 0;
 private List<Day> mDays;//一年中所有的天
 private Paint boxPaint;//方格畫筆
 private Paint textPaint;//文字畫筆
 private Paint infoPaint;//彈出框畫筆
 private Paint.FontMetrics metrics;//測(cè)量文字
 private float downX;//按下的點(diǎn)的X坐標(biāo)
 private float downY;//按下的點(diǎn)的Y坐標(biāo)
 private Day clickDay;//按下所對(duì)應(yīng)的天
 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);
  //將默認(rèn)值轉(zhuǎn)換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) {
  //方格的左上右下坐標(biāo)
  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月的文本標(biāo)記,坐標(biāo)應(yīng)該是x=padding,y=padding-boxSide/2(間隙),y坐標(biāo)在表格上面一點(diǎn)
    canvas.drawText(months[0],padding,padding-boxSide/2,textPaint);
   }
   if (day.week == 1 && i != 0) {
    //如果當(dāng)天是周1,那么說(shuō)明增加了一列
    column++;
    //如果列首的月份有變化,那么說(shuō)明需要畫月份
    if (day.month>month){
     month = day.month;
     //月份文本的坐標(biāo)計(jì)算,x坐標(biāo)在變化,而y坐標(biāo)都是一樣的,boxSide/2(間隙)
     canvas.drawText(months[month-1],padding+column*(boxSide+boxInterval),padding-boxSide/2,textPaint);
    }
   }
   //計(jì)算方格坐標(biāo)點(diǎn),x坐標(biāo)一致隨列數(shù)的增多而增加,y坐標(biāo)隨行數(shù)的增多而變化
   startX = padding + column * (boxSide + boxInterval);
   startY = padding + (day.week - 1) * (boxSide + boxInterval);
   endX = startX + boxSide;
   endY = startY + boxSide;
   //將該方格的坐標(biāo)保存下來(lái),這樣可以在點(diǎn)擊方格的時(shí)候計(jì)算彈框的坐標(biāo)
   day.startX = startX;
   day.startY = startY;
   day.endX = endX;
   day.endY = endY;
   //給畫筆設(shè)置當(dāng)前天的顏色
   boxPaint.setColor(day.colour);
   canvas.drawRect(startX, startY, endX, endY, boxPaint);
  }
  boxPaint.setColor(DEFAULT_BOX_COLOUR);//恢復(fù)默認(rèn)顏色
 }
 /**
  * 畫左側(cè)的星期
  * @param canvas 畫布
  */
 private void drawWeek(Canvas canvas) {
  //文字是左對(duì)齊,所以找出最長(zhǎng)的字
  float textLength = 0;
  for (String week : weeks) {
   float tempLength = textPaint.measureText(week);
   if (textLength < tempLength) {
    textLength = tempLength;
   }
  }
  //依次畫出星期文本,坐標(biāo)點(diǎn)x=padding-文本長(zhǎng)度-文本和方格的間隙,y坐標(biāo)隨行數(shù)變化
  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);
 }
 /**
  * 畫出右下角的顏色深淺標(biāo)志,因?yàn)槭怯覍?duì)齊的所以需要從右往左畫
  * @param canvas 畫布
  */
 private void drawTag(Canvas canvas) {
  //首先計(jì)算出兩個(gè)文本的長(zhǎng)度
  float moreLength = textPaint.measureText("More");
  float lessLength = textPaint.measureText("Less");
  //畫 More 文本,x坐標(biāo)=padding+(列數(shù)+1)*(方格邊長(zhǎng)+方格間隙)-一個(gè)方格間隙-文本長(zhǎng)度
  float moreX = padding + (column + 1) * (boxSide + boxInterval) - boxInterval - moreLength;
  //y坐標(biāo)=padding+(方格行數(shù)+1,和表格底部有些距離)*(方格邊長(zhǎng)+方格間隙)+字體的ascent高度
  float moreY = padding + 8 * (boxSide + boxInterval) + Math.abs(metrics.ascent);
  canvas.drawText("More", moreX, moreY, textPaint);
  //畫深淺色塊,坐標(biāo)根據(jù)上面的More依次計(jì)算就可以了
  float interval = boxSide - 2;//文字和色塊間的距離
  float leftX = moreX - interval - boxSide;
  float topY = moreY - boxSide;
  float rightX = moreX - interval;
  float bottomY = moreY;//色塊的Y坐標(biāo)是一樣的
  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) {
  //獲取點(diǎn)擊時(shí)候的坐標(biāo),用來(lái)判斷點(diǎn)在哪天,并彈出·
  if (MotionEvent.ACTION_DOWN == event.getAction()) {
   downX = event.getX();
   downY = event.getY();
   findClickBox();
  }
  return super.onTouchEvent(event);
 }
 /**
  * 判斷是否點(diǎn)擊在方格內(nèi)
  */
 private void findClickBox() {
  for (Day day : mDays) {
   //檢測(cè)點(diǎn)擊的坐標(biāo)如果在方格內(nèi),則彈出信息提示
   if (downX >= day.startX && downX <= day.endX && downY >= day.startY && downY <= day.endY) {
    clickDay = day;//紀(jì)錄點(diǎn)擊的哪天
    break;
   }
  }
  //點(diǎn)擊完要刷新,這樣每次點(diǎn)擊不同的方格,彈窗就可以在相應(yīng)的位置顯示
  refreshView();
 }
 /**
  * 點(diǎn)擊彈出文字提示
  */
 private void refreshView() {
  invalidate();
 }
 /**
  * 畫方格上的文字彈框
  * @param canvas 畫布
  */
 private void drawPopupInfo(Canvas canvas) {
  if (clickDay != null) {
   //先根據(jù)方格來(lái)畫出一個(gè)小三角形,坐標(biāo)就是方格的中間
   Path infoPath = new Path();
   //先從方格中心
   infoPath.moveTo(clickDay.startX + boxSide / 2, clickDay.startY + boxSide / 2);
   //然后是方格的左上點(diǎn)
   infoPath.lineTo(clickDay.startX, clickDay.startY);
   //然后是方格的右上點(diǎn)
   infoPath.lineTo(clickDay.endX, clickDay.startY);
   //畫出三角
   canvas.drawPath(infoPath,infoPaint);
   //畫三角上的圓角矩形
   textPaint.setColor(Color.WHITE);
   //得到當(dāng)天的文本信息
   String popupInfo = clickDay.toString();
   System.out.println(popupInfo);
   //計(jì)算文本的高度和長(zhǎng)度用以確定矩形的大小
   float infoHeight = metrics.descent - metrics.ascent;
   float infoLength = textPaint.measureText(popupInfo);
   Log.e("height",infoHeight+"");
   Log.e("length",infoLength+"");
   //矩形左上點(diǎn)應(yīng)該是x=當(dāng)前天的x+邊長(zhǎng)/2-(文本長(zhǎng)度/2+文本和框的間隙)
   float leftX = (clickDay.startX + boxSide / 2 ) - (infoLength / 2 + boxSide);
   //矩形左上點(diǎn)應(yīng)該是y=當(dāng)前天的y+邊長(zhǎng)/2-(文本高度+上下文本和框的間隙)
   float topY = clickDay.startY-(infoHeight+2*boxSide);
   //矩形的右下點(diǎn)應(yīng)該是x=leftX+文本長(zhǎng)度+文字兩邊和矩形的間距
   float rightX = leftX+infoLength+2*boxSide;
   //矩形的右下點(diǎn)應(yīng)該是y=當(dāng)前天的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+文字和矩形上面間距+文字頂?shù)交€高度
   canvas.drawText(popupInfo,leftX+boxSide,topY+boxSide+Math.abs(metrics.ascent),textPaint);
   clickDay = null;//重新置空,保證點(diǎn)擊方格外信息消失
   textPaint.setColor(Color.GRAY);//恢復(fù)畫筆顏色
  }
 }
 /**
  * 設(shè)置某天的次數(shù)
  * @param year 年
  * @param month 月
  * @param day 日
  * @param contribution 次數(shù)
  */
 public void setData(int year,int month,int day,int contribution){
  //先找到是第幾天,為了方便不做參數(shù)檢測(cè)了
  for (Day d : mDays) {
   if (d.year == year && d.month == month && d.date == day){
    d.contribution = contribution;
    d.colour = getColour(contribution);
    break;
   }
  }
  refreshView();
 }
 /**
  * 根據(jù)提交次數(shù)來(lái)獲取顏色值
  * @param contribution 提交的次數(shù)
  * @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;
 }
}

這樣弄個(gè)布局測(cè)試下:

<?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>

隨機(jī)弄些數(shù)據(jù):

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沒(méi)有錄好,看看圖片效果:

效果.png

查看源碼

以上所述是小編給大家介紹的Android自定義View實(shí)現(xiàn)仿GitHub的提交活躍表格,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!

相關(guān)文章

  • 探究Android客戶端網(wǎng)絡(luò)預(yù)連接優(yōu)化機(jī)制

    探究Android客戶端網(wǎng)絡(luò)預(yù)連接優(yōu)化機(jī)制

    一般情況下,我們都是用一些封裝好的網(wǎng)絡(luò)框架去請(qǐng)求網(wǎng)絡(luò),對(duì)底層實(shí)現(xiàn)不甚關(guān)注,而大部分情況下也不需要特別關(guān)注處理。了解底層的一些實(shí)現(xiàn),有益于我們對(duì)網(wǎng)絡(luò)加載進(jìn)行優(yōu)化。本文就是關(guān)于根據(jù)http的連接復(fù)用機(jī)制來(lái)優(yōu)化網(wǎng)絡(luò)加載速度的原理與細(xì)節(jié)
    2021-06-06
  • 避免 Android中Context引起的內(nèi)存泄露

    避免 Android中Context引起的內(nèi)存泄露

    本文主要介紹Android中Context引起的內(nèi)存泄露的問(wèn)題,這里對(duì)Context的知識(shí)做了詳細(xì)講解,說(shuō)明如何避免內(nèi)存泄漏的問(wèn)題,有興趣的小伙伴可以參考下
    2016-08-08
  • Android入門之利用Spinner實(shí)現(xiàn)彈出選擇對(duì)話框

    Android入門之利用Spinner實(shí)現(xiàn)彈出選擇對(duì)話框

    這篇文章主要為大家詳細(xì)介紹了Android里如何巧用Spinner做彈出選擇對(duì)話框,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下
    2022-11-11
  • Android之來(lái)電秀實(shí)戰(zhàn)示例

    Android之來(lái)電秀實(shí)戰(zhàn)示例

    這篇文章主要為大家介紹了Android之來(lái)電秀實(shí)戰(zhàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • Android OpenGLES2.0等腰直角三角形和彩色的三角形(三)

    Android OpenGLES2.0等腰直角三角形和彩色的三角形(三)

    這篇文章主要為大家詳細(xì)介紹了Android OpenGLES2.0等腰直角三角形和彩色的三角形,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • Android輕松實(shí)現(xiàn)多語(yǔ)言的方法示例

    Android輕松實(shí)現(xiàn)多語(yǔ)言的方法示例

    本篇文章主要介紹了Android輕松實(shí)現(xiàn)多語(yǔ)言的方法示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-11-11
  • Android開(kāi)發(fā)中解析xml文件XmlUtils工具類與用法示例

    Android開(kāi)發(fā)中解析xml文件XmlUtils工具類與用法示例

    這篇文章主要介紹了Android開(kāi)發(fā)中解析xml文件XmlUtils工具類與用法,結(jié)合實(shí)例形式分析了Android開(kāi)發(fā)中解析xml文件工具類定義與相關(guān)使用技巧,需要的朋友可以參考下
    2018-01-01
  • Android NDK開(kāi)發(fā)入門

    Android NDK開(kāi)發(fā)入門

    本文主要對(duì)NDK產(chǎn)生的背景、使用NDK原因、NDK簡(jiǎn)介、NDK開(kāi)發(fā)環(huán)境的搭建、如何運(yùn)行NDK提供的事例demo等進(jìn)行了詳細(xì)的介紹。具有很好的參考價(jià)值,需要的朋友一起來(lái)看下吧
    2016-12-12
  • Android系統(tǒng)底層Reboot流程源碼解讀

    Android系統(tǒng)底層Reboot流程源碼解讀

    本文主要關(guān)注?Android?系統(tǒng)底層的?Reboot?流程,主要涉及?Native、Kenrel、Recovery、Bootloader,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • AlertDialog點(diǎn)擊按鈕不消失的實(shí)現(xiàn)方法

    AlertDialog點(diǎn)擊按鈕不消失的實(shí)現(xiàn)方法

    我有一個(gè)文本輸入對(duì)話框,當(dāng)我點(diǎn)擊對(duì)話框上的“是”按鈕,它會(huì)驗(yàn)證輸入,然后關(guān)閉對(duì)話框,但是,如果輸入錯(cuò)誤,我想停留在同一個(gè)對(duì)話框中。怎么實(shí)現(xiàn)此功能呢?下面通過(guò)本文給大家分享下
    2017-01-01

最新評(píng)論