自定义控件_折线图(动态加载,阴影效果,滑动效果)


在这里插入图片描述

使用
inflate.findViewById(R.id.lineChart);
        Random random = new Random();//生成随机数
        for (int i = 0; i < 80; i++) {
            data[i][0]  =random.nextInt(19)+1;
            data[i][1]  =random.nextInt(29)+1;
        }
        lineChart.setData(data);
        lineChart.setSegmentX(10);
        lineChart.setSegmentY(10);
        lineChart.start();
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:background="#333888"
    android:layout_height="match_parent">

    <com.example.ocean.charts.line.LineChart
        android:id="@+id/lineChart"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        app:graphTitle = "骑行记录"
        app:xAxisNmae = "第几天"
        app:yAxisNmae = "骑行公里数"
        app:axisTextSize = "4sp"
        android:layout_marginBottom="30dp"/>

</RelativeLayout>
折线图实现类 LineChart.java
package com.example.ocean.charts.line;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.example.ocean.R;

import org.jetbrains.annotations.NotNull;

import androidx.annotation.Nullable;

public class LineChart extends BaseLineChart {
    protected final Context mContext;
    public   int[][] data ;//传入的数据

    private Paint linePaint;//折线画笔
    private int defaultOriginalX ;//设置固定的X轴起点位置,X轴触摸可以滑动,用于防止Y轴滑动
    private Paint vaguePaint;//阴影画笔
    private float moveX;//当前滑动到的位置


    public LineChart(Context context) {
        this(context,null);
    }

    public LineChart(Context context, @Nullable AttributeSet attrs) {
        this(context,attrs,0);
    }

