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

条形时钟

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (22投票s)

2015年4月14日

CPOL

5分钟阅读

viewsIcon

40374

downloadIcon

901

将时间显示为一系列( 进度) 条。

引言

尽管它们已经存在很长时间了,但前几天我正在看一个模拟时钟,注意到一些我以前从未留意过的事情。在一个有三个指针(即时针、分针、秒针)的时钟上,当秒针绕着表盘转动时,分针会极其微小地移动,而不是从一分钟跳到下一分钟。同样,时针也是如此,尽管慢得多。所以,例如,如果秒针在 :30,分针就会在两分钟之间的一半位置。如果分针在 :15,时针就会在两小时之间 1/4 的位置。嗯……

背景

虽然这是我在 CodeProject 的第一篇 Android 文章,但这并不是我创建的第一个应用程序。我创建的应用程序数量大概有 40 多个,其中两个已经发布,第三个也差不多快要发布了。然而,这是我创建的第一个应用程序,其中我必须添加代码来“绘制”一些 UI 控件,而不是让框架为我完成绘图。因此,本文将简要演示如何扩展 ProgressBar 类。

使用代码

我希望我的进度条扩展能够显示其值作为文本,可以显示在进度条上或进度条下方。鉴于这个要求,新类的名称就变成了

public class TextProgressBar extends ProgressBar

在不覆盖任何内容的情况下,可以在布局文件中这样引用该类

<com.dcrow.BarClock.TextProgressBar android:id="@+id/progHour"
    android:layout_height="64dp" 
    android:layout_marginbottom="25dp" 
    android:layout_width="match_parent" 
    android:max="23">

此时,进度条将正常运行。由于我需要一些自定义,因此需要覆盖 onDraw() 方法。实际的绘图将在这里进行。调用超类 onDraw() 将负责绘制实际的进度条,而所有其他内容(例如刻度线、文本)都可以在之后绘制。

我首先想要的是一条水平线,位于进度条的中间,并贯穿进度条的整个长度

// draw horizontal line the width of the progress bar
int nMiddle = getHeight() / 2;
canvas.drawLine(0, nMiddle, getWidth(), nMiddle, m_paintLine);

接下来,我想要的是在先前绘制的水平线上有一系列垂直刻度线。根据进度条的最大值,刻度线的数量和它们之间的间隔会有所不同。

// what is the distance between each tick mark?		
float increment = (float) getWidth() / getMax();		
		
// draw the tick marks
for (int i = 0; i <= getMax(); i++)
{
    float x = i * increment;			
			
    canvas.drawLine(x, nMiddle - 20.0f, x, nMiddle + 20.0f, m_paintLine);
}

看起来不错,但 **分针** 和 **秒针** 进度条上的刻度线有点难以辨认。对于这两个,解决方案是使每 5 个刻度线变大。模拟时钟表盘也符合这一点,因为数字 1-12 相隔 5 秒。我们只需要检查循环控制变量 i 的值,看看它是否能被 5 整除,比如

if (i % 5 == 0)
    canvas.drawLine(x, nMiddle - 30.0f, x, nMiddle + 30.0f, m_paintLine);
else
    canvas.drawLine(x, nMiddle - 20.0f, x, nMiddle + 20.0f, m_paintLine);

现在刻度线已经就位,是时候处理文本了(即进度条的值)。我希望它在进度线上居中,并随着进度从左到右移动。在最左边和最右边,我不希望它消失。

// what percent are we done?
float fPercent = ((float) getProgress() / getMax()) * getWidth();

// get the width of the text to be drawn
m_paintText.getTextBounds(m_text, 0, m_text.length(), m_bounds);

float pos = fPercent;  // tentative X position
if (pos < m_bounds.width() / 2.0f)
{
    m_paintText.setTextAlign(Align.LEFT);
    pos = 0.0f;        // don't go past left edge
}
else if (pos > getWidth() - (m_bounds.width() / 2.0f))
{
    m_paintText.setTextAlign(Align.RIGHT);
    pos = getWidth();  // don't go past right edge
}
else
    m_paintText.setTextAlign(Align.CENTER);

// draw the 'percent' text up from the bottom edge
canvas.drawText(m_text, pos, getHeight() - 10.0f, m_paintText);

有了绘制文本的代码后,进度条现在看起来像

控件本身(大部分)完成后,我需要响应时间变化并相应地更新进度条。我设置了一个 1 秒间隔的计时器,当时间改变时,将执行以下代码

Calendar now = Calendar.getInstance();
int nHour    = now.get(Calendar.HOUR_OF_DAY);
int nMinute  = now.get(Calendar.MINUTE);
int nSecond  = now.get(Calendar.SECOND);
					
m_progHour.setProgress(nHour);

m_progMinute.setProgress(nMinute);
						
m_progSecond.setProgress(nSecond);

关注点

最初,主布局看起来与以下内容类似

