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

揭示数字

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.18/5 (7投票s)

2016年9月14日

CPOL

7分钟阅读

viewsIcon

10216

使用多种方法揭示隐藏图像。

引言

微波炉已经存在几十年了,虽然无处不在,但它大多不被注意。然而,在过去的几十年里,我工作场所的微波炉在我等待食物“加热”的过程中给我带来了某种程度的乐趣。

我等待时玩的游戏涉及从各种角度查看数字时钟,同时它进行倒计时。你从近处开始,然后慢慢向后移动,显示更多的时钟。目标是看看我能看到多少数字,并且仍然能够猜出剩余时间。角度越锐利,显示的数字越少,因此猜测难度越大。一个基于这个概念的游戏应用正在酝酿中,但细节仍然模糊不清。

当我开始构建这个应用程序时,我需要克服的第一个障碍是显示一个数字。两个想法立即浮现在脑海中:基于计时器显示和基于区域显示。每次显示都会暴露出数字的更多部分。

使用计时器

为了基于计时器显示数字,我求助于 ValueAnimator 类。虽然这个类不直接进行任何绘图,但它确实计算动画值并将它们分配给你的对象,在我的例子中是一个自定义的 TextView。换句话说,你告诉这个类起始值和结束值(TextView 的高度,以像素为单位),以及你希望它需要多长时间才能到达那里(例如,10 秒),并且动画器的每次脉冲都会通过侦听器函数与你通信。动画器的繁重工作由插值器处理,插值器计算动画的经过分数,并确定动画是以线性还是非线性运动运行,例如加速和减速。因为我希望数字以平滑或线性的方式显示,所以我选择了 LinearInterpolator,其变化率是恒定的。还存在其他插值器,其变化率在整个动画过程中有所不同(例如,开始慢但结束快,开始和结束快但中间慢)。

当动画开始时,我像这样创建 ValueAnimator 对象

va = new ValueAnimator();
va.setFloatValues(0, m_rect.height());
va.setDuration(m_nTotalSeconds * 1000);
va.setInterpolator(new LinearInterpolator());
va.addUpdateListener(this);
va.addListener(this);
va.start();

在监听器函数中,您可以获取动画器已完成的比例。该比例将在 0.0 到 1.0 之间。有了这个值,您就可以在 onDraw() 函数中绘制相应数量的数字。这个函数中的“魔法”是通过将数字的 TextView 画布与一个临时 Rect 对象相交而发生的,该对象的边会根据您希望显示的方向而改变。最初,这个 Rect 对象与数字的 TextView 画布大小相同,因此它完全隐藏。随着每次连续的脉冲和对监听器函数的调用,比例接近 1.0,从而显示出更多数字。其代码如下:

@Override
public void onAnimationUpdate(ValueAnimator animation) 
{
    invalidate();
}

@Override
protected void onDraw(final Canvas canvas) 
{		
    if (va.isRunning())
    {
        // how much to change the side by
        int alpha = Math.round(m_rect.height() * va.getAnimatedFraction());
			
        Rect rect = new Rect(m_rect);
						
        if (m_strDirection.equals("Bottom"))
            rect.top = rect.bottom - alpha; // reveal from bottom to top
        else if (m_strDirection.equals("Top"))
            rect.bottom = rect.top + alpha; // reveal from top to bottom
        else if (m_strDirection.equals("Right"))
            rect.left = rect.right - alpha; // reveal from right to left
        else if (m_strDirection.equals("Left"))
            rect.right = rect.left + alpha; // reveal from left to right
			
        // over time, rect gets smaller and smaller
        canvas.clipRect(rect);						
    }
		
    super.onDraw(canvas);
}

您会注意到 Rect 操作周围的 if() 条件。如果数字需要完全显示,只需取消动画器即可。这将导致 isRunning() 方法返回 false,从而对基类 onDraw() 的调用将绘制整个数字。

当动画器达到结束值,从而时间值也已达到时,数字将完全显示,并且会调用 onAnimationEnd(),在那里可以移除动画器的监听器。这很重要,因为当动画器本身结束时,您不希望这些监听器在后台收到通知。这看起来像

@Override
public void onAnimationEnd(Animator animation) 
{
    animation.removeAllListeners();
}

一旦动画器开始显示,它看起来像这样

使用分区

要根据分区(例如,当点击“下一步”按钮时)显示数字,只需将动画器对象替换为计数器,以跟踪要显示的分区数量。每次计数器递增时,TextView 都会失效,以便使用下一个显示的分区重新绘制。启动动画的代码如下所示

public void start()
{
    m_nSectionsRevealed = 0;
    invalidate();
}

onDraw() 函数与前一个函数完全相同,只是 alpha 变量的计算方式不同。代码如下:

