使用ValueAnimator自定义动态XY图表View

使用ValueAnimator自定义动态XY图表View

效果

无废话,先上图:
使用ValueAnimator自定义动态XY图表View_第1张图片


分析需求

-1、x轴展示七天日期,y轴展示七天日期对应的值。
-2、需要一个动画,顺序的每天的数据展示出来。
-3、需要另一个动画,某一天的数据是从底部向上平衡到实际值。


实现思路

  • 使用drawLine画x轴与y轴的带箭头的线
  • 使用drawLine画x轴与y轴的刻度线及刻度值
  • 使用ValueAnimator产生个时间差依次准备去画点
  • 使用另个ValueAnimator产生个时间差drawCircle画红点
  • 将每个点用Path存储起来,然后drawPath即可画连起来的折线

实现

使用drawLine画x轴与y轴的带箭头的线

x轴,以左下角的起点作为起点。那么起点的x轴上的坐标是竖向坐标靠近左边半个箭头宽度mArrowSize+本身的getPaddingLeft。y轴可以同理推出为控件的高getHeight-底部的getPaddingBottom及横向坐标下半个箭头的高度mArrowSize

//画x轴
        int startX1 = getPaddingLeft() + mArrowSize;
        int startY1 = getHeight() - getPaddingBottom() - mArrowSize;//以左下角原点坐为起点
        int stopX1 = getWidth() - getPaddingRight();
        canvas.drawLine(startX1, startY1, stopX1, startY1,mXYPaint);
        canvas.drawLine(stopX1-mArrowSize,getHeight()-getPaddingBottom()-2*mArrowSize, stopX1, startY1,mXYPaint);
        canvas.drawLine(stopX1-mArrowSize,getHeight()-getPaddingBottom(), stopX1, startY1,mXYPaint);

画y轴

//y轴
canvas.drawLine(startX1, startY1, startX1,getPaddingTop(),mXYPaint);
        canvas.drawLine(getPaddingLeft(),getPaddingTop()+mArrowSize, startX1,getPaddingTop(),mXYPaint);
        canvas.drawLine(getPaddingLeft()+2*mArrowSize,getPaddingTop()+mArrowSize, startX1,getPaddingTop(),mXYPaint);

使用drawLine画x轴与y轴的刻度线及刻度值

需要注意的是x轴以为数据集大小来平分,为了箭头与最后一个刻盘预留些空间,需要将宽度减掉预留空间mRemainSpaceSize
示例起见y轴固定分成5个部分,为了让y轴上的坐标值看起来尽量大些,使用y轴上的实际数据最大值-实际数据最小值除3得出每份perGapY,分布上面4个部分的值,而y轴上的最小值就是实际数据最小值+每份perGapY(最小刻度值在最下面)

//画x轴的刻度
        int realWidth = getWidth() - getPaddingLeft() - getPaddingRight() - mArrowSize - mRemainSpaceSize;
        int perWidthSize = dataSize != 0? realWidth/dataSize : 0;
        for (int i=0;iint startX = getPaddingLeft() + mArrowSize + perWidthSize * (i + 1);
            int startY = startY1;
            canvas.drawLine(startX, startY, startX, startY - mMarkLineSize, mXYPaint);
            String xData = formateDate(xDatas.get(i));
            Rect textRect = new Rect();
            mXYTextPaint.getTextBounds(xData, 0, xData.length(), textRect);

            int textStartX = startX - textRect.width() / 2;
            int textStartY = startY + textRect.height()  + 10;
            canvas.drawText(xData,textStartX,textStartY,mXYTextPaint);
        }


        int realStartY = 0;
        //画y轴的刻度
        //y轴上分多少个刻度
        mYDataSize = 5;
        double perGapY = (maxYValue - minYValue) / (mYDataSize-2);
        int realHeight = getHeight() - getPaddingTop() - getPaddingBottom() - mArrowSize - mRemainSpaceSize;
        int perHeightSize = mYDataSize != 0? realHeight/ mYDataSize : 0;
        for (int i=1;i<=mYDataSize;i++){
            int startX = startX1;
            int startY = getHeight() - getPaddingBottom() - mArrowSize - perHeightSize * i;
            canvas.drawLine(startX, startY, startX + mMarkLineSize, startY, mXYPaint);
            if (i == 2){
                realStartY = startY;  //min y轴 开始的坐标位置
            }

            String yData = decimalFormatFor2Rate(minYValue + (i - 2) * perGapY);
            Rect textRect = new Rect();
            mXYTextPaint.getTextBounds(yData,0,yData.length(),textRect);

            int textStartX = startX - textRect.width() - 10;
            int textStartY = startY + textRect.height() / 2;
            canvas.drawText(yData,textStartX,textStartY,mXYTextPaint);
        }

