自定義View-七日利率折線圖

 好久沒寫過博客了,主要是前一段時間一直在找工作,也沒有時間去靜下心來寫這個(都是藉口)。最近一直在看自定義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)熟能生巧,掌握基本步驟,在此基礎上不斷提升

源碼文件和資源圖片

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章