餅圖本身沒有什麼難度,難點就在產品要求點擊圖例,餅圖要有對應區域的動畫,這個可把我折磨了很久,MPAndroidChart沒有改造出來,無奈去自定義的一個餅圖,也是站在前人的肩膀上修改爲自己需要的餅圖效果
好害怕這樣侵權了公司UI妹妹的設計,爲保護公司業務隱私,把公司數據給塗抹了
代碼在下面;先介紹圖例和餅圖關聯起來的代碼
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// pieChart.mCurrentPressedPosition = 0;
pieChart.mCurrentPressedPosition = 1;//可以做到點擊圖例找到對應的餅圖並有動畫效果,只需要指定position即可
pieChart.startTouchDownAnim();
}
});
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.graphics.RectF;
import android.text.Layout;
import android.text.SpannableString;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Display;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* Created by inf on 2016/11/29.
*/
// TODO: 2017/12/28 添加點擊事件的回調
public class PieChart extends View implements GestureDetector.OnGestureListener {
public static final int CLICK_ANIM_LENGTH = 50;
public static final int DURATION = 200;
/**
* view的寬高
*/
private int mWidth, mHeight;
/**
* 餅狀圖的半徑、內部空白圓的半徑
*/
private float mRadius, mInnerRadius;
/**
* 餅狀圖的外切
*/
private RectF mPieRect;
/**
* 各種畫筆
*/
private Paint mPiePaint, mBlankPaint, mLinePaint, mTextPaint, mLegendPaint;
private TextPaint mCenterTextPaint;
/**
* 實體類集合
*/
private List<IPieElement> mElements;
/**
* 各個元素的角度
*/
private List<Float> mAngles = new ArrayList<>();
/**
* 元素的顏色
*/
private List<String> mColors = new ArrayList<>();
/**
* 元素的描述
*/
private List<String> mDescription = new ArrayList<>();
/**
* 元素的佔比
*/
private List<String> mPercents = new ArrayList<>();
/**
* 中心文字
*/
private CharSequence mText;
private SparseArray<double[]> angles = new SparseArray<>();
private GestureDetector mDetector;
private boolean mIsAnimEnable;
public int y;
public int x;
private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()};
public PieChart(Context context) {
this(context, null);
}
public PieChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PieChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setData(List<IPieElement> elements) {
mElements = elements;
setValuesAndColors();
invalidate();
}
public void setAnimEnable(boolean enable) {
mIsAnimEnable = enable;
}
private void init() {
mDetector = new GestureDetector(getContext(), this);
mDetector.setIsLongpressEnabled(false);
mPieRect = new RectF();
mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPiePaint.setColor(Color.RED);
mBlankPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBlankPaint.setStrokeWidth((float) 0.1);
mBlankPaint.setColor(Color.WHITE);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStrokeWidth(4);
mLinePaint.setStyle(Paint.Style.STROKE);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(30);
mTextPaint.setColor(Color.WHITE);
mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCenterTextPaint.setTextSize(30);
mCenterTextPaint.setColor(Color.BLACK);
mLegendPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLegendPaint.setTextSize(30);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mDetector.onTouchEvent(event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0, height = 0;
if (widthMode == MeasureSpec.AT_MOST) {
width = (int) getSize();
} else {
width = widthSize;
}
if (heightMode == MeasureSpec.AT_MOST) {
height = (int) getSize();
} else {
height = heightSize;
}
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w - getPaddingLeft() - getPaddingRight();
mHeight = h - getPaddingTop() - getPaddingBottom();
mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.6);
resetRect();
mInnerRadius = (float) (mRadius * 0.6);
}
private Path mPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
//從12點方向開始畫
float sweepedAngle = -90;
mTextPaint.setTextSize(30);
for (int i = 0; mElements != null && mElements.size() > i; i++) {
//設置扇形的顏色
mPiePaint.setColor(Color.parseColor(mColors.get(i)));
mLinePaint.setColor(Color.parseColor(mColors.get(i)));
//畫扇形
if (mIsAnimEnable && i == mCurrentPressedPosition) {
setRect(sweepedAngle, i, mCurrentLength);
}
canvas.drawArc(mPieRect, sweepedAngle, mAngles.get(i), true, mPiePaint);
resetRect();
//掃過的角度++
double[] ang = new double[2];
ang[0] = sweepedAngle + 90;
ang[1] = ang[0] + mAngles.get(i);
angles.put(i, ang);
sweepedAngle += mAngles.get(i);
String percentText = mPercents.get(i) + "%";
//畫分割線
canvas.drawArc(mPieRect, sweepedAngle, 1, true, mBlankPaint);
sweepedAngle += 1;
float x = getXCoordinate(mAngles.get(i), sweepedAngle);
float y = getYCoordinate(mAngles.get(i), sweepedAngle);
mPath.reset();
mPath.moveTo(x, y);
mPath.lineTo((float) (x * 1.2), (float) (y * 1.2));
// canvas.drawPath(mPath, mLinePaint);
mPath.reset();
mPath.moveTo((float) (x * 1.2), (float) (y * 1.2));
//水平線的長度設置爲文字長度的1.5倍
float horizontalLineLength = (float) (getTextWidth(mTextPaint, percentText) * 1.5);
//當線的起點在第三、四象限時,先把path移動到終點位置,然後向起點畫線,使後面畫文字時,文字方向是正確的
if (x < 0) {
horizontalLineLength = -horizontalLineLength;
mPath.moveTo((float) (x * 1.2) + horizontalLineLength, (float) (y * 1.2));
mPath.lineTo((float) (x * 1.2), (float) (y * 1.2));
} else {
mPath.lineTo((float) (x * 1.2) + horizontalLineLength, (float) (y * 1.2));
}
// canvas.drawPath(mPath, mLinePaint);
//垂直方向的偏移量,畫文字時,文字顯示在path的下方,爲了讓文字顯示在上方,設置一個文字高度的垂直偏移量
float offsetV = -getTextHeight(mTextPaint, percentText);
canvas.drawTextOnPath(percentText, mPath, 0, offsetV, mTextPaint);
}
mPath.close();
//這裏開始畫中心空白部分以及文字,空白部分半徑設置爲整個圓半徑的0.6倍
RectF holeRect = mRectBuffer[0];
holeRect.left = x - mInnerRadius;
holeRect.top = y - mInnerRadius;
holeRect.right = x + mInnerRadius;
holeRect.bottom = y + mInnerRadius;
RectF boundingRect = mRectBuffer[1];
boundingRect.set(holeRect);
canvas.drawCircle(0, 0, mInnerRadius, mBlankPaint);
mCenterTextPaint.setTextAlign(Paint.Align.CENTER);
if (!TextUtils.isEmpty(mText)) {
// String[] texts = String.valueOf(mText).split(System.getProperty("line.separator"));//支持中間文本換行
// for (String text : texts) {
// calculateTextPaint(text);
// canvas.drawText(text, 0, y, mCenterTextPaint);
// y = y + 40;
// }
StaticLayout mCenterTextLayout = new StaticLayout(mText, mCenterTextPaint, canvas.getWidth(),
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
float layoutHeight = mCenterTextLayout.getHeight();
canvas.translate(0, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f);
mCenterTextLayout.draw(canvas);
}
canvas.restore();
if (mShowLegend) {
drawLegend(canvas);
}
}
private void resetRect() {
mPieRect.left = -mRadius;
mPieRect.top = -mRadius;
mPieRect.right = mRadius;
mPieRect.bottom = mRadius;
}
public void setRect(float sweepedAngle, int i, int animatedLength) {
float currentCenterAngle = sweepedAngle + mAngles.get(i) / 2;
if (currentCenterAngle >= -90 && currentCenterAngle <= 0) {
double actualAng = currentCenterAngle + 90;
mPieRect.right += Math.sin(getRadian(actualAng)) * animatedLength;
mPieRect.top -= Math.cos(getRadian(actualAng)) * animatedLength;
} else if (currentCenterAngle > 0 && currentCenterAngle <= 90) {
mPieRect.right += Math.cos(getRadian(currentCenterAngle)) * animatedLength;
mPieRect.bottom += Math.sin(getRadian(currentCenterAngle)) * animatedLength;
} else if (currentCenterAngle > 90 && currentCenterAngle <= 180) {
double actualAng = currentCenterAngle - 90;
mPieRect.left -= Math.sin(getRadian(actualAng)) * animatedLength;
mPieRect.bottom += Math.cos(getRadian(actualAng)) * animatedLength;
} else {
double actualAng = currentCenterAngle - 180;
mPieRect.left -= Math.cos(getRadian(actualAng)) * animatedLength;
mPieRect.top -= Math.sin(getRadian(actualAng)) * animatedLength;
}
}
private double getRadian(double actualAng) {
return actualAng * 2 * Math.PI / 360;
}
private Rect rect = new Rect();
/**
* 設置中心文字
*
* @param
*/
public void setCenterText(CharSequence text) {
if (text == null)
mText = "";
else {
mText = text;
}
}
/**
* 計算角度值和各個值的佔比
*/
private void setValuesAndColors() {
float sum = 0;
if (mElements != null && mElements.size() > 0) {
for (IPieElement ele : mElements) {
sum += ele.getValue();
mColors.add(ele.getColor());
mDescription.add(ele.getDescription());
}
BigDecimal totleAngel = BigDecimal.valueOf(360 - mElements.size());
for (int i = 0; i < mElements.size(); i++) {
IPieElement ele = mElements.get(i);
BigDecimal bigDecimal = new BigDecimal(String.valueOf(ele.getValue()));
BigDecimal sumBigDecimal = BigDecimal.valueOf(sum);
BigDecimal res = bigDecimal.divide(sumBigDecimal, 5, BigDecimal.ROUND_HALF_UP);
//計算角度
BigDecimal angle = res.multiply(totleAngel);
mAngles.add(angle.floatValue());
//計算百分比保留兩位小數並保存
mPercents.add(bigDecimal.multiply(new BigDecimal(100)).divide(sumBigDecimal, 2, BigDecimal.ROUND_HALF_UP).toPlainString());
}
}
}
@Override
public boolean onDown(MotionEvent motionEvent) {
mCurrentPressedPosition = getPosition(motionEvent);
startTouchDownAnim();
return true;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
mCurrentPressedPosition = getPosition(motionEvent);
// startTouchUpAnim();
if (mCurrentPressedPosition >= 0 && mListener != null) {
mListener.onItemClick(mCurrentPressedPosition);
}
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
mCurrentPressedPosition = getPosition(motionEvent);
startTouchUpAnim();
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
private int mCurrentLength;
public int mCurrentPressedPosition;
public void startTouchDownAnim() {
// ValueAnimatorCompat va= new ValueAnimatorCompat();
ValueAnimator va = ValueAnimator.ofInt(0, CLICK_ANIM_LENGTH);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentLength = (int) animation.getAnimatedValue();
invalidate();
}
});
va.setDuration(DURATION);
va.start();
}
public void startTouchUpAnim() {
// ValueAnimatorCompat va= new ValueAnimatorCompat();
ValueAnimator va = ValueAnimator.ofInt(CLICK_ANIM_LENGTH, 0);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentLength = (int) animation.getAnimatedValue();
invalidate();
}
});
va.setDuration(DURATION);
va.start();
}
/**
* 獲取點擊位置座標對應的餅狀圖的區域
*
* @param motionEvent
* @return 數據的position
*/
public int getPosition(MotionEvent motionEvent) {
float x = motionEvent.getX();
float y = motionEvent.getY();
float centerX = getWidth() / 2;
float centerY = getHeight() / 2;
//判斷點擊位置是否在innerRadius內
if ((Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) < mInnerRadius * mInnerRadius) {
return -1;
}
//判斷點擊位置是否在餅狀圖以外
if ((Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) > mRadius * mRadius) {
return -1;
}
// 判斷象限
// 第一象限
double angle = 0;
if (x > centerX && y < centerY) {
angle = Math.toDegrees(Math.atan((Math.abs(x - centerX)) / (Math.abs(centerY - y))));
} else if (x > centerX && y > centerY) {//第二象限
angle = Math.toDegrees(Math.atan(((y - centerY) / (x - centerX))));
angle += 90;
} else if (x < centerX && y > centerY) {//第三象限
angle = Math.toDegrees(Math.atan(((centerX - x) / (y - centerY))));
angle += 180;
} else if (x < centerX && y < centerY) {//第四象限
angle = Math.toDegrees(Math.atan(((centerY - y) / (centerX - x))));
angle += 270;
}
for (int i = 0; i < angles.size(); i++) {
double[] angs = angles.get(i);
if (angle >= angs[0] && angle <= angs[1]) {
return i;
}
}
return -1;
}
private OnItemClickListener mListener;
public interface OnItemClickListener {
void onItemClick(int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
mListener = listener;
}
private boolean mShowLegend = true;
/**
* 圖例開關
*
* @param enable
*/
public void enableLegend(boolean enable) {
mShowLegend = enable;
}
/**
* 畫圖例
*
* @param canvas
*/
private void drawLegend(Canvas canvas) {
float verticalOffset = 0;
for (int i = 0; i < mElements.size(); i++) {
IPieElement ele = mElements.get(i);
mLegendPaint.setColor(Color.parseColor(ele.getColor()));
mLegendPaint.getTextBounds(ele.getDescription(), 0, ele.getDescription().length(), rect);
verticalOffset = rect.height() + 20;
canvas.translate(0, verticalOffset);
mLegendPaint.setStrokeWidth(8);
canvas.drawLine(10, 0, 80, 0, mLegendPaint);
canvas.drawText(ele.getDescription(), 90, rect.height() / 2, mLegendPaint);
}
}
/**
* 把文字分兩行,並畫在圓內接正方形內,依此計算畫筆的textSize
*
* @param text
*/
private void calculateTextPaint(String text) {
if (!TextUtils.isEmpty(text)) {
measureText(text, 200);
}
}
/**
* 遞歸調用,計算testSize
*
* @param text
* @param textSize
*/
private void measureText(String text, int textSize) {
mTextPaint.setTextSize(textSize);
float width = getTextWidth(mTextPaint, text);
float height = getTextHeight(mTextPaint, text);
if (width > mInnerRadius * 1.41421) {
textSize--;
measureText(text, textSize);
return;
}
if (height * 2.5 > mInnerRadius * 1.41421) {
textSize--;
measureText(text, textSize);
}
}
private float getTextHeight(Paint paint, String text) {
Rect rect = new Rect();
paint.getTextBounds(text, 0, text.length(), rect);
return rect.height();
}
/**
* @param paint
* @param text
* @return
*/
private float getTextWidth(Paint paint, String text) {
Rect rect = new Rect();
paint.getTextBounds(text, 0, text.length(), rect);
return rect.width();
}
/**
* 獲取圓弧中點的x軸座標
*
* @param angle 圓弧對應的角度
* @param sweepedAngle 掃過的角度
* @return 圓弧中點的x軸座標
*/
private float getXCoordinate(float angle, float sweepedAngle) {
float x = (float) (mRadius * Math.cos(Math.toRadians(sweepedAngle - angle / 2)));
return x;
}
/**
* 獲取圓弧中點的y軸座標
*
* @param angle 圓弧對應的角度
* @param sweepedAngle 掃過的角度
* @return 圓弧中點的y軸座標
*/
private float getYCoordinate(float angle, float sweepedAngle) {
float y = (float) (mRadius * Math.sin(Math.toRadians(sweepedAngle - angle / 2)));
return y;
}
private float getSize() {
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
float widht = display.getWidth();
float height = display.getHeight();
return Math.min(widht, height);
}
public static SpannableString centerSpannableText(String start, String middle, String end) {
String stringConetnt = start + middle + end;
SpannableString str = new SpannableString(stringConetnt);
if (middle.length() <= 6) {
str.setSpan(new RelativeSizeSpan(1.6f), start.length(), start.length() + middle.length(), 0);
} else if (middle.length() < 9) {
str.setSpan(new RelativeSizeSpan(1.5f), start.length(), start.length() + middle.length(), 0);
} else if (middle.length() < 12) {
str.setSpan(new RelativeSizeSpan(1.3f), start.length(), start.length() + middle.length(), 0);
} else if (middle.length() < 18) {
str.setSpan(new RelativeSizeSpan(0.9f), start.length(), start.length() + middle.length(), 0);
}
return str;
}
public static SpannableString centerSpannableText(String start) {
// String stringConetnt = start + middle + end;
String stringConetnt = start;
SpannableString str = new SpannableString(stringConetnt);
if (start.length() <= 6) {
str.setSpan(new RelativeSizeSpan(1.6f), start.length(), 0 + start.length(), 0);
} else if (start.length() < 9) {
str.setSpan(new RelativeSizeSpan(1.0f), start.length(), start.length() + start.length(), 0);
}
// else if (middle.length() < 9) {
// str.setSpan(new RelativeSizeSpan(1.5f), start.length(), start.length() + middle.length(), 0);
// } else if (middle.length() < 12) {
// str.setSpan(new RelativeSizeSpan(1.3f), start.length(), start.length() + middle.length(), 0);
// } else if (middle.length() < 18) {
// str.setSpan(new RelativeSizeSpan(0.9f), start.length(), start.length() + middle.length(), 0);
// }
return str;
}
}