使用ValueAnimator产生个时间差依次准备去画点

使用mDrawDotIndex来表示即将画的点的位置,只有在未开始即将开始时启动动画,否则会无限循环启动动画,不停的invalidate不停的画。

if (-1 == mDrawDotIndex){
            mAnimator = ValueAnimator.ofInt(0, dataSize * 100);
            mAnimator.setDuration(5000);
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int animatedValue = (Integer) animation.getAnimatedValue();
                    int i = animatedValue / 100;
                    if (!mReadedNumber.contains(i)&&i//准备画第i个动画
                        mDrawDotIndex = i;
                        mPercent = -1;
                        postInvalidate();
                    }
                    isDrawing = true;
                }
            });
            mAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    mDrawDotIndex = dataSize - 1;
                    isDrawing = false;
                    mReadedNumber.clear();
                    mPath.reset();
                    mPathReadedIndex.clear();
                    mAnimator.removeAllUpdateListeners();
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    mDrawDotIndex = dataSize - 1;
                    isDrawing = false;
                }

                @Override
                public void onAnimationRepeat(Animator animation) {
                }
            });
            mAnimator.start();
        }

使用另个ValueAnimator产生个时间差drawCircle画红点

这里简单起点直接指定红点的半径10,将即将要画的最后一个点mDrawDotIndex使用动画从底部往上画出来,使用drawCircle不断的变化y轴即可形成从底部往上平衡的效果。

//画点
        int radius = 10;
        for (int i=0;iif (i<=mDrawDotIndex){
                double yValue = yDatas.get(i);
                double number =  (yValue - minYValue) / perGapY;
                final int startX = getPaddingLeft() + mArrowSize + perWidthSize * (i + 1) - radius/2;
                final int startY = (int) (realStartY - number*perHeightSize - radius/2);
                if (i != mDrawDotIndex){
                    canvas.drawCircle(startX, startY, radius, mDotPaint);
                    if (i != 0 && !mPathReadedIndex.contains(i)){
                        mPath.lineTo(startX, startY);
                        mPathReadedIndex.add(i);
                    }
                }else{
                    //动画效果
                    int fromX = startY1;

                    if (-1 == mPercent){
                        mDotAnimator = ValueAnimator.ofFloat(0, 1);
                        mDotAnimator.setDuration(1000);
                        mDotAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator animation) {
                                mPercent = (float) animation.getAnimatedValue();
                                invalidate();
                            }
                        });
                        mDotAnimator.addListener(new ValueAnimator.AnimatorListener() {
                            @Override
                            public void onAnimationStart(Animator animation) {
                                mAnimator.pause();
                            }

                            @Override
                            public void onAnimationEnd(Animator animation) {
                                mAnimator.resume();
                                mPercent = 1;
                                if (mDrawDotIndex != 0 && !mPathReadedIndex.contains(mDrawDotIndex)) {
                                    mPath.lineTo(startX, startY);
                                    mPathReadedIndex.add(mDrawDotIndex);
                                }
                                animation.removeAllListeners();
                            }

                            @Override
                            public void onAnimationCancel(Animator animation) {
                                mPercent = 1;
                            }

                            @Override
                            public void onAnimationRepeat(Animator animation) {
                            }
                        });
                        mDotAnimator.start();
                    }else{
                        int gapY = startY - fromX;
                        int thisStartX = (int) (startY - (1-mPercent)*gapY);
                        canvas.drawCircle(startX, thisStartX, radius, mDotPaint);
                    }
                }

                if (i == 0&&!mPathReadedIndex.contains(i)) {
                    mPath.moveTo(startX,startY);
                    mPathReadedIndex.add(i);
                }

            }
        }

