android软件百分比怎么实现,Android实现轻量线性与百分比图表的方法

本文详细介绍了如何在Android中自定义线性图表和百分比圆形图表,包括尺寸计算、滑动固定效果和动画绘制的实现。作者强调了自定义视图的重要性,并提供了具体的代码示例,包括滑动时部分区域固定显示的技巧以及通过Path动画实现平滑的图表过渡效果。此外,文章还展示了百分比圆形图表中数字在扇形区域的定位计算方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

经常要用到图表统计数据,在WEB开发中图表绘制是一件简单的事情,因为有比较多的开源方案。但在Android中开源方案并不多。但目前github上有多个关于图表的框架,比如MPAndroidChart很好,但是很大,没必要因为一个小的图标让工程项目扩大很多,另外有些轻量级的框架,但是个人感觉都很难满足自己的需求,再者就算很好的框架,那也是别人的,只有自己动手写起来,了解前前后后的坑,自己才能成长,而且在写的过程,我们能发现更多的细节,比如绘制的时候内存分配的问题,Canvas直接绘制和通过Bitmap绘制等等,所以这篇文章的目的:

1.是给大家提供自定义view绘制的思路

2.滑动自定义view的部分区域怎么实现

3.path动画绘制的实现

4.熟悉canvas的api,总之能直接动手了,那就自定义view就通关了,所以就写这篇文章主要是鼓励大家多去实现。

效果图

797d097aa4d1ae44e61de34152addf12.gif

线性图表实现的思路:

线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的,注意,这句话只适用大部分线性表,而不是全部。

由于屏幕的宽度有限,所以我们一屏经过计算,最好显示的7个点,所以我们首先需要对我们的view宽度进行计算,首先拿到屏幕的宽度,然后再进行/7,得到每个间隔的宽度,然后乘以我们x的坐标点的个数,其中的onMeasure的方法:

int widthParentMeasureMode = MeasureSpec.getMode(widthMeasureSpec);

int widthParentMeasureSize = MeasureSpec.getSize(widthMeasureSpec);

int heightParentMeasureMode = MeasureSpec.getMode(heightMeasureSpec);

int heightParentMeasureSize = MeasureSpec.getSize(heightMeasureSpec);

int resultWidthSize = 0;

int resultHeightSize = 0;

int resultWidthMode = MeasureSpec.EXACTLY;//用来对childView进行计算的

int resultHeightMode = MeasureSpec.EXACTLY;

int paddingWidth = getPaddingLeft() + getPaddingRight();

int paddingHeight = getPaddingTop() + getPaddingBottom();

ViewGroup.LayoutParams thisLp = getLayoutParams();

switch (widthParentMeasureMode) {

//父类不加限制给子类

case MeasureSpec.UNSPECIFIED:

//这个代表在布局写死了宽度

if (thisLp.width > 0) {

resultWidthSize = thisLp.width;

resultWidthMode = MeasureSpec.EXACTLY;

} else {

resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);

resultWidthMode = MeasureSpec.UNSPECIFIED;

}

break;

case MeasureSpec.AT_MOST:

//这个代表在布局写死了宽度

if (thisLp.width > 0) {

resultWidthSize = thisLp.width;

resultWidthMode = MeasureSpec.EXACTLY;

} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {

resultWidthSize = Math.max(0, widthParentMeasureSize - paddingWidth);

resultWidthMode = MeasureSpec.AT_MOST;

} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);

resultWidthMode = MeasureSpec.AT_MOST;

}

break;

case MeasureSpec.EXACTLY:

//这个代表在布局写死了宽度

if (thisLp.width > 0) {

resultWidthSize = Math.min(widthParentMeasureSize, thisLp.width);

resultWidthMode = MeasureSpec.EXACTLY;

} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {

resultWidthSize = widthParentMeasureSize;

resultWidthMode = MeasureSpec.EXACTLY;

} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);

resultWidthMode = MeasureSpec.AT_MOST;

}

break;

}

