自定义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)熟能生巧,掌握基本步骤,在此基础上不断提升

源码文件和资源图片

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值