    public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr,0);
    }

    public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        initPaint();
    }

    /**        Explain : 初始化折线画笔
    * @author LiXiang */
    private void initPaint() {
        linePaint = new Paint(basePaint);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(getResources().getColor(R.color.blue_rgba_24_261_255));
        linePaint.setStrokeWidth(2f);

        vaguePaint = new Paint(basePaint);
        vaguePaint.setStyle(Paint.Style.FILL);
        vaguePaint.setStrokeWidth(dip2px(4));
        vaguePaint.setColor(getResources().getColor(R.color.blue_sharder));
    }


    public void setData(int[][] data) {
        this.data = data;
    }

    /**        Explain : 设置X轴刻度距离
    * @author LiXiang */
    @Override
    public void setSegmentX(int x) { segmentX = dip2px(x); }

    /**        Explain : 设置Y轴刻度距离
     * @author LiXiang */
    @Override
    public void setSegmentY(int y) { segmentY = dip2px(y); }

    /**        Explain : Y轴长度,默认为视图的高度
    * @author LiXiang */
    @Override
    protected int getSpaceY() {
        return getDefaultSpaceY();
    }

    /**        Explain : X轴的长度,由刻度距离和数据量决定
    * @author LiXiang */
    @Override
    protected int getSpaceX() {
        return defaultOriginalX + segmentX*data.length;
    }



    /**        Explain : 绘制折线
    * @author LiXiang */
    @Override
    protected void drawLine(Canvas canvas, Paint basePaint) {
    
    //每次绘制阴影之前都需重新设置渐变熟悉
        vaguePaint.setShader(new LinearGradient(//填充方向是以Y轴为基准 若是自上而下 就是 0,0,0,getMeasuredHeight()
                defaultOriginalX, endY, defaultOriginalX, originalY,//从上到下逐渐变淡
                new int[]{
                        getResources().getColor(R.color.blue_sharder),
                        getResources().getColor(android.R.color.transparent)},
                null, Shader.TileMode.CLAMP)
        );
        
        //先绘制折线阴影部分,以防止折线被覆盖
        canvas.drawPath(drawSegmentVague(drawVague()),vaguePaint);
        //绘制折线
        canvas.drawPath(drawSegment(drawLineReal()),linePaint);
    }

    /**        Explain : 获取折线整体的path
    * @author LiXiang */
    private Path drawLineReal() {
        Path path = new Path();
        path.moveTo(originalX+segmentX,originalY-data[0][1]*segmentY);//移动到第一个点的位置
        for (int i = 0; i < data.length; i++) {
            path.lineTo(originalX+segmentX*(i+1),originalY-data[i][1]*segmentY);
        }
        return path;
    }

    /**        Explain : 获取折线阴影整体的path
    * @author LiXiang */
    private Path drawVague() {
        Path pathVague = new Path();
        pathVague.moveTo(originalX+segmentX,originalY);//移动到第一个点的X轴
        pathVague.lineTo(originalX+segmentX,originalY-data[0][1]*segmentY);//与第一个点的X轴连成线
        for (int i = 0; i < data.length; i++) {//将后面的每一个点遍历入path
            pathVague.lineTo(originalX+segmentX*(i+1),originalY-data[i][1]*segmentY);
        }
        return pathVague;
    }

    /**        Explain : 依照动画进程获取阴影片段
    * @author LiXiang */
    @NotNull
    private Path drawSegmentVague(Path path) {
            Path pathVagueSegment = drawSegment(path);
            //添加当前X轴绘制到的点,形参封闭区域
            pathVagueSegment.lineTo(originalX+segmentX*mAnimatedValue * data.length,originalY);
            pathVagueSegment.close();//形参封闭区域
            return pathVagueSegment;
    }
    /**        Explain : 依照动画进程获取折线片段
    * @author LiXiang */
    @NotNull
    private Path drawSegment(Path path) {
        Path pathVagueSegment = new Path();
        PathMeasure pathMeasureCover = new PathMeasure(path, false);
        float curveLength = pathMeasureCover.getLength();
        //取当前path的某一段长度
        pathMeasureCover.getSegment(0, curveLength * ( mAnimatedValue), pathVagueSegment, true);
        return pathVagueSegment;
    }

    /**        Explain : 绘制Y轴刻度值
    * @author LiXiang */
    @Override
    protected void drawAxisScaleValuveY(Canvas canvas, Paint basePaint) {
        int scaleSize = (originalY - endY) / segmentY;
        for (int i = 1; i < scaleSize +1; i++) {
            canvas.drawText(i+"",//显示的数字
                    defaultOriginalX-dip2px(10),//往左偏移10dp,防止遮住Y轴
                    (originalY-i*segmentY)+getDimenssion(i).height()/2,//使数字在刻度的中间
                    basePaint);
        }
    }

    /**        Explain : 计算数字的宽高
     * @author LiXiang */
    @NotNull
    private Rect getDimenssion(int intNumber) {
        String number = intNumber+"";
        //获取数字宽高尺寸
        Rect rect= new Rect();
        basePaint.getTextBounds(number,0,number.length(),rect);
        return rect;
    }

    /**        Explain : 绘制X轴刻度值
    * @author LiXiang */
    @Override
    protected void drawAxisScaleValuveX(Canvas canvas, Paint basePaint) {
        //获取刻度的数量,当长度超过屏幕的时候选择对应数据量的长度,小于的时候选屏幕的宽度
        int scaleSize = (getSpaceX()>getDefaultSpaceX()? data.length : getDefaultSpaceX()/ segmentX) ;
        for (int i = 1; i < scaleSize +1; i++) {
            canvas.drawText(i+"",//显示的数字
                    originalX+i*segmentX-getDimenssion(i).width()/2,//往下偏移10dp,防止遮住X轴
                    originalY+dip2px(15),//使数字在刻度的中间
                    basePaint);
        }
    }

    /**        Explain : 绘制Y轴刻度
    * @author LiXiang */
    @Override
    protected void drawAxisScaleY(Canvas canvas, Paint basePaint) {
        int scaleSize = (originalY - endY)/ segmentY;
        for (int i = 0; i < scaleSize; i++) {
            canvas.drawLine(defaultOriginalX,originalY-i*segmentX,defaultOriginalX+dip2px(2),originalY-i*segmentX,basePaint);
        }
    }

    /**        Explain : 绘制X轴
     * @author LiXiang */
    @Override
    protected void drawAxisScaleX(Canvas canvas, Paint basePaint) {
        //获取刻度的数量,当长度超过屏幕的时候选择对应数据量的长度,小于的时候选屏幕的宽度
        int scaleSize = (getSpaceX()>getDefaultSpaceX()? data.length : getDefaultSpaceX()/ segmentX) ;
        for (int i = 0; i < scaleSize; i++) {
            canvas.drawLine(originalX+i*segmentX,originalY,originalX+i*segmentX,originalY-dip2px(2),basePaint);
        }
    }

    /**        Explain : 绘制Y轴
     * @author LiXiang */
    @Override
    protected void drawAxisY(Canvas canvas, Paint basePaint) {
        canvas.drawLine(defaultOriginalX,originalY,defaultOriginalX,endY,basePaint);
    }

    /**        Explain : 绘制X轴
     * @author LiXiang */
    @Override
    protected void drawAxisX(Canvas canvas, Paint basePaint) {
        canvas.drawLine(originalX,originalY,endX+originalX,originalY,basePaint);
    }


    /**        Explain : 绘制X轴箭头
     * @author LiXiang */
    protected void drawAxisArrowX(Canvas canvas, Paint basePaint) {
        Path path = new Path();
        path.moveTo(endX+dip2px(2)+originalX,originalY);
        path.lineTo(endX+originalX,originalY + dip2px(2));
        path.lineTo(endX+originalX,originalY - dip2px(2));
        path.close();
        canvas.drawPath(path,basePaint);
        canvas.drawText(xAxisNmae,endX+dip2px(16)+originalX,originalY + dip2px(2),basePaint);

    }

    /**        Explain : 绘制Y轴箭头
     * @author LiXiang */
    public void drawAxisArrowY(Canvas canvas, Paint basePaint) {
        Path path = new Path();
        path.moveTo(defaultOriginalX,endY - dip2px(2));
        path.lineTo(defaultOriginalX+ dip2px(2),endY );
        path.lineTo(defaultOriginalX- dip2px(2),endY );
        path.close();
        canvas.drawPath(path,basePaint);
        canvas.drawText(yAxisNmae,defaultOriginalX,endY - dip2px(8),basePaint);
    }




    private float minOriginalX;
    private float maxOriginalX;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        defaultOriginalX = getPaddingLeft() + defaultPadding;
        minOriginalX = getWidth()  - segmentX * (data.length+4);//起点可以移动到的最小值
        maxOriginalX = defaultOriginalX;//起点的最大值就是最初的位置
    }

    

    /**        Explain : 动画每进行一次刷新都重置X轴起点位置
    * @author LiXiang */
    @Override
    protected void invalidateOtherData() {
        super.invalidateOtherData();
            originalX = (int) ( defaultOriginalX 
                    + (minOriginalX-defaultOriginalX)*mAnimatedValue);//当前X起点可以移动的总距离(X轴起点的正值+X轴起点的最小负值)*当前动画的进度
    }

    
    /**        Explain : 监听当前的滑动事件
    * @author LiXiang */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                moveX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                if (segmentX * data.length > getWidth() - defaultOriginalX) {//当期的宽度不足以呈现全部数据的时候才可以滑动
                    float dis = event.getX() - moveX;//获取当前滑动的距离
                    moveX = event.getX();//记录当前滑动的位置
                    
                    //设置滑动到两端的极限值
                    if (originalX + dis < minOriginalX) {
                        originalX = (int) minOriginalX;
                    } else if (originalX + dis > maxOriginalX) {
                        originalX = (int) maxOriginalX;
                    } else {
                        originalX = (int) (originalX + dis);
                    }
                    invalidate();//刷新数据
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                this.getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return true;
    }

}