m_dSectionPercent = 1.0 / (double) m_nTotalSections; // how much each section is worth
m_dSectionPercent *= m_rect.height();                // in relation to the height of the TextView

@Override
protected void onDraw(final Canvas canvas) 
{		
    int alpha = (int) Math.round(m_nSectionsRevealed * m_dSectionPercent);
    ...
}

当点击按钮显示下一部分时,只需增加计数器并强制重绘,就像这样

public void revealNextSection()
{
    m_nSectionsRevealed = Math.min(m_nSectionsRevealed + 1, m_nTotalSections);
    invalidate();
}

如果图像需要完全显示,只需将 m_nSectionsRevealed 设置为等于 m_nTotalSections

当所有这些组合在一起时,显示效果如下

倒计时

在我解决了这两种显示方法之后,我开始思考我踏上这段旅程的最初原因,并觉得缺少了什么。虽然每种显示方法都具有挑战性,通过改变显示时间和使用的部分数量,但它还需要一些别的东西。我回到了源头。微波炉和猜测它的当前数字(你只有一秒钟的时间在它倒计时到下一个数字之前完成),要求你知道以前的数字。换句话说,在你不知道前一个数字是 7 还是 9 之前,你不知道当前数字是 6 还是 8。在前一种显示方法中,数字保持不变;你必须在计时器到期或所有部分都显示之前猜出数字。

我突然想到,我需要将前两种方法的部分组合成第三种显示方法:Countdown(与微波炉的工作方式非常相似)。这种方法将从一个数字开始,然后从那里倒计时。随着计时器的每一次脉冲,TextView 中的数字不会显示更多相同的数字,而是会递减一。此外,每次点击“下一步”按钮时,会绘制更多数字。因此,例如,如果起始数字是 8,并显示了一个部分,一秒钟后,数字将变为 7,仍然只显示一个部分。再过一秒钟,数字将变为 6,仍然只显示一个部分。这将一直持续到 0。如果在倒计时过程中,您点击了“下一步”按钮,那么随后的每个数字都将显示两个部分。

下图显示了最多三个部分被揭示。您会注意到,时钟上剩余时间越少,以及揭示的部分越多,该轮可能的得分就越低。

Using the Code

有了三个 TextView 重写,现在只是把它们放在适当的位置。在布局文件中,只需将 TextView 替换为自定义视图的名称即可,但我也喜欢提供完全限定的包,例如

<com.dcrow.RevealNumber.SectionedTextView
    android:id="@+id/tvNumber" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" />

这样,对我来说就很明显,它不仅仅是一个普通的 TextView 小部件。个人喜好。

我希望字体类似于微波炉上的七段显示器(SSD),所以我找到了一个合适的字体文件并将其放置如下

Typeface font = Typeface.createFromAsset(getActivity().getAssets(), "digital-7mi.ttf");
m_tvNumber = (TimedTextView) v.findViewById(R.id.tvNumber);
m_tvNumber.setTypeface(font);

我尝试的最初几个字体不起作用,因为它们将每个数字居中。这导致数字1使用的段与其余九个数字使用的段位于不同的位置。我最终找到一个“右对齐”的字体。

为了让自定义的 TextView 能够与片段(fragment)进行进度或完成情况的通信,每个 TextView 都定义了一个接口,片段必须实现该接口。该接口的定义如下:

public interface Callback
{
    public void onRevealUpdate( double fraction );
    public void onRevealDone();
}

每个片段对 fraction 值处理方式略有不同,但无论如何它所代表的意义是相同的:数字已显示了多少。例如,在使用区域显示的情况下,实现代码可能看起来像这样:

public class SectionedFragment extends Fragment implements SectionedTextView.Callback
{
    ...

    // @Override
    public void onRevealUpdate( double fraction ) 
    {
        String strSections = String.format(Locale.getDefault(), "%d", 
                             m_nTotalSections - m_tvNumber.getSectionsRevealed());
        m_tvSections.setText(strSections);
	
        double dPointsDeducted = Level.getPoints() * fraction;
        String strScore = String.format(Locale.getDefault(), 
                          "%.0f", Level.getPoints() - dPointsDeducted);
        m_tvScore.setText(strScore);
    }

    //================================================================

    // @Override
    public void onRevealDone() 
    {
        // nothing special to do here
    }
}

本文随附的所有源代码和资源即将发布。我应该很快就能把所有东西清理干净。

增强功能

由于大多数现代智能手机都配备了加速度计,感兴趣的读者可以更进一步,读取加速度计的 X(俯仰)值来确定您观看显示屏的角度。当您向前倾斜设备时,角度会使得数字可见部分减少。当您向后倾斜设备时,角度会使得数字可见部分增加。

尽情享用!

© . All rights reserved.