-1、x轴展示七天日期,y轴展示七天日期对应的值。
-2、需要一个动画,顺序的每天的数据展示出来。
-3、需要另一个动画,某一天的数据是从底部向上平衡到实际值。
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);
需要注意的是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;i<dataSize;i++){
int 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);
}
使用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<dataSize) {
mReadedNumber.add(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();
}
这里简单起点直接指定红点的半径10,将即将要画的最后一个点mDrawDotIndex
使用动画从底部往上画出来,使用drawCircle不断的变化y轴即可形成从底部往上平衡的效果。
//画点
int radius = 10;
for (int i=0;i<dataSize;i++){
if (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,最后一个点可以在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;
/** * <br /> author: chenshufei * <br /> date: 15/10/28 * <br /> email: [email protected] */
public class DynamicXYChartView extends View {
private int mWidthSize = 0;
private int mHeightSize = 0;
private Map<Date,Double> 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<Integer> mReadedNumber;
private List<Integer> 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<Date, Double> 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<Integer>();
mPath = new Path();
mPathReadedIndex = new ArrayList<Integer>();
}
@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<Date> xDatas = new ArrayList<>();
List<Double> yDatas = new ArrayList<>();
double minYValue = Double.MAX_VALUE;
double maxYValue = 0;
for(Map.Entry<Date,Double> entry : mData.entrySet()){
xDatas.add(entry.getKey());
if (entry.getValue()<minYValue){
minYValue = 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;i<dataSize;i++){
int 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<dataSize) {
mReadedNumber.add(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;i<dataSize;i++){
if (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;
/** * <br /> author: chenshufei * <br /> date: 15/10/21 * <br /> 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<Date,Double> dateDoubleMap = emulateData();
mDynamicXYChartView = (DynamicXYChartView) findViewById(R.id.dxycv_data);
mDynamicXYChartView.setData(dateDoubleMap,null);
}
private Map<Date, Double> emulateData() {
Map<Date, Double> 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;
}
}
}
布局
<?xml version="1.0" encoding="utf-8"?>
<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。