基础类 BaseLineChart.java

package com.example.ocean.charts.line;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.AttributeSet;

import com.example.ocean.R;
import com.example.ocean.charts.BaseChart;

import androidx.annotation.Nullable;

public abstract class BaseLineChart extends BaseChart {

    protected int axisTextColor;//刻度值颜色
    protected int axisTextSize;//刻度值字体大小
    protected String xAxisNmae;//X轴名字
    protected String yAxisNmae;//Y轴名字
    protected String graphTitle;//图表名字
    protected int segmentX;//轴间距
    protected int segmentY;//轴间距

    public BaseLineChart(Context context) {
            this(context,null);
        }

        public BaseLineChart(Context context, @Nullable AttributeSet attrs) {
            this(context,attrs,0);
        }

        public BaseLineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr,0);
        }

        public BaseLineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            /**        Explain : 获取样式
             * @author LiXiang */
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.GraphStyle);
            graphTitle = typedArray.getString(R.styleable.GraphStyle_graphTitle);
            xAxisNmae = typedArray.getString(R.styleable.GraphStyle_xAxisNmae);
            yAxisNmae = typedArray.getString(R.styleable.GraphStyle_yAxisNmae);
            axisTextColor = typedArray.getColor(R.styleable.GraphStyle_axisTextColor,Color.GRAY);
            axisTextSize= (int) typedArray.getDimension(R.styleable.GraphStyle_axisTextSize,4);
            axisTextSize = dip2px(axisTextSize);
            if (typedArray != null) {
                typedArray.recycle();
            }
