手把手教你實現(xiàn)Android中智能設備數(shù)據(jù)表格繪制

最近做一個android智能手表的app,要給用戶呈現(xiàn)的就是用戶每天,每周,每月數(shù)據(jù)信息,既然要使得用戶能一眼就看出自己的數(shù)據(jù)趨勢,當然最好的就是折線統(tǒng)計圖或者柱狀圖了。
要實現(xiàn)這要的功能就需要借助于android強大的自定義控件了。
閑話休提,言歸正傳:
慣例先上效果,如下:

這里寫圖片描述
這里寫圖片描述

下面開始自定義控件的第一步:

1.在工程目錄res/values下新建attrs文件
2.在文件中聲明需要的屬性

    <!--坐標軸線條粗細-->
    <attr name="coordinatesLineWidth" format="dimension"/>
    <!--坐標軸字體大小-->
    <attr name="coordinatesTextSize" format="dimension" />
    <!--坐標軸字體顏色-->
    <attr name="coordinatesTextColor" format="color" />
    <!--折線顏色-->
    <attr name="lineColor" format="color" />
    <!--折線粗細-->
    <attr name="lineWidth" format="dimension" />
    <!--小圓點半徑-->
    <attr name="averageCircleradius" format="dimension" />
    <!--表格的數(shù)據(jù)類型-->
    <attr name="tableType" format="string" />
    <!--大圓點的顏色-->
    <attr name="maxcircleColor" format="color" />
    <!--小圓點的顏色-->
    <attr name="mincircleColor" format="color" />
    <!--背景色-->
    <attr name="bgColor" format="color" />

    <declare-styleable name="HealthyTableView">
        <attr name="coordinatesLineWidth"/>
        <attr name="coordinatesTextSize"/>
        <attr name="coordinatesTextColor"/>
        <attr name="lineColor"/>
        <attr name="lineWidth"/>
        <attr name="averageCircleradius"/>
        <attr name="tableType"/>
        <attr name="maxcircleColor"/>
        <attr name="mincircleColor"/>
        <attr name="bgColor"/>
    </declare-styleable>

3.在工程目錄指定包名下創(chuàng)建自定義控件的類:

public class HealthyTablesView extends View {
    public HealthyTablesView(Context context) {
        this(context,null);
    }

    public HealthyTablesView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public HealthyTablesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

該類聲明了三個參數(shù)的構造函數(shù),讓一個參數(shù)的構造函數(shù)調用二個參數(shù)的構造函數(shù),讓兩個參數(shù)的構造函數(shù)調用三個參數(shù)的構造函數(shù),接下來在第三個參數(shù)的構造函數(shù)中獲取我們自定義控件的屬性值:
老板,我貼代碼了哦!

TypedArray array = context.getTheme().obtainStyledAttributes(attrs,
    R.styleable.HealthyTableView, defStyleAttr, 0);
  int index = array.getIndexCount();
  for (int i = 0; i < index; i++)
  {
   int attr = array.getIndex(i);

   switch (attr)
   {
   case R.styleable.HealthyTableView_coordinatesLineWidth:
    // 這里將以px為單位,默認值為2px;
    mCoordinatesLineWidth = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 2, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_coordinatesTextColor:mCoordinatesTextColor = array.getColor(attr, Color.parseColor("#808080"));
    break;
   case R.styleable.HealthyTableView_coordinatesTextSize:
    mCoordinatesTextSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 11, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_lineColor:
    mLineColor = array.getColor(attr, Color.BLUE);
    break;
   case R.styleable.HealthyTableView_averageCircleradius:
    mCircleradius = array.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_bgColor:
    mBgColor = array.getColor(attr, Color.WHITE);
    break;
   case R.styleable.HealthyTableView_lineWidth:
    mLineWidth = array.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 11, getResources().getDisplayMetrics()));
    break;
   case R.styleable.HealthyTableView_maxcircleColor:
    mMaxcircleColor = array.getColor(attr, Color.GREEN);
    break;
   case R.styleable.HealthyTableView_mincircleColor:
    mMincircleColor = array.getColor(attr, Color.WHITE);
    break;
   case R.styleable.HealthyTableView_tableType:
    mDrawType = array.getString(attr);
    break;
   }
  }
  // 記得釋放資源
  array.recycle();
 }

