好久沒寫過博客了,主要是前一段時間一直在找工作,也沒有時間去靜下心來寫這個(都是藉口)。最近一直在看自定義View相關的東西,因爲這個不太會啊,一般想用的時候第一反應是去網上找有沒有類似的,但是如果想自力更生,還是靠自己啊,萬一項目哪天來個毀天滅地的需求,一點不會,還想要工資嗎?
看了一些相關的自定義控件,基本步驟都差不多,這次想分享的是一個七日劃利率折線圖。
效果圖1:(不帶陰影的)
效果圖2:(帶陰影的)
自定義控件相關的步驟就不說了,暫時只針對這個控件來分享。
繪畫步驟:
1.畫座標系:畫經過(0,0)點的x,y軸,這個是起點,畫好這個就好畫另外的座標軸了;
2.畫平行的橫縱座標軸:這步就需要計算刻度了,具體就是根據控件寬高和最大間隔數來一條一條的畫出來;
3.畫刻度標識:在第二步的基礎上,畫刻度標識,刻度都畫好了,標識就很簡單了,不過這個需要根據設置的數據來畫的;
4.畫最後一個點的顯示框:最後一個點處畫一個小圓,然後畫一個帶有改點值大小的圖片;
5.其實到這裏,簡單的折線圖已經畫好了,後來又想加點什麼,看到有些七日化利率折線圖有那張陰影,我也想嘗試做一下,一開始不知道怎麼做,第一個思路是是不是從折線經過的每一個點畫一條垂線就好了,但是折線經過的每個點怎麼獲得呀?不知道。然後無意間看到一篇介紹谷歌官方培訓課程自定義View的博客,裏面講可以畫哪些東西,提到了一個drawPath方法,然後就查了一下Path的作用,終於找到解決方法了,Path可以構成一個多邊形,drawPath就可以畫多邊形了,這樣把所有的點連成一個封閉多邊形畫出來就可以了。完美!
源碼:
package widget;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import com.ethanco.circleprogressdemo.R;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import widget.weekprofitview.SystemUtil;
/**
* Created by Beiing on 2016/1/15.
*
* 七日利率折線圖
*/
public class MyChartView extends View {
public static final String TAG = "MyChartView";
public interface ChartDataSupport{
String getTitle();
float getValue();
}
protected int mWidth;//控件寬度
protected int mHeight;//控件高度
//不包含經過原點的 x ,y 軸的數量
protected int xAxles = 5;
protected int yAxles = 6;
protected int yStep;// 每個y軸之間的間隔px
protected int xStep;// 每個x軸之間的間隔px
protected int leftPadding, topPadding, rightPadding, bottomPadding;//控件內部間隔
protected int x0, y0; // 座標軸圓點
protected float lastPointRadius = 8;//最後一個點處圓圈的半徑
protected Paint coordinatePaint; // 座標軸畫筆
protected Paint titlePaint; // 標題畫筆
protected Paint foldLinePaint; // 折線畫筆
protected Paint shadowPaint;// 陰影畫筆
protected boolean isShadow = true;// 是否啓用陰影
protected Bitmap textBitmap;//帶有文本的Bitmap
protected List<String> yValues;// y軸顯示的值 : 根據傳進來的數據計算, size = xAxles+1
protected List<? extends ChartDataSupport> mData; // 傳進來的所有數據
public MyChartView(Context context) {
this(context, null);
}
public MyChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initViewSize();
}
//初始化畫筆
private void initPaint() {
coordinatePaint = new Paint();
coordinatePaint.setStyle(Paint.Style.FILL);
coordinatePaint.setStrokeWidth(2);
coordinatePaint.setColor(Color.GRAY);
coordinatePaint.setAntiAlias(true);
titlePaint = new Paint();
titlePaint.setTextSize(SystemUtil.dip2px(getContext(), 12));
foldLinePaint = new Paint();
foldLinePaint.setStyle(Paint.Style.FILL);
foldLinePaint.setAntiAlias(true);
foldLinePaint.setStrokeWidth(3);
foldLinePaint.setColor(Color.RED);
shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
shadowPaint.setStyle(Paint.Style.FILL);
// LinearGradient gradient = new LinearGradient(0, 0, mWidth, mHeight, Color.parseColor("#22ff0000"), Color.parseColor("#22ff8800"), Shader.TileMode.CLAMP);
// shadowPaint.setShader(gradient);
shadowPaint.setColor(Color.parseColor("#22ff0000"));
}
// 初始化長、間隔大小之類
private void initViewSize() {
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
leftPadding = SystemUtil.dip2px(getContext(), 15);
topPadding = leftPadding;
rightPadding = leftPadding;
bottomPadding = SystemUtil.dip2px(getContext(), 20);
x0 = leftPadding;
y0 = mHeight - bottomPadding;
yStep = (mWidth - leftPadding * 2 - rightPadding) / yAxles;
xStep = (mHeight - topPadding * 2 - bottomPadding) / xAxles;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mData != null && mData.size() > 0){
//畫座標系
drawCoordinate(canvas);
//畫折線
drawFoldLine(canvas);
//畫折線到x軸之間的陰影
if(isShadow)
drawShadow(canvas);
}
}
/**
* 畫座標系
* @param canvas
*/
private void drawCoordinate(Canvas canvas) {
// 畫y軸
canvas.drawLine(x0, topPadding, x0, mHeight - bottomPadding, coordinatePaint);
// 畫x軸
canvas.drawLine(x0, y0, mWidth - rightPadding, mHeight - bottomPadding, coordinatePaint);
for(int i= 1; i <= yAxles; i++){
//y軸 : 從左往右畫
canvas.drawLine(i*yStep + leftPadding, topPadding, i*yStep + leftPadding, mHeight - bottomPadding, coordinatePaint);
}
for (int i = 1; i <= xAxles; i++) {
// x軸 : 從底部往上畫
canvas.drawLine((float) (leftPadding + 0.8 * yStep), mHeight - bottomPadding - i*xStep, mWidth - rightPadding, mHeight - bottomPadding - i*xStep, coordinatePaint);
}
Paint.FontMetrics fontMetrics = titlePaint.getFontMetrics(); // 獲取標題文字的高度(fontMetrics.descent - fontMetrics.ascent)
float textH = fontMetrics.descent - fontMetrics.ascent;
//畫x軸刻度顯示標題
for(int i = 0; i < mData.size(); i++){
String text = mData.get(i).getTitle();
canvas.drawText(text, i * yStep + 3, mHeight - bottomPadding + textH,titlePaint);
}
// 畫y軸刻度標題
for (int i = 0; i < xAxles; i++) {
String value = yValues.get(i);
canvas.drawText(value, leftPadding + 3, mHeight - bottomPadding - (i + 1 )*xStep + textH / 4,titlePaint);
}
}
/**
* 畫折線圖
* @param canvas
*/
private void drawFoldLine(Canvas canvas) {
int size = mData.size();
float yHeight = (xAxles - 1) * xStep; // y軸最大值和最小值之間對應區域的高度差
float minValue = getMinValue();
float delta = Float.parseFloat(yValues.get(yValues.size()-1)) - minValue; // y軸最大值和最小值的差
// Log.e(TAG, "delta:" + delta);
float startX, startY, endX, endY;
for (int i = 1; i <size; i++) {
startX = (i-1) *yStep + leftPadding;
endX = (i)*yStep + leftPadding;
startY = mHeight - bottomPadding - xStep - (mData.get(i - 1).getValue() - minValue ) * yHeight / delta;
endY = mHeight - bottomPadding - xStep- (mData.get(i).getValue() - minValue ) * yHeight / delta;
// Log.e(TAG, "startY:" + startY + ", endY:" + endY);
canvas.drawLine(startX, startY, endX, endY, foldLinePaint);
}
//最後一個點處畫個小圓圈
//1.得到最後一個點的座標值
float lastPointX = (size-1) *yStep + leftPadding;
float lastPointY = mHeight - bottomPadding - xStep- (mData.get(size - 1).getValue() - minValue ) * yHeight / delta;
canvas.drawCircle(lastPointX, lastPointY, lastPointRadius, foldLinePaint);
// 畫最後一個值的提示
textBitmap = getBitMapWithText(getContext(), R.mipmap.ico_popincome , String.valueOf(mData.get(size - 1).getValue()));
float left = lastPointX - textBitmap.getWidth() * 0.92f;
float top = lastPointY - textBitmap.getHeight() * 1.2f;
canvas.drawBitmap(textBitmap, left, top, foldLinePaint);
}
/**
* 畫折線到x軸之間的陰影
* @param canvas
*/
private void drawShadow(Canvas canvas) {
Path path = new Path();
path.moveTo(leftPadding,mHeight - bottomPadding);
int size = mData.size();
float yHeight = (xAxles - 1) * xStep; // y軸最大值和最小值之間對應區域的高度差
float minValue = getMinValue();
float delta = Float.parseFloat(yValues.get(yValues.size()-1)) - minValue; // y軸最大值和最小值的差
float startX = 0f, startY, endX, endY;
for (int i = 0; i <size; i++) {
startX = i *yStep + leftPadding;
startY = mHeight - bottomPadding - xStep - (mData.get(i).getValue() - minValue ) * yHeight / delta;
path.lineTo(startX, startY);
}
path.lineTo(startX ,mHeight - bottomPadding);
path.lineTo(leftPadding,mHeight - bottomPadding);
canvas.drawPath(path, shadowPaint);
}
// 設置數據
public void setData(List<? extends ChartDataSupport> mData){
this.mData = mData;
handleData();
invalidate(); //強制重畫一次
}
/**
* 根據傳進來的數據:得到x,y軸需要畫的文字
*/
private void handleData() {
if (mData != null && mData.size() > 0) {
yValues = new ArrayList<>();
float maxValue = getMaxValue();
float minValue = getMinValue();
float yGap = maxValue - minValue;
float yStep = yGap / (xAxles - 1);
//保留小數點後三位
DecimalFormat df =new java.text.DecimalFormat("#.000");
for (int i = 0; i < xAxles; i++) {
yValues.add(df.format(minValue + yStep * i));
}
}
}
// 需要得到傳進來的最大值和最小值
private float getMinValue(){
float minValue = mData.get(0).getValue();
int size = mData.size();
for (int i = 1; i < size; i++) {
float value = mData.get(i).getValue();
if(minValue > value){
minValue = value;
}
}
return minValue;
}
private float getMaxValue(){
float maxValue = mData.get(0).getValue();
int size = mData.size();
for (int i = 1; i < size; i++) {
float value = mData.get(i).getValue();
if(maxValue < value){
maxValue = value;
}
}
return maxValue;
}
/**
* 給定背景圖,得到一個帶有文字的Bitmap
* @param gContext
* @param gResId
* @param gText
* @return
*/
public Bitmap getBitMapWithText(Context gContext, int gResId, String gText) {
Resources resources = gContext.getResources();
float scale = resources.getDisplayMetrics().density;
Bitmap bitmap = BitmapFactory.decodeResource(resources, gResId);
android.graphics.Bitmap.Config bitmapConfig =
bitmap.getConfig();
// set default bitmap config if none
if (bitmapConfig == null) {
bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
}
// resource bitmaps are imutable,
// so we need to convert it to mutable one
bitmap = bitmap.copy(bitmapConfig, true);
Canvas canvas = new Canvas(bitmap);
// new antialised Paint
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// text color - #3D3D3D
paint.setColor(Color.WHITE);
// text size in pixels
paint.setTextSize((int) (15 * scale));
// text shadow
// paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);
// draw text to the Canvas center
Rect bounds = new Rect();
paint.getTextBounds(gText, 0, gText.length(), bounds);
int x = (bitmap.getWidth() - bounds.width()) / 2;
int y = (bitmap.getHeight() + bounds.height()) / 2 - 5;
canvas.drawText(gText, x, y, paint);
return bitmap;
}
/**
* 設置是否啓用陰影
* @param isShadow
*/
public void setShadow(boolean isShadow){
this.isShadow = isShadow;
}
}
分析:
1)開頭定義了一個接口,接口裏有兩個方法,
String getTitle();
float getValue();
主要是爲了實現泛型的效果,不管傳進來的是什麼類,只要實體類實現了該接口都可以繪製。
2)該控件沒有實現onMeasure()方法,因爲不需要精確的控制控件的尺寸等,簡單的實現了onSizeChanged 方法。
3)還有一個需要注意的是得到一個帶有文字的Bitmap,源碼中有這樣的方法。
4)代碼中有一個dp轉px的工具類沒貼出來
不足和總結:
1)該控件主要是針對七日化利率做的,所以x軸刻度不能過多
2)最後一點處的提示框暫時還沒有判斷位置,因爲如果最後一個點太靠控件頂部火太靠左,這個提示框就有可能被擋住一部分,所以需要在繪製的時候判斷最後一個點的位置,當然還需要提供不同的資源背景圖(自己也可以畫哦)
3)暫時還沒有定義自定義標籤,所以不支持在佈局文件中定義相關屬性,有需要的自己改源碼吧
4)一開始覺得自定義控件好難,自己只會用不會寫,這個算是一個開始吧,慢慢寫還是可以的,雖然簡單了點
5)熟能生巧,掌握基本步驟,在此基礎上不斷提升