EPG basics and animation setup

We'll start by creating a class-extending view. On its onDraw() method we will draw the following parts:

We'll also need to trigger a redraw cycle if we are animating some variables.

So, let's start with this implementation of the onDraw() method, and let's proceed method by method:

@Override 
protected void onDraw(Canvas canvas) { 
   animateLogic(); 
 
   long currentTime = System.currentTimeMillis(); 
 
   drawBackground(canvas); 
   drawEPGBody(canvas, currentTime, frScrollY); 
   drawTimeBar(canvas, currentTime); 
   drawCurrentTime(canvas, currentTime); 
 
   if (missingAnimations()) invalidate(); 
} 
 

The easiest method to implement will be drawBackground():

private static final int BACKGROUND_COLOR = 0xFF333333; 
private void drawBackground(Canvas canvas) { 
    canvas.drawARGB(BACKGROUND_COLOR >> 24,  
            (BACKGROUND_COLOR >> 16) & 0xff, 
            (BACKGROUND_COLOR >> 8) & 0xff,  
            BACKGROUND_COLOR & 0xff); 
} 

In this case, we have defined a background color as 0xFF333333, which is some kind of dark gray, and we are just filling the whole screen with the drawARGB() call, masking and shifting the color components.

Now, let's go for the drawTimeBar() method:

private void drawTimeBar(Canvas canvas, long currentTime) { 
    calendar.setTimeInMillis(initialTimeValue - 120 * 60 * 1000); 
    calendar.set(Calendar.MINUTE, 0); 
    calendar.set(Calendar.SECOND, 0); 
    calendar.set(Calendar.MILLISECOND, 0); 
 
    long time = calendar.getTimeInMillis(); 
    float x = getTimeHorizontalPosition(time) - frScrollX + getWidth()
/ 4.f; while (x < getWidth()) { if (x > 0) { canvas.drawLine(x, 0, x, timebarHeight, paintTimeBar); } if (x + timeBarTextBoundaries.width() > 0) { SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm", Locale.US); String date = dateFormatter.format(new Date(time)); canvas.drawText(date, x + programMargin, (timebarHeight - timeBarTextBoundaries.height()) /
2.f + timeBarTextBoundaries.height(),paintTimeBar); } time += 30 * 60 * 1000; x = getTimeHorizontalPosition(time) - frScrollX + getWidth() /
4.f; } canvas.drawLine(0, timebarHeight, getWidth(), timebarHeight, paintTimeBar); }

Let's explain what this method is doing:

  1. First, we got the initial time at which we'd like to start drawing the time marks:
calendar.setTimeInMillis(initialTimeValue - 120 * 60 * 1000); 
calendar.set(Calendar.MINUTE, 0); 
calendar.set(Calendar.SECOND, 0); 
calendar.set(Calendar.MILLISECOND, 0); 
 
long time = calendar.getTimeInMillis();  

We defined the initialTimeValue in our class constructor as half an hour to the current time. We also removed the minutes, seconds, and milliseconds as we'd like to indicate the exact hours and the exact half hour past each hour, for instance: 9.00, 9.30, 10.00, 10.30, and so on in this example.

Then we created a helper method to get the screen position based on a timestamp that will be used in many other places in the code:

private float getTimeHorizontalPosition(long ts) { 
    long timeDifference = (ts - initialTimeValue); 
    return timeDifference * timeScale; 
} 
  1. In addition, we need to calculate a timescale based on the device screen density. To calculate it, we defined a default timescale:
private static final float DEFAULT_TIME_SCALE = 0.0001f;  
  1. In the class constructor, we adjusted the timescale depending on the screen density:
final float screenDensity = getResources().getDisplayMetrics().density; 
timeScale = DEFAULT_TIME_SCALE * screenDensity;  

We know there are many Android devices with different screen sizes and densities. Doing it this way, instead of hardcoding the pixel dimensions, makes the rendering as close as possible on all devices.

With the help of this method, we can easily loop on blocks of half an hour until we reach the end of the screen:

float x = getTimeHorizontalPosition(time) - frScrollX + getWidth() / 4.f; 
while (x < getWidth()) { 
 
    ... 
     
    time += 30 * 60 * 1000; // 30 minutes 
    x = getTimeHorizontalPosition(time) - frScrollX + getWidth() / 4.f; 
} 
 

By adding 30 minutes, converted to milliseconds, to the time variable we increment the horizontal marks in blocks of 30 minutes.

We've taken into consideration the frScrollX position as well. This variable will be updated when we add interactions that allow us to scroll, but we will see that later in this chapter.

The rendering is quite straightforward: we draw a vertical line as long as the x coordinate is inside the screen:

if (x > 0) { 
    canvas.drawLine(x, 0, x, timebarHeight, paintTimeBar); 
} 
 