好了,準備工作差不多了,然后呢?然后測量寬高后就開始畫圖了。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        /**
         * 自定義控件的寬高必須由調用者自己指定具體的數(shù)值
         */
        if (widthSpecMode == MeasureSpec.EXACTLY)
        {
            mWidth = widthSpecSize;
        }
        else
        {
            mWidth = 300;

        }

        if (heightSpecMode == MeasureSpec.EXACTLY)
        {
            //高是寬的3/5,這樣好嗎?
            mHeight = (mWidth / 5) * 3;
        }
        else
        {
            mHeight = 230;
        }
        Log.i(TAG, "width=" + mWidth + "...height=" + mHeight);
        setMeasuredDimension(mWidth, mHeight);
    }

開始畫圖了:
重寫onDraw(),在里面繪制坐標系:

 /**
  * 畫坐標系
  * 
  * @param canvas
  */
 private void drawCoordinates(Canvas canvas)
 {

  // X軸
  Log.i(TAG, "drawCoordinates");
  canvas.drawLine(getPaddingLeft(), mHeight - getPaddingBottom(),
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);
  // X軸上的箭頭
  canvas.drawLine(mWidth - getPaddingRight() - 20,
    mHeight - getPaddingBottom() - 10,
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);
  canvas.drawLine(mWidth - getPaddingRight() - 20,
    mHeight - getPaddingBottom() + 10,
    mWidth - getPaddingRight(), mHeight - getPaddingBottom(),
    xyPaint);

  // 繪制Y軸
  canvas.drawLine(getPaddingLeft(), getPaddingTop(), getPaddingLeft(),
    mHeight - getPaddingBottom(), xyPaint);

  // Y軸上的箭頭
  canvas.drawLine(getPaddingLeft() - 10, getPaddingTop() + 20 ,
    getPaddingLeft(), getPaddingTop(), xyPaint);
  canvas.drawLine(getPaddingLeft() + 10, getPaddingTop() + 20 ,
    getPaddingLeft(), getPaddingTop(), xyPaint);
 }
這里寫圖片描述

接下來繪制X軸上的時間值,這里以周為例,因為沒有真實的數(shù)據(jù),此次講義都已模擬數(shù)據(jù)為主;
定義一個數(shù)組,然后將X軸等分為7等分,畫上間斷線,寫上數(shù)值

//02號到8號,一周的時間
weeks = new String[]{"02","03","04","05","06","07","08"};

/**
  * 繪制X軸上的數(shù)值
  * 
  * @param canvas
  */
 private void drawCoordinatesXvalues(Canvas canvas)
 {

  // -40 為X軸留點邊界。 /6分成7等分

  for (int i = 0; i < weeks.length; i++)
  {
   textPaint.getTextBounds(weeks[i], 0, weeks[i].length(), textBound);
   // 畫間斷線
   canvas.drawLine(getPaddingLeft() + (i * XScale),
     mHeight - getPaddingBottom() - 10,
     getPaddingLeft() + (i * XScale),
     mHeight - getPaddingBottom(), xyPaint);
   // -textBound.width()/2 是為了讓字體和間斷線居中
   canvas.drawText(weeks[i],
     getPaddingLeft() + (i * XScale) - textBound.width() / 2,
     mHeight - getPaddingBottom() + 30, textPaint);
  }
 }

上圖:


這里寫圖片描述

上面的邏輯和計算并不復雜,就是將X軸的距離等分7等分,然后畫上間斷線和數(shù)值就OK了。

接下來計算Y軸上的要畫得數(shù)值,因為Y軸上的數(shù)值要根據(jù)用戶的真實數(shù)據(jù)來確定,所以幅度很大,不確定性因素也很多。這樣就需要我們動態(tài)的計算Y軸上的數(shù)值區(qū)間:

