揭示数字






4.18/5 (7投票s)
使用多种方法揭示隐藏图像。
引言
微波炉已经存在几十年了,虽然无处不在,但它大多不被注意。然而,在过去的几十年里,我工作场所的微波炉在我等待食物“加热”的过程中给我带来了某种程度的乐趣。
我等待时玩的游戏涉及从各种角度查看数字时钟,同时它进行倒计时。你从近处开始,然后慢慢向后移动,显示更多的时钟。目标是看看我能看到多少数字,并且仍然能够猜出剩余时间。角度越锐利,显示的数字越少,因此猜测难度越大。一个基于这个概念的游戏应用正在酝酿中,但细节仍然模糊不清。
当我开始构建这个应用程序时,我需要克服的第一个障碍是显示一个数字。两个想法立即浮现在脑海中:基于计时器显示和基于区域显示。每次显示都会暴露出数字的更多部分。
使用计时器
为了基于计时器显示数字,我求助于 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(俯仰)值来确定您观看显示屏的角度。当您向前倾斜设备时,角度会使得数字可见部分减少。当您向后倾斜设备时,角度会使得数字可见部分增加。
尽情享用!