将每个点用Path存储起来,然后drawPath即可画连起来的折线

上面已经了个成员变量Path,最后一个点可以在mDotAnimator动画结束时添加进去。这样本次动画mAnimator的最后一个点在结束平稳效果后添加进path中,再将其drawPath画出来。

canvas.drawPath(mPath,mFoldLinePaint);

完整代码

自定义DynamicXYChartView

package com.afeita.test;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
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.util.TypedValue;
import android.view.View;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * 
author: chenshufei *
date: 15/10/28 *
email: [email protected] */
public class DynamicXYChartView extends View { private int mWidthSize = 0; private int mHeightSize = 0; private Map mData; private Paint mDotPaint; private Paint mFoldLinePaint; private Paint mXYPaint; private int mArrowSize; private int mRemainSpaceSize; private int mMarkLineSize; private int mXYTextSize; private Paint mXYTextPaint; private int mYDataSize; private int mDrawDotIndex = -1; private ValueAnimator mAnimator; private boolean isDrawing = false; private float mPercent = -1; private List mReadedNumber; private List mPathReadedIndex; private Path mPath; private ValueAnimator mDotAnimator; public interface OnDrawStataListener{ void onDrawStata(boolean isDrawing); } public DynamicXYChartView(Context context, AttributeSet attrs) { super(context, attrs); init(context,attrs); } public DynamicXYChartView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } public void setData(Map data,OnDrawStataListener listener) { if (isDrawing){ if (null != listener){ listener.onDrawStata(isDrawing); } }else{ this.mData = data; mDrawDotIndex = -1; postInvalidate(); } } private void init(Context context, AttributeSet attrs) { //当未指定宽与高时,即是at_most模式时,设置其默认的宽与高 mWidthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,500,getResources().getDisplayMetrics()); mHeightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,300,getResources().getDisplayMetrics()); mArrowSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()); //x与y轴留出空白空间,实际 mRemainSpaceSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, getResources().getDisplayMetrics()); mMarkLineSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()); mXYTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, getResources().getDisplayMetrics()); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //判断是否是Almost模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); switch (widthMode){ case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: widthSize = mWidthSize; break; } switch (heightMode){ case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: heightSize = mHeightSize; break; } setMeasuredDimension(widthSize, heightSize); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); //初始化 painter等 //点的 mDotPaint = new Paint(); mDotPaint.setAntiAlias(true); mDotPaint.setStyle(Paint.Style.FILL); mDotPaint.setDither(true); mDotPaint.setColor(Color.RED); //折线 mFoldLinePaint = new Paint(); mFoldLinePaint.setAntiAlias(true); mFoldLinePaint.setStyle(Paint.Style.STROKE); mFoldLinePaint.setDither(true); mFoldLinePaint.setStrokeWidth(5); mFoldLinePaint.setColor(Color.BLACK); //坐标 mXYPaint = new Paint(); mXYPaint.setAntiAlias(true); mXYPaint.setStyle(Paint.Style.FILL); mXYPaint.setDither(true); mXYPaint.setStrokeWidth(5); mXYPaint.setColor(Color.BLACK); //坐标上的文字 mXYTextPaint = new Paint(); mXYTextPaint.setAntiAlias(true); mXYTextPaint.setStyle(Paint.Style.FILL); mXYTextPaint.setDither(true); mXYTextPaint.setColor(Color.BLACK); mXYTextPaint.setTextSize(mXYTextSize); mReadedNumber = new ArrayList(); mPath = new Path(); mPathReadedIndex = new ArrayList(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // 释放资源 if (null != mAnimator){ mAnimator.cancel(); mAnimator.removeAllListeners(); mAnimator = null; } if (null != mDotAnimator){ mDotAnimator.cancel(); mDotAnimator.removeAllUpdateListeners();; mDotAnimator = null; } } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); if (mData==null){ return; } //获取多少对数据,准备组织其坐标点位置 final int dataSize = mData!=null ? mData.size() : 0; //将Map转成x与y轴上的list数据 List xDatas = new ArrayList<>(); List yDatas = new ArrayList<>(); double minYValue = Double.MAX_VALUE; double maxYValue = 0; for(Map.Entry entry : mData.entrySet()){ xDatas.add(entry.getKey()); if (entry.getValue()if (entry.getValue()>maxYValue){ maxYValue = entry.getValue(); } yDatas.add(entry.getValue()); } //不管什么情况,先把x与y轴画出来 //x轴 int startX1 = getPaddingLeft() + mArrowSize; int startY1 = getHeight() - getPaddingBottom() - mArrowSize; int stopX1 = getWidth() - getPaddingRight(); canvas.drawLine(startX1, startY1, stopX1, startY1,mXYPaint); canvas.drawLine(stopX1-mArrowSize,getHeight()-getPaddingBottom()-2*mArrowSize, stopX1, startY1,mXYPaint); canvas.drawLine(stopX1-mArrowSize,getHeight()-getPaddingBottom(), stopX1, startY1,mXYPaint); //y轴 canvas.drawLine(startX1, startY1, startX1,getPaddingTop(),mXYPaint); canvas.drawLine(getPaddingLeft(),getPaddingTop()+mArrowSize, startX1,getPaddingTop(),mXYPaint); canvas.drawLine(getPaddingLeft()+2*mArrowSize,getPaddingTop()+mArrowSize, startX1,getPaddingTop(),mXYPaint); //画x轴的刻度 int realWidth = getWidth() - getPaddingLeft() - getPaddingRight() - mArrowSize - mRemainSpaceSize; int perWidthSize = dataSize != 0? realWidth/dataSize : 0; for (int i=0;iint startX = getPaddingLeft() + mArrowSize + perWidthSize * (i + 1); int startY = startY1; canvas.drawLine(startX, startY, startX, startY - mMarkLineSize, mXYPaint); String xData = formateDate(xDatas.get(i)); Rect textRect = new Rect(); mXYTextPaint.getTextBounds(xData, 0, xData.length(), textRect); int textStartX = startX - textRect.width() / 2; int textStartY = startY + textRect.height() + 10; canvas.drawText(xData,textStartX,textStartY,mXYTextPaint); } int realStartY = 0; //画y轴的刻度 //y轴上分多少个刻度 mYDataSize = 5; double perGapY = (maxYValue - minYValue) / (mYDataSize-2); int realHeight = getHeight() - getPaddingTop() - getPaddingBottom() - mArrowSize - mRemainSpaceSize; int perHeightSize = mYDataSize != 0? realHeight/ mYDataSize : 0; for (int i=1;i<=mYDataSize;i++){ int startX = startX1; int startY = getHeight() - getPaddingBottom() - mArrowSize - perHeightSize * i; canvas.drawLine(startX, startY, startX + mMarkLineSize, startY, mXYPaint); if (i == 2){ realStartY = startY; //min y轴 开始的坐标位置 } String yData = decimalFormatFor2Rate(minYValue + (i - 2) * perGapY); Rect textRect = new Rect(); mXYTextPaint.getTextBounds(yData,0,yData.length(),textRect); int textStartX = startX - textRect.width() - 10; int textStartY = startY + textRect.height() / 2; canvas.drawText(yData,textStartX,textStartY,mXYTextPaint); } if (-1 == mDrawDotIndex){ mAnimator = ValueAnimator.ofInt(0, dataSize * 100); mAnimator.setDuration(5000); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int animatedValue = (Integer) animation.getAnimatedValue(); int i = animatedValue / 100; if (!mReadedNumber.contains(i)&&i//准备画第i个动画 mDrawDotIndex = i; mPercent = -1; postInvalidate(); } isDrawing = true; } }); mAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mDrawDotIndex = dataSize - 1; isDrawing = false; mReadedNumber.clear(); mPath.reset(); mPathReadedIndex.clear(); mAnimator.removeAllUpdateListeners(); } @Override public void onAnimationCancel(Animator animation) { mDrawDotIndex = dataSize - 1; isDrawing = false; } @Override public void onAnimationRepeat(Animator animation) { } }); mAnimator.start(); } //画点 int radius = 10; for (int i=0;iif (i<=mDrawDotIndex){ double yValue = yDatas.get(i); double number = (yValue - minYValue) / perGapY; final int startX = getPaddingLeft() + mArrowSize + perWidthSize * (i + 1) - radius/2; final int startY = (int) (realStartY - number*perHeightSize - radius/2); if (i != mDrawDotIndex){ canvas.drawCircle(startX, startY, radius, mDotPaint); if (i != 0 && !mPathReadedIndex.contains(i)){ mPath.lineTo(startX, startY); mPathReadedIndex.add(i); } }else{ //动画效果 int fromX = startY1; if (-1 == mPercent){ mDotAnimator = ValueAnimator.ofFloat(0, 1); mDotAnimator.setDuration(1000); mDotAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPercent = (float) animation.getAnimatedValue(); invalidate(); } }); mDotAnimator.addListener(new ValueAnimator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { mAnimator.pause(); } @Override public void onAnimationEnd(Animator animation) { mAnimator.resume(); mPercent = 1; if (mDrawDotIndex != 0 && !mPathReadedIndex.contains(mDrawDotIndex)) { mPath.lineTo(startX, startY); mPathReadedIndex.add(mDrawDotIndex); } animation.removeAllListeners(); } @Override public void onAnimationCancel(Animator animation) { mPercent = 1; } @Override public void onAnimationRepeat(Animator animation) { } }); mDotAnimator.start(); }else{ int gapY = startY - fromX; int thisStartX = (int) (startY - (1-mPercent)*gapY); canvas.drawCircle(startX, thisStartX, radius, mDotPaint); } } if (i == 0&&!mPathReadedIndex.contains(i)) { mPath.moveTo(startX,startY); mPathReadedIndex.add(i); } } } canvas.drawPath(mPath,mFoldLinePaint); } public static String decimalFormatFor2Rate(double rate) { return new DecimalFormat("#,##0.00").format(rate); } public static String formateDate(Date date){ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM-dd"); return simpleDateFormat.format(date); } }

使用DynamicXYChartView的activity代码如下:

package com.afeita.test;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import com.example.chenshufei.myapplication.R;

import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;

/**
 * 
author: chenshufei *
date: 15/10/21 *
email: [email protected] */
public class SecondActivity extends Activity implements View.OnClickListener { private DynamicXYChartView mDynamicXYChartView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); findViewById(R.id.btn_repaint).setOnClickListener(SecondActivity.this); Map dateDoubleMap = emulateData(); mDynamicXYChartView = (DynamicXYChartView) findViewById(R.id.dxycv_data); mDynamicXYChartView.setData(dateDoubleMap,null); } private Map emulateData() { Map dateDoubleMap = new LinkedHashMap<>(); Random random = new Random(); Calendar calendar = Calendar.getInstance(); for (int i = 6;i>=0;i--){ calendar.add(Calendar.DATE,-1*i); double value = random.nextDouble() * 10 + 5; dateDoubleMap.put(calendar.getTime(), value); calendar = Calendar.getInstance(); } return dateDoubleMap; } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_repaint: mDynamicXYChartView.setData(emulateData(), new DynamicXYChartView.OnDrawStataListener() { @Override public void onDrawStata(boolean isDrawing) { Toast.makeText(SecondActivity.this, "正在绘制中,请稍候...", Toast.LENGTH_SHORT).show(); } }); break; } } }

布局


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical" android:layout_width="match_parent"
              android:layout_height="match_parent">


    <Button
        android:id="@+id/btn_repaint"
        android:layout_marginTop="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="重新绘制图表"
        android:layout_gravity="center_horizontal"/>

    <com.afeita.test.DynamicXYChartView
        android:id="@+id/dxycv_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        />
LinearLayout>

注意:为了使用DynamicXYChartView在布局xml中支持wrap_content,这时指定了当计算出是AT_MOST时,分别指定了宽为500dp,高为300dp。

你可能感兴趣的:(android开发)