1.首先計算出用戶數(shù)據(jù)中的最大值和最小值來確定區(qū)間:
2.將計算出的最大值和最小值向上向下取一定幅度的值,比如最大值123,最小值63,最大值就可以取123+10,最小值取60-10,

 /**
  * 最高位 為什么要取出最高值,這里主要是通過計算動態(tài)的算出Y軸上的數(shù)值區(qū)間,
  * 比如心率是60-100,不計算寫死就是0-180,這樣折線的所有點就全部落在中間一點的地帶,上下都有較大的空白,影響美觀(心率一般在60-100之間)
  * 比如計步的幅度很大,如果不通過動態(tài)計算就不知道Y軸畫的數(shù)值給多少合適,比如Y軸數(shù)值寫死為0-20000,
  * 那么如果運動量偏少,比如都是1000步左右,折線就顯得幾乎和X=0平齊了
  * @param num
  * @return
  */
 private int getResultNum(float num)
 {
  int resultNum;
  int gw = 0; // 個位
  int sw = 0; // 十位
  int bw = 0; // 百位
  int qw = 0; // 千位
  int ww = 0; // 萬位

  if (num > 0)
  {
   gw = (int) (num % 10 / 1);
  }
  if (num > 10)
  {
   sw = (int) (num % 100 / 10);
  }

  if (num > 100)
  {
   bw = (int) (num % 1000 / 100);
  }

  if (num > 1000)
  {
   qw = (int) (num % 10000 / 1000);
  }

  if (num > 10000)
  {
   ww = (int) (num % 100000 / 10000);
  }
  /*********************************/
  if (ww >= 1)
  {
    resultNum=qw>5? ww * 10000 + 10000: ww * 10000 + 5000;
  }
  else if (qw >= 1)
  {
   resultNum=bw>5?qw*1000+1000:qw*1000+500;
  }
  else if (bw >= 1)
  {
   resultNum = bw * 100 + sw * 10 + 10;

  }
  else if (sw >= 1)
  {

   resultNum=gw>5?sw * 10 + 20:sw * 10 + 10;
  }
  else
  {
   resultNum = 0;
  }

  return resultNum;
 }

上面的代碼顯然是統(tǒng)一加上了某個數(shù)值,這個數(shù)值可以根據(jù)你的項目需求自己定義,但取下限的時候顯然就要減去某個數(shù)值:具體為什么要這么做注釋寫得比較詳細。

真正意義上的計算Y軸上數(shù)值刻度了:

/**
  * 傳入數(shù)組中的最大值和最小值,計算出在Y軸上數(shù)值的區(qū)間
  * 
  * @param max
  * @param min
  * @return
  */
 private int[] cacluterYValues(float max, float min)
 {
  int[] values;
  int min1;
  int max1;
  int resultNum = getResultNum(min); // 計算出的最小值
  max1 = getResultNum(max); // 計算出最大值
  if (resultNum <= 20) // 如果小于等于20 就不要減20,否則Y最小值是0了
  {
   min1 = resultNum - 10;
  }
  else
  {

   min1 = resultNum - 20;
  }

  if (resultNum <= 10 || resultNum == 0) // 如果小于10 就不用再減了,否則就是負數(shù)了
  {
   min1 = 0;
  }

  // 將計算出的數(shù)值均分為5等分
  double ceil = Math.ceil((max1 - min1) / 4);
  values = new int[]
  { min1, (int) (min1 + ceil), (int) (min1 + ceil * 2),
    (int) (min1 + ceil * 3), (int) (min1 + ceil * 4) };
  return values;

 }

這樣就計算出來了Y軸需要動態(tài)畫的數(shù)值。

接下來就開始畫吧:模擬數(shù)據(jù)的代碼這里就不貼了,后面會給出整個項目的源碼,感興趣的自己看看就懂了。

