Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Reveal Number

4.18/5 (7 votes)
14 Sep 2016CPOL7 min read 9.8K  
Revealing a hidden image using multiple methods

Introduction

The microwave oven has been around for decades, and while ubiquitous, it mostly goes unnoticed. For the past umpteen years, however, the microwave oven at my place of employment has provided me with a certain level of entertainment while waiting on my food to become "agitated."

The "game" that I play while waiting involves looking at the digital clock at various angles while it counts down. You start off close and slowly work your way backward revealing more of the clock. The goal is to see how little of the digit(s) I can see and still be able to guess the time remaining. The sharper the angle, the less of the digit is shown and thus more difficult to guess. A game app based on this concept was brewing, but the details were still fuzzy.

When I started putting the app together, the first obstacle I needed to get over was that of revealing a number. Two ideas immediately came to mind: reveal based on a timer, and reveal based on sections. Each reveal would expose more of the number.

Using a Timer

To reveal the number based on a timer, I turned to the ValueAnimator class. While this class doesn't directly do any drawing, what it does do is calculate animation values and assign them to your object, a custom TextView in my case. In other words, you tell the class the starting and ending values (the height of the TextView, in pixels), and how long you want it to take in getting there (e.g., 10 seconds), and each pulse of the animator will communicate with you through a listener function. The heavy work of the animator is handled in the interpolator, which calculates the elapsed fraction of the animation, and determines whether the animation runs with linear or non-linear motion, such as acceleration and deceleration. Because I wanted the number to be revealed in a smooth, or linear, fashion, I opted for the LinearInterpolator whose rate of change is constant. Other interpolators exist where the rate of change differs throughout the animation (e.g., start slow but finish fast, start and end fast but slow in the middle).

When it's time for the animation to start, I create the ValueAnimator object like:

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();

In the listener function, you can get the animator's fraction of how much has been completed. That fraction will be somewhere between 0.0 and 1.0. With that value, you can then draw the appropriate amount of the number in the onDraw() function. The magic in this function happens as a result of intersecting the number's TextView canvas with a temporary Rect object whose sides are changed depending on which direction you want the reveal to happen. Initially, this Rect object is the same size as the number's TextView canvas so it is totally hidden. With each successive pulse and call to the listener function, the fraction approaches 1.0, thus revealing more of the number. The code for this looks like:

@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);
}

You'll note the if() condition surrounding the Rect manipulation. If the number needs to be fully revealed, it's simply a matter of cancelling the animator. This will cause the isRunning() method to return false and thus the call to the base onDraw() will draw the entire number.

When the animator has reached the ending value, and thus the time value has also been reached, the number will be fully revealed and onAnimationEnd() will be called where the animator's listeners can be removed. This is important as you don't want those listeners being notified in the background when the animator itself has ended. This looks like:

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

Once the animator starts its reveal, it looks like this:

Using Sections

To reveal the number based on sections, such as when a "next" button is clicked, the animator object is simply replaced with a counter to keep track of how many sections to show. Each time the counter is incremented, the TextView is invalidated so that it gets redrawn with the next section being displayed. The code to start the animation looks like:

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

The onDraw() function looks just like the previous one except for how the alpha variable is calculated. That looks like:

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);
    ...
}

When the button is clicked to reveal the next section, it's simply a matter of incrementing the counter and forcing a redraw, like:

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

If the image needs to be fully revealed, simply set m_nSectionsRevealed equal to m_nTotalSections.

When all of this is put together, the reveal looks like:

Counting Down

After I got these two methods of revealing all worked out, I started to think about my original reason for embarking on this journey and felt like something was missing. While each method of reveal was challenging, by changing the reveal time and the number of sections to use, it needed something else. I went back to the source. The microwave and the guessing of its current number, which you only have a second to do before it counts down to the next number, requires you to know the previous number(s). In other words, you don't know that the current number is a 6 or an 8 until you know if the previous number is a 7 or a 9. With the previous methods of reveal, the number stayed constant; you had until the timer expired or all of the sections were revealed to guess the number.

It occurred to me that I needed to combine pieces of the two previous methods into a third reveal method: Countdown (very similar to what a microwave is doing). This method would start with a number and count down from there. With each pulse of the timer, instead of more of the same number being revealed, the number in the TextView would decrement by one. Additionally, each time a "next" button is clicked, more of the number is drawn. So, for example, if the starting number was 8 with one section being revealed, one second later the number would change to 7, with still only one section being revealed. One more second later, the number would change to 6, with still only one section being revealed. This would continue down to 0. If during the countdown, you clicked the "next" button, two sections would be revealed for each subsequent number.

The following image shows up to three sections being revealed. You'll note that the less time remaining on the clock plus the more sections revealed will lower the possible score for that round.

Using the Code

With three TextView overrides, now it was just a matter of putting them in place. In the layout file, simply replacing TextView with the name of the custom view will suffice, but I also like providing the fully-qualified package, like:

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

This way, it stands out to me that it is not just an ordinary TextView widget. Personal preference.

I wanted the font to resemble the seven-segment display (SSD) on the microwave, so I found an appropriate font file and put it in place like:

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

The first couple of fonts I tried did not work because they centered each digit. This caused the segments used by the number 1 to be in a different spot than the segments used by the other nine numbers. I finally found one that was "right justified."

In order for the custom TextViews to communicate with the fragment about progress or completion, each one defines an interface that the fragment will be required to implement. The definition of this interface looks like:

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

What each fragment does with the fraction value differs slightly, but what it represents is the same regardless: how much of the number has been revealed. In the case of revealing using sections, for example, the implementation code would look something like:

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
    }
}

All of the source code and resources accompanying this article are forthcoming. I should have everything cleaned up shortly.

Enhancements

Since most modern smartphones have an accelerometer, interested readers could take this concept one step further and read the X (pitch) values from the accelerometer to determine the angle at which you are viewing the display. As you tilt the device forward, the angle is such that less of the number would be visible. As you tilt the device backward, the angle is such that more of the number would be visible.

Enjoy!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)