switch (heightParentMeasureMode) {

//父view不加限制

case MeasureSpec.UNSPECIFIED:

//这个代表在布局写死了宽度

if (thisLp.height > 0) {

resultHeightSize = thisLp.height;

resultHeightMode = MeasureSpec.EXACTLY;

} else {

resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());

resultHeightMode = MeasureSpec.UNSPECIFIED;

}

break;

case MeasureSpec.AT_MOST:

if (thisLp.height > 0) {

resultHeightSize = heightParentMeasureSize;

resultHeightMode = MeasureSpec.EXACTLY;

} else if (thisLp.height == ViewGroup.LayoutParams.MATCH_PARENT) {

resultHeightSize = Math.max(0, heightParentMeasureSize - paddingHeight);

resultHeightMode = MeasureSpec.AT_MOST;

} else if (thisLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {

resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());

resultHeightMode = MeasureSpec.UNSPECIFIED;

}

break;

case MeasureSpec.EXACTLY:

//这个代表在布局写死了宽度

if (thisLp.height > 0) {

resultHeightSize = Math.min(heightParentMeasureSize, getMeasuredWidth());

resultHeightMode = MeasureSpec.EXACTLY;

} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {

resultHeightSize = heightParentMeasureSize;

resultHeightMode = MeasureSpec.EXACTLY;

} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {

resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());

resultHeightMode = MeasureSpec.AT_MOST;

}

break;

}

setMeasuredDimension(MeasureSpec.makeMeasureSpec(resultWidthSize, resultWidthMode),

MeasureSpec.makeMeasureSpec(resultHeightSize, resultHeightMode));

设置好了尺寸,我们就可以绘制界面,这里我们onDraw的时候,就依次绘制横线和竖线,在绘制横线的时候,将Y坐标的数字一起绘制上去,同理绘制竖线的时候,把x坐标的数字绘制上去,折线的画根据数字计算出坐标点,然后创建一个path,首先moveTo(firstX,firstY),然后lineTo下面的点就可以了,最后绘制上path,然而这样的话,我们在滑动的时候,会发现这个view都会跟着一起滚动了,那么我们怎样才能实现view的部分pinned呢?在这个时候,我们就需要先创建一个bitmap,将需要滑动的部分绘制到这个bitmap上去,然后bitmap在绘制到这个canvas上的时候,保持固定的位置就行了,好了再说就懵逼了,还是上代码吧:

float tempTableLeftPadding = getYMaxTextWidth();

if (mBitmap == null || mYNumCanvas == null) {

mBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - getYMaxTextWidth()), getMeasuredHeight(), Bitmap.Config.ARGB_8888);

mYNumCanvas = new Canvas(mBitmap);

}

mYNumCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

mYNumCanvas.translate(mScrollPosX,0);//这段代码就是来实现滑动的操作

//绘制横线

for (int y = 0, size = mYdots.length; y < size; y++) {

String tempText = String.valueOf(mYdots[mYdots.length - 1 - y]);

mYNumCanvas.drawLine(0, (float) (mYinterval * y), (float) (mXdots.length * mXinterval), (float) (mYinterval * y), mXlinePaint);

canvas.drawText(tempText, getYMaxTextWidth() - mYNumPaint.measureText(tempText), getYMaxTextHeight() + (float) (mYinterval * y), mYNumPaint);

}

//绘制竖线

for (int x = 0, size = mXdots.length; x <= size; x++) {

mYNumCanvas.drawLine((float) (mXinterval * x), 0, (float) (mXinterval * x), (float) (mYinterval * mYvisibleNum), mXlinePaint);

if (x >= 1) {

String tempText = mXdots[x - 1];

mYNumCanvas.drawText(tempText, (float) (mXinterval * x) - mYNumPaint.measureText(tempText) / 2, (float) (mYvisibleNum * mYinterval + getYMaxTextHeight()), mYNumPaint);

}

}

if (isAnimationOpen)//是否需要开启动画绘制,这个后面会解释实现方式

mYNumCanvas.drawPath(mLineDrawPath, mLinePaint);

else