/**
  * 畫Y軸上的數(shù)值
  * 
  * @param canvas
  */
 private void drawYValues(Canvas canvas, float max, int[] value)
 {
 //這里除以max這個最大值是為了有多大的去見就分成多少等分,是的后面折線的點更精準,否者就會對不齊刻度,
  float YScale = ((float) mHeight - getPaddingBottom() - getPaddingTop()
    - 40) / max;
  for (int i = 0; i < value.length; i++)
  {
   String text = value[i] + "";
   int scale = value[i] - value[0];
   canvas.drawLine(getPaddingLeft(),
     mHeight - getPaddingBottom() - (YScale * scale),
     getPaddingLeft() + 10,
     mHeight - getPaddingBottom() - (YScale * scale), textPaint);
   textPaint.getTextBounds(text, 0, text.length(), textBound);
   // +textBound.height()/2 主要是為了讓字體和間斷線居中
   canvas.drawText(text,
     getPaddingLeft() - 40, mHeight - getPaddingBottom()
       - (YScale * scale) + textBound.height() / 2,
     textPaint);
  }

 }

效果圖:


這里寫圖片描述

顯然,畫線的邏輯并不復雜,只是計算Y軸上的值花了一定精力。

現(xiàn)在畫折線了:

1.首先畫出小圓點,然后將各個小圓點收尾相連接就是折線效果了:

 private void drawLine(Canvas canvas, float arraymax, float yMin)
 {

  //這里是整個Y軸可用高度除以最大值,就是每個值占有刻度上的幾等分;
  float YScale = ((mHeight - getPaddingBottom() - getPaddingTop() - 40))/ arraymax;
  for (int i = 0; i < values.length; i++)
  {
   //為什么是values[i] - arraymin(數(shù)據(jù)值-Y坐標最小值)? 
   //因為圓點是以數(shù)據(jù)值來畫得,數(shù)據(jù)值和Y軸坐標最小值的差就是整個數(shù)據(jù)的區(qū)間;
   int scale = (int) (values[i] - yMin);

   int j;
   /**
    * 畫折線
    */
   if (i < 6)
   {
    int textScale = (int) (values[i + 1] - yMin);
    j = i + 1;
    canvas.drawLine(getPaddingLeft() + (XScale * i),
      mHeight - getPaddingBottom() - (YScale * scale),
      getPaddingLeft() + (XScale * j),
      mHeight - getPaddingBottom() - (YScale * textScale),
      linePaint);
   }

   String text = String.valueOf(values[i]);
   textPaint.getTextBounds(text, 0, text.length(), textBound);
   canvas.drawText(text,
     getPaddingLeft() + (XScale * i) - textBound.width() / 2,
     mHeight - getPaddingBottom() - (YScale * scale) - 15,
     textPaint);

   /**
    * 兩個小圓點
    */
   canvas.drawCircle(getPaddingLeft() + (XScale * i),
     mHeight - getPaddingBottom() - (YScale * scale), 10,
     maxCirclePaint);
   canvas.drawCircle(getPaddingLeft() + (XScale * i),
     mHeight - getPaddingBottom() - (YScale * scale), 10 - 2,
     minCirclePaint);

  }

 }

注意上面的arraymax yMin兩個值的含義。arraymax一定是Y軸上區(qū)間的差值,比如軸上的數(shù)組為[60,70,80,90,100],那么arrayma就是100-60;yMin見注釋。
這里為什么要畫兩個圓?兩個同心圓能夠達到大圓是空心的效果,那畫筆設置為STROKE不就行了?


這里寫圖片描述

看到了吧,感覺從圓中間穿過去了,是不是覺得不爽啊,于是有人就說,我把圓的半徑算出來就行了,畫線的時候減去這個半徑,哥哥,如果前后兩點不在同一直線上你還得算夾角,你慢慢算吧。算好了告訴我!

這里寫圖片描述
 是不是美觀很多???騷年?

這里的工作基本就完了,至于睡眠要畫兩條線,獲取不同的數(shù)據(jù) 調用兩次畫圓點和線的方法就OK了。
至于代碼里如果覺得部分邏輯混亂冗余,那就將就一下吧。

最后附上源碼地址:源碼下載(https://git.oschina.net/xy001/anroidwatchtable.git)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容