//            Typeface font0 = Typeface.create(Typeface.SANS_SERIF, Typeface.DEFAULT_BOLD.getStyle());
//            basePaint.setTypeface(font0);
            //初始化基础画笔
            basePaint.setTextSize(dip2px(8));//设置标题大小
            basePaint.setTextAlign(Paint.Align.CENTER);//标题对其格式
            basePaint.setStrokeWidth(dip2px(0.1f));//画笔粗细
            basePaint.setStrokeCap(Paint.Cap.ROUND);//设置圆角
            basePaint.setColor(Color.GRAY);//设置画笔颜色


        }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        drawTitle(canvas,basePaint);//绘制标题
        drawAxisArrowX(canvas,basePaint);//绘制X轴箭头
        drawAxisArrowY(canvas,basePaint);//绘制Y轴箭头

        //以下类交给子类处理
        drawAxisX(canvas,basePaint);//绘制X轴
        drawAxisY(canvas,basePaint);//绘制Y轴
        drawAxisScaleX(canvas,basePaint);//绘制X轴刻度
        drawAxisScaleY(canvas,basePaint);//绘制Y轴刻度
        drawAxisScaleValuveX(canvas,basePaint);//绘制X轴刻度值
        drawAxisScaleXValuveY(canvas,basePaint);//绘制Y轴刻度值
        drawLine(canvas,basePaint);//绘制折线
    }


    /**        Explain : 绘制X轴箭头
    * @author LiXiang */
    protected void drawAxisArrowX(Canvas canvas, Paint basePaint) {
        Path path = new Path();
        path.moveTo(endX+dip2px(2),originalY);
        path.lineTo(endX,originalY + dip2px(2));
        path.lineTo(endX,originalY - dip2px(2));
        path.close();
        canvas.drawPath(path,basePaint);
        canvas.drawText(xAxisNmae,endX+dip2px(16),originalY + dip2px(2),basePaint);
    }

    /**        Explain : 绘制Y轴箭头
     * @author LiXiang */
    protected void drawAxisArrowY(Canvas canvas, Paint basePaint) {

        Path path = new Path();
        path.moveTo(originalX,endY - dip2px(2));
        path.lineTo(originalX+ dip2px(2),endY );
        path.lineTo(originalX- dip2px(2),endY );
        path.close();
        canvas.drawPath(path,basePaint);
        canvas.drawText(yAxisNmae,originalX,endY - dip2px(8),basePaint);
    }

    /**        Explain : 设置标题
     * @author LiXiang
     * @param canvas
     * @param basePaint  */
    private void drawTitle(Canvas canvas, Paint basePaint) {
        if (!TextUtils.isEmpty(graphTitle)) {
            Paint titlePaint = new Paint(basePaint);
            titlePaint.setFakeBoldText(true);//设置字体加粗
            titlePaint.setTextSize(axisTextSize);
            titlePaint.setColor(axisTextColor);
            canvas.drawText(graphTitle,getWidth() / 2,originalY+dip2px(35),titlePaint);
        }
    }

    protected abstract void setSegmentX(int x);//X轴长度
    protected abstract void setSegmentY(int y);//Y轴长度
    protected abstract void drawLine(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisScaleXValuveY(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisScaleValuveX(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisScaleY(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisScaleX(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisY(Canvas canvas, Paint basePaint);

    protected abstract void drawAxisX(Canvas canvas, Paint basePaint);

}


基础类 BaseChart.java
package com.example.ocean.charts;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Paint;
import android.os.Build;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;

import com.example.ocean.R;

import androidx.annotation.Nullable;

public abstract class BaseChart extends View {

    protected final Context mContext;

    protected Paint basePaint;//基础画笔
    protected int originalX ;//X轴起点
    protected int originalY ;//Y轴起点
    protected int endX ;//X轴终点
    protected int endY ;//Y轴终点
    protected int defaultPadding = dip2px(30);//默认内边距
    protected ValueAnimator valueAnimator;//动画
    protected boolean starting = false;//是否正在执行动画
    protected float mAnimatedValueMax = 1;//动画最大值
    protected float mAnimatedValue = 0;//动画当前值
    protected boolean onTouch = false;//是否正在触摸

    public BaseChart(Context context) {
        this(context,null);
    }

    public BaseChart(Context context, @Nullable AttributeSet attrs) {
        this(context,attrs,0);
    }

    public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr,0);
    }

    public BaseChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        initBasePaint();
    }

    /**        Explain : 初始化基础画笔
    * @author LiXiang */
    private void initBasePaint() {
        if (basePaint == null) {
        basePaint = new Paint();
        basePaint.setAntiAlias(true);//抗锯齿
        basePaint.setDither(true);//防抖动
        }
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        originalX = getPaddingLeft() + defaultPadding;
        originalY = getMeasuredHeight() - getPaddingBottom() - 2*defaultPadding;
        endX = getSpaceX()>getDefaultSpaceX()?getSpaceX():getDefaultSpaceX();
        endY = getSpaceY()>getDefaultSpaceY()?getSpaceY():getDefaultSpaceY();

    }

    protected int getDefaultSpaceY() { return getPaddingTop() + defaultPadding; }

    protected int getDefaultSpaceX() { return getMeasuredWidth() - getPaddingRight() - defaultPadding; }

    protected abstract int getSpaceY();

    abstract protected int getSpaceX();


    protected int dip2px(float dipValue) {
        final float scale = Resources.getSystem().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }



    public void start() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            startAnimator();
        } else {
            this.post(new Runnable() {//可以避免页面未初始化完成造成的 空白
                @Override
                public void run() {
                    startAnimator();
                }
            });
        }
    }

    private void startAnimator() {
        if ( starting) {//只能绘制一次 或者正在绘制过程中的话不能再次绘制
            return;
        }
        starting = true;
        valueAnimator = ValueAnimator.ofFloat(0, mAnimatedValueMax).setDuration(5000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatedValue = (float) valueAnimator.getAnimatedValue();
                if (starting) {
                    System.out.println("mAnimatedValue:" + mAnimatedValue);
                    invalidateOtherData();
                    invalidate();
                }
            }
        });
        valueAnimator.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                starting = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        valueAnimator.start();
    }

    protected  void invalidateOtherData(){};

    public void postDelayedInvalidate() {
        onTouch = false;//置为响应触摸操作的绘制
        getParent().requestDisallowInterceptTouchEvent(false);//离开绘制区域,拦截触摸事件
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                invalidate();
            }
        }, 1000);
    }

    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler();

}

style样式属性
 <!--    折线图图表样式-->
    <declare-styleable name="GraphStyle">
        <attr name="graphTitle" format="string"/>
        <attr name="xAxisNmae" format="string"/>
        <attr name="yAxisNmae" format="string"/>
        <attr name="axisTextSize" format="dimension|integer"/>
        <attr name="axisTextColor" format="color|integer"/>
        <attr name="axisDevidedSizeX" format="integer"/>
        <attr name="axisDevidedSizeY" format="integer"/>
    </declare-styleable>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值