mYNumCanvas.drawPath(mLinePath, mLinePaint);

canvas.drawBitmap(mBitmap, tempTableLeftPadding, getYMaxTextHeight() / 2, null);

上面的mScrollPosX是根据手势监听类GestureDetector来获取的:

@Override

public boolean onTouchEvent(MotionEvent event) {

if (!isAnimationOpen || isDrawOver)

return mGestureDetector.onTouchEvent(event);

return super.onTouchEvent(event);

}

然而绘制了,我们感觉还缺少了什么,嗯,没错就是动画效果,这里我们用到通过的path绘制实现动画的方案,就是先通过PathMeasure得到path的长度,然后根据动画时间,通过ValueAnimator计算它在某个时刻的坐标,然后重新进行绘制path路径:

private void startPathAnim(long duration) {

ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mLineLength);

valueAnimator.setDuration(duration);

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

float value = (Float) animation.getAnimatedValue();

// 获取当前点坐标封装到mCurrentPosition

mPathMeasure.getPosTan(value, mCurrentPosition, null);

mLineDrawPath.lineTo(mCurrentPosition[0], mCurrentPosition[1]);

invalidate();

}

});

valueAnimator.start();

}

百分比圆形图表实现

679ee79ed03546c78abba0388deb8915.png

其实这个的实现,相比上一个少了很多,大多是集中在onDraw方法里面,关键点是在百分比的数字,怎么横向显示在扇形区域,这里我就主要这个计算规则提出来:

private void drawText(Canvas canvas, float sweepAngle, float startAngle, ArcVo temp) {

float middleAngle;

middleAngle = startAngle + sweepAngle / 2;

float startX;

float startY;

float endX;

float endY;

String drawText = temp.getPercentInCircle() * 100 + "%";

if (middleAngle <= 90) {

//在第四象限

double angle = middleAngle;

angle = Math.toRadians(angle);

startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);

endX = (float) (mRaduis + Math.cos(angle) * mRaduis);

startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);

} else if (middleAngle <= 180) {

//在第三象限

double angle = 180 - middleAngle;

angle = Math.toRadians(angle);

startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);

startX = (float) (mRaduis - Math.cos(angle) * mRaduis);

endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);

} else if (middleAngle <= 270) {

//在第二象限

double angle = 270 - middleAngle;

angle = Math.toRadians(angle);

startY = endY = (float) (mRaduis - Math.cos(angle) * mRaduis);

startX = (float) (mRaduis - Math.sin(angle) * mRaduis);

endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);

} else {

//在第一象限

double angle = 360 - middleAngle;

angle = Math.toRadians(angle);

startY = endY = (float) (mRaduis - Math.sin(angle) * mRaduis);

endX = (float) (mRaduis + Math.cos(angle) * mRaduis);

startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);

}

mTextPath.reset();

mTextPath.moveTo(startX, startY);

mTextPath.lineTo(endX, endY);

if (middleAngle > 180) {

canvas.drawTextOnPath(drawText, mTextPath, 0, UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);

} else {

canvas.drawTextOnPath(drawText, mTextPath, 0, -UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);

}

}

@Override

protected void onDraw(Canvas canvas) {

if (!canDraw()) return;

float sweepAngle;

float startAngle = 0;

for (int i = 0, size = mDisArcList.size(); i < size; i++) {

ArcVo temp = mDisArcList.get(i);

mArcPaint.setColor(temp.getScanColor());

sweepAngle = temp.getPercentInCircle() * 360;

canvas.drawArc(mDrawCircleRect, startAngle, sweepAngle, true, mArcPaint);

drawText(canvas, sweepAngle, startAngle, temp);

startAngle = startAngle + sweepAngle;

}

}

使用方式:

如果你觉得你们的项目正好要用到类似的图标,在项目的gradle文件中,增加compile 'wellijohn.org.simplelinechart:linechart:0.0.2'具体的方法,欢迎移步到github上去看,已经封装成库上传至jcenter,上面有具体的使用方法(图表地址),目前暴露的方法不多,可以留言增加

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值