<linearlayout android:layout_height="match_parent"
    android:layout_width="match_parent" 
    android:orientation="vertical" 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" > 

    <textview android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="@string/hour" 
        android:textsize="20sp">
        
    <com.dcrow.barclock.textprogressbar android:id="@+id/progHour" 
        android:layout_height="64dp" 
        android:layout_width="match_parent" 
        android:max="23">

    <textview android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="@string/minute" 
        android:textsize="20sp">
    
    <com.dcrow.barclock.textprogressbar android:id="@+id/progMinute" 
        android:layout_height="64dp" 
        android:layout_width="match_parent" 
        android:max="59">
    
    <textview android:layout_height="wrap_content" 
        android:layout_width="wrap_content" 
        android:text="@string/second" 
        android:textsize="20sp">
    
    <com.dcrow.barclock.textprogressbar android:id="@+id/progSecond" 
        android:layout_height="64dp" 
        android:layout_width="match_parent" 
        android:max="59">
    
</LinearLayout>

这按预期工作,但它没有给 **时针** 和 **分针** 进度条提供足够的粒度。每个条的进度作为一个整体单位前进,而不是作为一个整体的百分比。为了纠正这个问题,我只需要将每个条的 max 值分别更改为 86399(一天的 0 基秒数)和 3599(一小时的 0 基秒数)。现在,由于每个条都基于总秒数,我需要更改发送到每个条的 setProgress() 方法的值,如下所示

m_progHour.setProgress((nHour * 3600) + (nMinute * 60) + nSecond);

m_progMinute.setProgress((nMinute * 60) + nSecond);
						
m_progSecond.setProgress(nSecond);

最终结果

请注意,由于 **秒针** 进度条显示 32 秒,**分针** 进度条在两个分钟间隔之间大约处于 1/2 的位置。同样,由于 **分针** 进度条显示 49 分钟,**时针** 进度条在两个小时间隔之间略高于 3/4 的位置。这就是我想要创建的外观。

颜色

在创建此应用程序的过程中,我一部分时间在想,进度条及其背景的渐变颜色足以使其“不那么无聊”。我找到的第一个解决方案涉及在布局文件中使用进度条的 android:progressDrawable 属性。将其分配给可绘制文件夹中的以下文件即可完成此操作

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="3dp" />

            <gradient android:startColor="#f0f0f0"
                android:centerY="1.0"
                android:endColor="#b9b9b9"
                android:angle="270" />
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="3dp" />

                <gradient android:startColor="#93d1ea"
                    android:centerY="1.0"
                    android:endColor="#35aad7"
                    android:angle="270" />
            </shape>
        </clip>
    </item>
</layer-list>

有了这些颜色,进度条就有了蓝色渐变,背景则是灰色渐变,两者都从上到下。看了一会儿之后,我认为最好还是让颜色可自定义。于是我朝着那个方向努力……

我需要找到一种方法,在运行时获取这个可绘制对象,修改它,然后将其放回原位。在尝试了所有我能找到的方法几天之后,一无所获。无论我放什么代码,android:progressDrawable 属性总是优先。然后我决定放弃使用该属性,而是完全通过代码来完成。

从进度条的 setProgressDrawable() 方法向后追溯,我发现有几个可绘制对象需要创建并连接在一起才能使其工作。因为进度条实际上使用三个可绘制对象(即背景、次级进度、进度)相互堆叠来实现其效果,所以我需要向 setProgressDrawable() 发送一个 LayerDrawable 对象。该对象将包含其他可绘制对象。

用作背景的可绘制对象看起来像

GradientDrawable gdBackground = 
    new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {colors[0], colors[1]});
gdBackground.setShape(GradientDrawable.RECTANGLE);
gdBackground.setCornerRadius(7.0f);

用作次级进度和(主)进度可绘制对象看起来像

GradientDrawable gdProgress = 
    new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {colors[2], colors[3]});
gdProgress.setShape(GradientDrawable.RECTANGLE);
gdProgress.setCornerRadius(7.0f); // just enough round to take the sharpness off
ClipDrawable cdProgress = new ClipDrawable(gdProgress, Gravity.LEFT, ClipDrawable.HORIZONTAL);

将它们放入数组,并让 LayerDrawable 对象知道顺序看起来像

// since we do not have a secondary progress bar, use the same drawable for both
Drawable[] drawables = {gdBackground, cdProgress, cdProgress};
			
LayerDrawable ld = new LayerDrawable(drawables);
ld.setId(0, android.R.id.background);
ld.setId(1, android.R.id.secondaryProgress);
ld.setId(2, android.R.id.progress);
			
bar.setProgressDrawable(ld);

由于我需要为每个(四个)进度条执行此操作,因此我将其全部封装在一个属于活动的 changeColors(TextProgressBar bar, int[] colors) 方法中。我可以在活动的 onCreate() 方法中调用该方法,或者在更改颜色的其他地方(例如,在偏好设置中)调用。

尽情享用!

© . All rights reserved.