自定義控件_折線圖(動態加載,陰影效果,滑動效果)


在這裏插入圖片描述

使用

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>

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