分布条






4.94/5 (7投票s)
将值显示为堆叠柱
引言
我正在开发一个个人应用程序,它通过蓝牙与 Arduino 板通信,并且连接到 Arduino 板的数字 I/O 引脚是一个串行设备。从这个设备中提取数字并将它们分成三组后,我需要显示它们以及它们对总和的贡献。因此,我需要一种特定类型的控件来完成此操作(即,能够显示最多三个值)。ProgressBar
不适合此要求,因为它仅限于主值和次值。我需要从 View
派生一个类并自己进行绘制。
背景
这个控件可以被看作是一种“堆叠柱状图”,它显示了个别项目与整体的关系,比较了每个值对总数的贡献。您可能已经在 Microsoft Excel 中使用过这样的图表。我想要的就是这种外观,而不需要所有图表化的内容。
我的三组代表低、正常和高值,所有这些都是可选的。因此,控件将被分成 1、2 或 3 个部分,具体取决于该部分是否有值。如果没有,则 simply omitted from drawing。该部分的值将需要“叠加”在其部分中绘制。出于美观目的,我还希望四个外角是圆角的,不需要太多,但足以柔化尖锐的角。让我们开始吧...
使用代码
在 onDraw()
方法中,我将结合使用 drawRoundRect()
和 drawRect()
来实现所需的结果。首先,所有三个部分都将使用 drawRoundRect()
绘制。然后,将使用 drawRect()
返回并使那些“不”应该圆角的角变方。
那么,给定部分需要多大呢?好吧,如果一个值是整体的某个百分比,那么一个部分将是控件宽度的相同百分比。例如,如果三个值是 45、360 和 120,则第一个部分的宽度将是 45/525,即控件宽度的 8.5%;第二个部分的宽度将是 360/525,即控件宽度的 68.5%;第三个部分的宽度将是 120/525,即控件宽度的 22.8%。如果控件的宽度是 464,则三个部分的宽度分别为 39.7、318.1 和 106。
drawRoundRect()
需要知道三件事:矩形的边界、角的半径(我使用了 11 的值)以及要使用的画笔类型。所有三个部分的代码基本相同,只是矩形的左右两侧不同。由于这些部分是从左到右“堆叠”的,因此每个连续部分的左侧值与前一个部分的右侧值相同。这样它们就可以紧密地挨在一起。在计算一个部分的右侧值时,它是其左侧值加上它占整体的百分比。使用上面的数字,第一个部分的左右边界是 0.0 和 39.7;第二个部分的边界是 39.7 和 357.9;第三个部分的边界是 357.9 和 464。onDraw()
方法初步看起来像
protected void onDraw(Canvas canvas) { super.onDraw(canvas); int nWidth = getWidth(); int nHeight = getHeight(); float left = 0.0f; float top = 0.0f; float right = 0.0f; float bottom = nHeight; // low value left = 0.0f; float low = (float) m_nLowCount / m_nTotalCount; right = left + (nWidth * low); rect.set(left, top, right, bottom); canvas.drawRoundRect(rect, m_radius, m_radius, paintLow); // normal value left = right; float normal = (float) m_nNormalCount / m_nTotalCount; right = left + (nWidth * normal); rect.set(left, top, right, bottom); canvas.drawRoundRect(rect, m_radius, m_radius, paintNormal); // high value left = right; float high = (float) m_nHighCount / m_nTotalCount; // unused because the remainder of the bar is the 'high' value right = nWidth; rect.set(left, top, right, bottom); canvas.drawRoundRect(rect, m_radius, m_radius, paintHigh); }
这会产生类似
三个部分的分布看起来是正确的。我接下来需要做的是使内部角变方,以便条形看起来像一个整体。我花了一段时间寻找解决这个“问题”的方案,但没有找到。也许有一种更符合 Android 方式的方法,但我最终做的是调用几次 drawRect()
,使用非常小的矩形,刚好足以覆盖半径。下图很好地总结了这一点
由于两种形状都填充了相同的颜色,最终结果是一个左侧圆角而右侧直角的矩形。因此,要使第一个部分的右角变方,我在 onDraw()
方法中添加了以下代码
canvas.drawRect(rect.right - m_radius, rect.top, rect.right, rect.top + m_radius, paintLow); canvas.drawRect(rect.right - m_radius, rect.bottom - m_radius, rect.right, rect.bottom, paintLow);
如果控件的高度为 45,则第一次调用绘制的矩形左、上、右、下边界分别为 28.7、0、39.7 和 11。第二次调用绘制的矩形边界分别为 28.7、34、39.7 和 45。由于我向所有三个部分添加了大致相同的代码,所以我最终将其放入一个单独的函数中,该函数在每次 drawRoundRect()
调用后被调用。此函数中的额外 if()
条件用于处理一个或多个部分缺失的情况。例如,如果只有一个部分,其左右边界将等于 0
和 width
,因此不需要将其角变方。
private void squareCorners( Paint paint, Canvas canvas, int width ) { if (rect.left == 0.0f) { if (rect.right != (float) width) { // square the right side of this rectangle canvas.drawRect(rect.right - m_radius, rect.top, rect.right, rect.top + m_radius, paint); canvas.drawRect(rect.right - m_radius, rect.bottom - m_radius, rect.right, rect.bottom, paint); } } else { // square the left side of this rectangle canvas.drawRect(rect.left, rect.top, rect.left + m_radius, rect.top + m_radius, paint); canvas.drawRect(rect.left, rect.bottom - m_radius, rect.left + m_radius, rect.bottom, paint); } if (rect.right == (float) width) { if (rect.left != 0.0f) { // square the left side of this rectangle canvas.drawRect(rect.left, rect.top, rect.left + m_radius, rect.top + m_radius, paint); canvas.drawRect(rect.left, rect.bottom - m_radius, rect.left + m_radius, rect.bottom, paint); } } else { // square the right side of this rectangle canvas.drawRect(rect.right - m_radius, rect.top, rect.right, rect.top + m_radius, paint); canvas.drawRect(rect.right - m_radius, rect.bottom - m_radius, rect.right, rect.bottom, paint); } }
这会产生类似
太棒了!现在剩下的就是在每个部分上绘制其值。执行此操作的方法当然是 drawText()
。但是需要使用哪些坐标呢?在创建用于绘制文本的 Paint
对象时,它有一个 setTextAlign(Align.CENTER)
方法,其中“文本在 x
和 y
坐标上水平居中绘制”。这意味着在调用 drawText()
时,第二个参数将是控件宽度的(水平)中心。如果 Align.CENTER
也处理垂直居中就好了。由于 drawText()
将第三个参数解释为垂直起始点,因此我们需要按如下方式计算它
float cy = (rectArea.height() + rectText.height()) / 2.0f;
当我开始尝试低、正常和高的不同值时,我注意到如果一个值占总数的百分比足够小,它的部分就会太小而无法正确显示其值。所以我“添加”了一点“填充”到文本边界的每一侧,以确保它能够容纳。绘制居中文本的函数如下所示
private void drawCenteredText( Canvas canvas, Paint paint, String strText, RectF rectArea ) { // get the text boundaries Rect rectText = new Rect(); textPaint.getTextBounds(strText, 0, strText.length(), rectText); // need enough room for text plus 'padding' pixels on each side if ((padding + rectText.width() + padding) < rectArea.width()) { // draw text centered horizontally and vertically float cx = (rectArea.left + rectArea.right) / 2.0f; float cy = (rectArea.height() + rectText.height()) / 2.0f; canvas.drawText(strText, cx, cy, textPaint); } }
此函数在每次调用 squareCorners()
后被调用。分布条现在看起来像
太棒了!我用各种值组合测试了它,尤其是那些在有效范围边缘的值(例如,[0,1,0]、[0,1,1]、[1,1,1]、[1,300,25])。该控件在纵向和横向方向上都显示正常。对于其中一个或多个值缺失的情况,控件通过简单地在绘制中省略该部分来响应,例如
由于低值现在占总数的 27.2%,因此其部分的宽度为 126.5。同样,高值现在占总数的 72.7%,其部分的宽度为 337.4。对于部分不够宽而无法容纳其文本值的情况,文本将被简单地省略,例如
像使用其他库一样使用该库。有关详细信息,请参见此处。在将控件添加到布局文件时,请务必使用完整的包名。
关注点
可能取决于您的显示器分辨率才能注意到,但我希望这些部分具有渐变配色方案,其中内部颜色较深,外部颜色稍浅。为此,我为三个部分的每个着色器创建了一个 LinearGradient
对象。构造函数的第五个参数是颜色数组。第六个参数是您希望这些颜色如何分布的渐变线。由于我希望均匀分布(从内到外),我只使用了 null
。不希望在 onDraw()
方法中创建新对象,但仍需要能够响应屏幕尺寸/方向变化,我在 onSizeChanged()
方法中更新了 LinearGradient
对象
protected void onSizeChanged(int w, int h, int oldw, int oldh) { Resources r = getResources(); // light on top and bottom, dark in the middle int[] blue = new int[]{r.getColor(R.color.ltblue), r.getColor(R.color.dkblue), r.getColor(R.color.ltblue)}; int[] green = new int[]{r.getColor(R.color.ltgreen), r.getColor(R.color.dkgreen), r.getColor(R.color.ltgreen)}; int[] red = new int[]{r.getColor(R.color.ltred), r.getColor(R.color.dkred), r.getColor(R.color.ltred)}; gradientLow = new LinearGradient(0, 0, 0, getHeight(), blue, null, TileMode.CLAMP); paintLow.setShader(gradientLow); gradientNormal = new LinearGradient(0, 0, 0, getHeight(), green, null, TileMode.CLAMP); paintNormal.setShader(gradientNormal); gradientHigh = new LinearGradient(0, 0, 0, getHeight(), red, null, TileMode.CLAMP); paintHigh.setShader(gradientHigh); }
我想要一个高于正常大小的字体,我的第一直觉是调用 setTextSize()
,参数是我通常在布局文件中使用的值,即 16 到 22。当我开始插入这些值时,文本在模拟器上看起来不错,但在真实设备上会太小。如果我使用一个值使真实设备看起来正确(例如,55 左右),则在模拟器上绘制的文本会非常大。我解决这个问题的方法是在 dimens.xml
文件中放置一个值,然后让框架为我处理缩放。然后只需使用该资源值调用 setTextSize()
,例如
textPaint.setTextSize(getResources().getDimensionPixelOffset(R.dimen.text_medium));
现在,字体大小在不同设备上看起来都正确了。我希望添加的另一个功能,但为了及时发表本文而没有添加,是在每个部分上方或下方有一个“气泡”,显示该部分所占的百分比。
尽情享用!