65.9K
CodeProject 正在变化。 阅读更多。
Home

分布条

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (7投票s)

2015年11月16日

CPOL

7分钟阅读

viewsIcon

11904

downloadIcon

277

将值显示为堆叠柱

引言

我正在开发一个个人应用程序,它通过蓝牙与 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() 条件用于处理一个或多个部分缺失的情况。例如,如果只有一个部分,其左右边界将等于 0width,因此不需要将其角变方。

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) 方法,其中“文本在 xy 坐标上水平居中绘制”。这意味着在调用 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));

现在,字体大小在不同设备上看起来都正确了。我希望添加的另一个功能,但为了及时发表本文而没有添加,是在每个部分上方或下方有一个“气泡”,显示该部分所占的百分比。

尽情享用!

© . All rights reserved.