we draw the time in HH:mm format, just next to it:

SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm", Locale.US); 
String date = dateFormatter.format(new Date(time)); 
canvas.drawText(date, 
        x + programMargin, 
        (timebarHeight - timeBarTextBoundaries.height()) / 2.f 
                + timeBarTextBoundaries.height(), paintTimeBar); 
 

One small performance improvement we can do is to store the strings so we don't have to call the format method again and again, and avoid costly object creation. We can do so by creating a HashMap that takes a long variable as a key and returns a string:

String date = null; 
if (dateFormatted.containsKey(time)) { 
    date = dateFormatted.get(time); 
} else { 
    date = dateFormatter.format(new Date(time)); 
    dateFormatted.put(time, date); 
} 
 

We use the formatted date if we already have it, or format it first and store it on the HashMap if it's the first time.

We can now go on to draw the current time indicator. It is quite easy; it's just a vertical box that is slightly wider than a single line, so we use drawRect() instead of drawLine():

private void drawCurrentTime(Canvas canvas, long currentTime) { 
    float currentTimePos = frChNameWidth +
getTimeHorizontalPosition(currentTime) - frScrollX; canvas.drawRect(currentTimePos - programMargin/2, 0, currentTimePos + programMargin/2, timebarHeight, paintCurrentTime); canvas.clipRect(frChNameWidth, 0, getWidth(), getHeight()); canvas.drawRect(currentTimePos - programMargin/2, timebarHeight, currentTimePos + programMargin/2, getHeight(), paintCurrentTime); }

As we already have the getTimeHorizontalPosition method, we can easily pinpoint where to draw the current time indicator. As we will be scrolling through the TV programs, we split the drawing into two parts: one that draws a line over the time bar, without any clipping; and another line from the end of the time bar to the bottom of the screen. In the latter we apply a clipping to only draw it on top of the TV programs.

To understand this more clearly, let's take a look at a screenshot of the result:

At the left side, we have got the icons representing the channels, on the top side is the time bar, and the rest is the body of the EPG with different TV programs. We'd like to avoid the current timeline, in red, going over the channel icons, so we apply the clipping we have just mentioned.

Finally, we can implement the drawing of the whole EPG body. It's a bit more complex than the other methods, so let's go through it step by step. First, we need to calculate the number of channels we have to draw to avoid doing unnecessary calculations and trying to draw outside the screen:

int startChannel = (int) (frScrollY / channelHeight); 
verticalOffset -= startChannel * channelHeight; 
int endChannel = startChannel + (int) ((getHeight() -  timebarHeight) / channelHeight) + 1; 
if (endChannel >= channelList.length) endChannel = channelList.length - 1; 

Like we did with the timescale, we also define a default channel height and compute it based on the screen density:

private static final int CHANNEL_HEIGHT = 80; 
... 
channelHeight = CHANNEL_HEIGHT * screenDensity; 
 

Now that we know the initial channel and the end channel we need to draw, we can outline the drawing loop:

canvas.save(); 
canvas.clipRect(0, timebarHeight, getWidth(), getHeight()); 
 
for (int i = startChannel; i <= endChannel; i++) { 
    float channelTop = (i - startChannel) * channelHeight -
verticalOffset +
timebarHeight; float channelBottom = channelTop + channelHeight; ... } canvas.drawLine(frChNameWidth, timebarHeight, frChNameWidth, getHeight(), paintChannelText); canvas.restore();

We'll be modifying the canvas clipping several times, so let's save it at the beginning of the method and restore it at the end. This way we won't impact any other drawing method completed after this. Inside the loop, for each channel, we also calculate the channelTop and channelBottom values as they'll be handy later when drawing it. These values indicate the vertical coordinates for the top and the bottom of the channel we are drawing.

Let's now draw the icon for each channel, requesting it first from the internet if we don't have it. We'll be using Picasso to manage the Internet requests, but we can use any other library:

if (channelList[i].getIcon() != null) { 
    float iconMargin = (channelHeight -
channelList[i].getIcon().getHeight()) / 2;
canvas.drawBitmap(channelList[i].getIcon(), iconMargin, channelTop
+ iconMargin, null); } else { if (channelTargets[i] == null) { channelTargets[i] = new ChannelIconTarget(channelList[i]); } Picasso.with(context) .load(channelList[i] .getIconUrl()) .into(channelTargets[i]); }

There is information about Picasso at:
http://square.github.io/picasso/.

Also, for each channel, we need to draw the TV programs that are inside the screen. Once again, let's use the method we previously created to convert a timestamp into a screen coordinate:

for (int j = 0; j < programs.size(); j++) { 
    Program program = programs.get(j); 
 
    long st = program.getStartTime(); 
    long et = program.getEndTime(); 
 
    float programStartX = getTimeHorizontalPosition(st); 
    float programEndX = getTimeHorizontalPosition(et); 
 
    if (programStartX - frScrollX > getWidth()) break; 
    if (programEndX - frScrollX >= 0) { 
 
        ... 
         
    } 
} 

Here, we are getting the program start and end positions from the program start and end times. If the start position is beyond the width of the screen, we can stop checking more TV programs as they'll all be outside the screen, assuming the TV programs are sorted by time in ascending order. Also, if the end position is less than 0, we can skip this specific TV program as it'll also be drawn outside the screen.

The actual drawing is quite simple; we are using a drawRoundRect for the TV program background and we are drawing the program name centered on it. We're also clipping the area just in case the name is longer than the TV program box:

canvas.drawRoundRect(horizontalOffset + programMargin + programStartX, 
       channelTop + programMargin, 
       horizontalOffset - programMargin + programEndX, 
       channelBottom - programMargin, 
       programMargin, 
       programMargin, 
       paintProgram); 
 
canvas.save(); 
canvas.clipRect(horizontalOffset + programMargin * 2 + programStartX, 
       channelTop + programMargin, 
       horizontalOffset - programMargin * 2 + programEndX, 
       channelBottom - programMargin); 
 
paintProgramText.getTextBounds(program.getName(), 0, program.getName().length(), textBoundaries); 
float textPosition = channelTop + textBoundaries.height() + ((channelHeight - programMargin * 2) - textBoundaries.height()) / 2; 
canvas.drawText(program.getName(), 
           horizontalOffset + programMargin * 2 + programStartX, 
           textPosition, 
           paintProgramText); 
canvas.restore(); 
 

We've also added a small check to see if a TV program is currently playing. If the current time is greater than or equal to the program start time and smaller than its end time, we can conclude that the TV program is currently playing and render it with the highlighted color.

if (st <= currentTime && et > currentTime) { 
    paintProgram.setColor(HIGHLIGHTED_PROGRAM_COLOR); 
    paintProgramText.setColor(Color.BLACK); 
} else { 
    paintProgram.setColor(PROGRAM_COLOR); 
    paintProgramText.setColor(Color.WHITE); 
} 
 

Let's now add the animation cycle. For this example, we have chosen the fixed time-step mechanism. We'll only animate the scroll variables, both horizontal and vertical, and the movement of the channel part of the screen:

private void animateLogic() { 
    long currentTime = SystemClock.elapsedRealtime(); 
    accTime += currentTime - timeStart; 
    timeStart = currentTime; 
 
    while (accTime > TIME_THRESHOLD) { 
        scrollX += (scrollXTarget - scrollX) / 4.f; 
        scrollY += (scrollYTarget - scrollY) / 4.f; 
        chNameWidth += (chNameWidthTarget - chNameWidth) / 4.f; 
        accTime -= TIME_THRESHOLD; 
    } 
 
    float factor = ((float) accTime) / TIME_THRESHOLD; 
    float nextScrollX = scrollX + (scrollXTarget - scrollX) / 4.f; 
    float nextScrollY = scrollY + (scrollYTarget - scrollY) / 4.f; 
    float nextChNameWidth = chNameWidth + (chNameWidthTarget -
chNameWidth) / 4.f; frScrollX = scrollX * (1.f - factor) + nextScrollX * factor; frScrollY = scrollY * (1.f - factor) + nextScrollY * factor; frChNameWidth = chNameWidth * (1.f - factor) + nextChNameWidth *
factor; }

In our renderings and calculations later, we will use the frScrollX, frScrollY, and frChNameWidth variables, which contain the fractional parts between the current logic tick and the following one.

We'll see how to scroll in the next section when talking about adding interaction to the EPG, but we have just introduced the movement of the channel part. Right now, we are only rendering each channel as an icon, but, to have more information, we have added a toggle that makes the channel box, where we currently have the icon, become larger and draw the channel title next to the icon.

We've created a Boolean switch to track which state we are rendering and to draw the channel name if required:

if (!shortChannelMode) { 
    paintChannelText.getTextBounds(channelList[i].getName(), 
            0, 
            channelList[i].getName().length(), 
            textBoundaries); 
 
    canvas.drawText(channelList[i].getName(), 
            channelHeight - programMargin * 2, 
            (channelHeight - textBoundaries.height()) / 2 +
textBoundaries.height() + channelTop, paintChannelText); }

The toggle is quite simple, as it just changes the channel box width target to channelHeight, so it'll have square dimensions, or two times the channelHeight when drawing the text. The animation cycle will take care of animating the variable:

if (shortChannelMode) { 
    chNameWidthTarget = channelHeight * 2; 
    shortChannelMode = false; 
} else { 
    chNameWidthTarget = channelHeight; 
    shortChannelMode = true; 
}