編輯:關於Android編程
在上一篇雷達圖中留下了一個坑——折線圖。折線圖(broken-line graph)大概是初中數學就開始學習的,用來統計一段時間內某個數據的趨勢。反正就是用來反映數據的變化情況的,比如初中數學考試就會經常拿某個學生的成績來作為例子。
這次我們還是仿照max+的折線圖來做(我真的沒給max+做廣告,只是因為裡面的折線圖比較簡單而已,至少比扇貝單詞的要簡單一些→_→)。不過這次不像雷達圖那樣只是繪制View,我們可以設置一些可修改的屬性。首先分析一下原圖要怎麼畫。
首先這種圖呢官方沒有給我們提供類型的控件,所以我們需要自定義一個(廢話,如果有的話我還寫這篇文章干啥?)。
先觀察一下上圖,和之前的雷達圖類似,只有文字可以用TextView來繪制,其他都需要重繪,這樣我們不打算使用官方的TextView了。這個折線圖有X軸和Y軸(包含標尺和名字),背景中有相間的淺灰色的柱子,另外還有幾條水平方向的參考線,最後就是數據形成的折線以及折線和坐標軸圍成的一個填充了顏色的多邊形。坐標軸和參考線可以用Canvas.drawLine()實現,文字可以用Canvas.drawText()實現,柱子可以用Canvas.drawRect()實現,最後是折線,由於是不規則的形狀,我們用萬能的Path來實現。好了,這就是該View所要繪制的東西。
在寫代碼之前,還有一個問題要考慮,就是我們希望哪些屬性是可修改的,然後把這些屬性寫到res/attrs.xml中,動態地去修改它。比如現在我們希望坐標軸的顏色,數字顏色大小,坐標軸名稱的文字顏色和大小還有折線的顏色和數據填充區域的不透明度(顏色設定為和折線的顏色一樣,這樣看起來不會撞色顯得很奇怪)是可修改的。
以下是attrs.xml的定義:
各個屬性都已經有注釋了,這裡就不再多說。
接下來是我們的邏輯實現,創建一個BLGView(這是broken-line graph的縮寫,不是板藍根!!!)繼承android.view.View,具體實現如下:
public class BLGView extends View { //默認參數 private final int DEFAULT_LINE_COLOR = Color.parseColor("#5DA3EC"); private final int DEFAULT_FILL_ALPHA = 0x55; private final int DEFAULT_AXIS_COLOR = Color.parseColor("#9099A3"); private final int DEFAULT_AXIS_TEXT_COLOR = Color.parseColor("#5B6C7E"); private final int DEFAULT_AXIS_TEXT_SIZE = 16; private final int DEFAULT_BAR_COLOR = Color.parseColor("#F3F3F3"); private final String DEFAULT_AXIS_X = "X軸"; private final String DEFAULT_AXIS_Y = "Y軸"; //屬性變量 private int lineColor; private int fillAlpha; private int axisColor; private int axisTextColor; private int axisTextSize; private int stuffTextSize; //坐標軸標尺字體大小 private int realAxisTextSize; private int realStuffTextSize; private String axisX; private String axisY; //畫筆 Paint mPaint = new Paint(); TextPaint tPaint = new TextPaint(); //測試數據 private float[] mData = new float[]{ 92.7f, 90.7f, 73.4f, 85.8f, 86.0f, 68.3f, 75.5f, 79.3f, 85.8f, 91.9f, 88.3f }; public BLGView(Context context) { this(context, null); } public BLGView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BLGView); lineColor = a.getColor(R.styleable.BLGView_lineColor, DEFAULT_LINE_COLOR); fillAlpha = a.getInt(R.styleable.BLGView_fillAlpha, DEFAULT_FILL_ALPHA); axisColor = a.getColor(R.styleable.BLGView_axisColor, DEFAULT_AXIS_COLOR); axisTextColor = a.getColor(R.styleable.BLGView_axisTextColor, DEFAULT_AXIS_TEXT_COLOR); axisTextSize = a.getInt(R.styleable.BLGView_axisTextSize, DEFAULT_AXIS_TEXT_SIZE); stuffTextSize = axisTextSize - 2; // 設置坐標軸上的標尺數字字號比坐標軸名稱的字號小2sp axisX = a.getString(R.styleable.BLGView_axisX); axisY = a.getString(R.styleable.BLGView_axisY); a.recycle(); init(); } //初始化 private void init() { if (TextUtils.isEmpty(axisX)) { axisX = DEFAULT_AXIS_X; } if (TextUtils.isEmpty(axisY)) { axisY = DEFAULT_AXIS_Y; } realAxisTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, axisTextSize, getResources().getDisplayMetrics()); realStuffTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, stuffTextSize, getResources().getDisplayMetrics()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //折線圖的最大寬度 int maxWidth = (int) (getWidth() * 0.8); //折線圖的最大高度(固定了寬高比例為5:3) int maxHeight = (int) (maxWidth * 0.6); float space = maxWidth * 0.15f; canvas.save(); canvas.translate(space, getHeight() - space); //柱圖 mPaint.setColor(DEFAULT_BAR_COLOR); mPaint.setStyle(Paint.Style.FILL); float barWidth = maxWidth / 10f; for(int i = 0; i < 10; i+=2) { canvas.drawRect(barWidth * i, -maxHeight, barWidth * (i + 1), 0, mPaint); } //坐標軸 mPaint.setColor(axisColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(2f); canvas.drawLine(0f, 0f, maxWidth, 0f, mPaint); canvas.drawLine(0f, 0f, 0f, -maxHeight, mPaint); //坐標軸標尺 tPaint.setColor(axisColor); tPaint.setTextSize(realStuffTextSize); Rect r; String s; for(int i = 0; i < 100; i+=10) { r = new Rect(); s = String.valueOf(i); tPaint.getTextBounds(s, 0, s.length(), r); canvas.drawText(s, barWidth * (i / 10) - r.width() / 2f, r.height(), tPaint); } float step = maxHeight * 0.25f; float min = getMinData(mData); float max = getMaxData(mData); DecimalFormat df = new DecimalFormat("##0.0"); float v1 = min + (max - min) / 3; float v2 = v1 + (max - min) / 3; String minStr = String.valueOf(min); String maxStr = String.valueOf(max); String v1Str = String.valueOf(df.format(v1)); String v2Str = String.valueOf(df.format(v2)); String[] values = new String[]{minStr, v1Str, v2Str, maxStr}; mPaint.setColor(Color.parseColor("#D0D0D0")); mPaint.setStrokeWidth(1); r = new Rect(); tPaint.getTextBounds("8", 0, 1, r); float halfLetter = r.width(); for (int i = 0; i < 4; i++) { canvas.drawLine(0, -step * (i + 0.5f), maxWidth, -step * (i + 0.5f), mPaint); tPaint.getTextBounds(values[i], 0, values[i].length(), r); canvas.drawText(values[i], -r.width() - halfLetter, -(step * (i + 0.5f) - r.height() / 2.0f), tPaint); } tPaint.setColor(axisTextColor); tPaint.setTextSize(realAxisTextSize); r = new Rect(); tPaint.getTextBounds(axisX, 0, axisX.length(), r); canvas.drawText(axisX, maxWidth, r.height(), tPaint); tPaint.getTextBounds(axisY, 0, axisY.length(), r); canvas.drawText(axisY, -(r.width() + halfLetter), -(maxHeight + r.height()), tPaint); //繪制數據 float scope = max - min; for (int i = 0; i < mData.length; i++) { mData[i] = (mData[i] - min) / scope * 0.75f * maxHeight + 0.125f * maxHeight; } Path linePath = new Path(); Path fillPath = new Path(); linePath.moveTo(0, -mData[0]); for (int i = 1; i < mData.length; i++) { linePath.lineTo(maxWidth * i / 10, -mData[i]); } fillPath.addPath(linePath); fillPath.lineTo(maxWidth, 0); fillPath.lineTo(0, 0); fillPath.close(); mPaint.setColor(lineColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(4); canvas.drawPath(linePath, mPaint); mPaint.setColor(Color.argb(fillAlpha, Color.red(lineColor), Color.green(lineColor), Color.blue(lineColor))); mPaint.setStyle(Paint.Style.FILL); canvas.drawPath(fillPath, mPaint); canvas.restore(); } /** * 獲取數組最大值 * * @param f 數組 * @return 數組的最大值 * */ private float getMaxData(float[] f) { if(null == f || 0 == f.length) { return Float.NaN; } float max = f[0]; for(int i = 1; i < f.length; i++) { if(max < f[i]) { max = f[i]; } } return max; } /** * 獲取數組最小值 * * @param f 數組 * @return 數組的最小值 * */ private float getMinData(float[] f) { if(null == f || 0 == f.length) { return Float.NaN; } float min = f[0]; for(int i = 1; i < f.length; i++) { if(min > f[i]) { min = f[i]; } } return min; } /** * 設置數據 * * @param f 數據源 * */ public void setData(float[] f) { this.mData = f; invalidate(); } /** * 設置折線的顏色 * */ public void setLineColor(int lineColor) { this.lineColor = lineColor; invalidate(); } /** * 設置數據區域的顏色不透明度(顏色和折線一致) * */ public void setFillAlpha(int fillAlpha) { this.fillAlpha = fillAlpha; invalidate(); } /** * 設置坐標軸的顏色 * */ public void setAxisColor(int axisColor) { this.axisColor = axisColor; invalidate(); } /** * 設置坐標軸名稱的字體顏色 * */ public void setAxisTextColor(int axisTextColor) { this.axisTextColor = axisTextColor; invalidate(); } /** * 設置坐標軸名稱的字體大小 * */ public void setAxisTextSize(int axisTextSize) { this.axisTextSize = axisTextSize; this.stuffTextSize = axisTextSize - 2; realAxisTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, axisTextSize, getResources().getDisplayMetrics()); realStuffTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, stuffTextSize, getResources().getDisplayMetrics()); invalidate(); } /** * 設置X軸的名稱 * */ public void setAxisX(String axisX) { this.axisX = axisX; invalidate(); } /** * 設置Y軸名稱 * */ public void setAxisY(String axisY) { this.axisY = axisY; invalidate(); } }
雖然代碼中加了注釋(其實等於沒有),不過還是解釋一下沒部分的含義。
首先我們需要給BLGView設置一些默認的屬性值,當用戶沒有在布局文件或者代碼中設置屬性值時也能有一個默認的效果 然後是一些可變的屬性變量,還有兩個畫筆(一個用來畫圖,一個用來寫字),一組測試數據。 實現一個參數和兩個參數的構造器,在其中初始化屬性,注意字體大小因為原來是SP做單位的,但是手機在繪制的時候是按照手機實際屏幕分辨率來計算控件的大小,所以要把SP轉換。 在onDraw方法中,開始畫柱子、坐標軸、參考線、標尺、坐標軸名稱和數據。值得說明的是,因為我們的數據有時候會集中在某一個范圍中,比如測試數據集中在[68.3, 92.7]區間中,所以如果縱坐標從0開始算,那麼整個圖形就會縮在上方,這不僅不能很直觀地反映數據起伏(因為視覺效果造成誤讀),同時也不好看,讓折線看起來下方空上方密集。因此這裡的做法是縱坐標只畫出數據源的最小值到最大值這個范圍,並且令最小值在縱坐標的1/8處,最大值在縱坐標的7/8處,在繪制數據那一塊的第一個for循環其實就是重新計算數據的各個點的縱坐標的實際位置,公式大家可以自己推導一下(看來學好數學還是有點用處的→_→)。 最後就是暴露一些設置可變屬性的方法,設置好屬性記得調用invalidate()方法讓系統重繪。放上兩張測試圖:
默認的狀態
非默認狀態:
到此,整個折線圖的功能就算完成了,別問我為什麼不測試動態設置,因為我懶哈哈哈哈,好吧,上面是直接在布局文件中截圖的。
其實這個View的功能還可以做擴展,假設當數據量很大,把所有的數據一次性顯示出來同樣不易做分析,可以考慮把折線圖顯示一部分,然後通過左右滑動來觀看更多的數據等等,這個我以後(ruguo youkong)會繼續完善的。
這個折線圖和上一篇的雷達圖都只是顯示數據,沒有交互動作的,以後(ruguo youkong)我會繼續給大家帶來一些有交互性的自定義View。
說到android studio的調試,很多人可能會說,這有什麼可講的不就是一個斷點調試麼,剛開始我也是這麼認為的,直到我了解之後,才發現,調試原來可以玩的這麼牛。下面我
Andrioid 編譯系統是你用於build,test,runapp的工具箱。編譯系統的運行,可以通過Android Studio的菜單或者是獨立的命令行。通過編譯系統的
高斯模糊、加載監聽、圓角圖片這些相信大家都很熟悉,那如何實現這些效果,請大家參考本文進行學習。1、引用compile com.github.bumptech.glide:
ContextMenu介紹: 如果一個View注冊了上下文菜單,那麼當長按該View時便會彈出一個浮動菜單,來供選擇下一步操作。 實現這個功能需要調用setOnCrea