Introduction

This is an Android application that, I believe, contains just
slightly more than the minimum necessary feature set to be able to
view .cbz format files.

These features are:

A "list view"of the .cbz
files on the SD card, as shown in the above image. For each file:

Show a thumbnail of the first
page of the comic.

Show the name of the file.

Allow user to start viewing the
rest of the file.

A "viewer" to read the
.cbz file.

Viewer will show one page (or
part of page) at a time.

Fling gestures are used to move
to the next or previous page.

Double tap will zoom in on a part
of the page.

Pinch can be used to zoom in and
out.

When zoomed in, image can be
scrolled using drag gestures.

User can set a bookmark (Comic
book & page). When application is initially started, the
application will go to the bookmark.

A menu, to allow user to set the bookmark, return to the
bookmark, or go to a list view of .cbz files to select a different
comic to view.

In terms of Android features, this code demonstrates how to:

Enumerate files on a SD card

Read a zip file.

Read (and resize) a bitmap from a
file

Show a bitmap, with zoom, scroll,
pinch zoom, and fling functionality.

Handle the user changing the
screen orientation between landscape and portrait.

Use intents to pass data between
activities in an Android application.

Save user settings to persistent
storage, and retrieve them later.

Provide a menu.

Customize the layout of the items
of a ListActivity

Create a simple dialog.

Do work in on a background thread using AsyncTask

Warning, this is the second Android application I've written, and
my first Code Project article, so there are probably many things I've
done that could be improved. Feedback is welcomed.

Using the code

If you don't know how to set up Eclipse and the Android SDK, go
here
for instructions.

Download the project, unzip and import into Eclipse. Requires minimum of Android 2.3

Comic Book File Format

There are actually a number of formats for storing comic books.
The simplest (and easiest for us) is
.cbz.
It's a set of image files
(usually PNG or JPEG) that have been packed into a zip archive file.
Each image is a page of the comic.

CbzComic.java in this project handles decoding the contents of
.cbz files. From the preceding .cbz description, a .cbz archive file
can be thought of as an array of Bitmaps. So, the most important
functions of the CbzComic class are "Get Bitmap representing
page N" and "Get number of Bitmaps". These are
implemented by the functions getPage() and numPages() respectively.

Reading the contents of a Zip archive file.

Android provides two main classes to read a ZIP file,
ZipInputSteam, and ZipFile. ZipFile provides random read access to a
Zip file. As we want to be able to move both forward and backward,
and even jump to a specific page of the comic, this is the class we
want to use. (ZipInputStream only allows access to the contents of
the file in a serial fashion, not what we want.)

Using the ZipFile class is reasonably simple. Each file stored in
the archive has corresponding ZipEntry. To extract a file from the
archive, calling ZipFile.getInputStream() with the appropriate
ZipEntry will return the file as an InputStream.

There are two ways to get a ZipEntry. ZipFile.getEntry(String
entryName), will return the ZipEntry with the specific name, but
requires you to know the entryName in advance. The other way is ZipFile.entries(), which returns an enumeration that gives you all
entries.

As we wish to access the files in the zip archive in random order,
the simplest way to achieve this would be to use ZipFile.entries() to
get all the entries, and place them in an array. Then, to get the
file that represents page 'n' of the comic we'd simply get the
ZipEntry held in the 'n'th element of the array, and use this to get
the InputStream.

However, as Android is typically used in mobile devices that are
memory constrained, instead of storing the ZipEntries themselves in
an array, the CbzFile class stores the name of each entry in an
array. Then, when we want a particular page from the archive,
ZipFile.getEntry() is called with the name to obtain the appropriate
ZipEntry, which is then used to obtain the InputStream.

Once we have an InputStream, converting it into a bitmap is
trivial. The BitmapFactory class's decodeStream() function does this
for us.

Thus, the most interesting functions in CbzComic are the
constructor, which builds the array of ZipEntry names, and getPage(),
which does the index to entryName to ZipEntry mapping and InputStream
to Bitmap conversion. There is also getPageAsThumbnail(), which
shows how to get a page's bitmap that has been scaled down. So it
could be used, for example, as a thumbnail on a menu.

Viewing the pages of a Comic:

The viewing of a comic is between two classes; BitmapView.java,
and BitmapViewController.java.

The BitmapView is responsible for showing the page image and
responding to the user's zoom, pinch and scroll gestures to display
the appropriate part of the selected image.

The BitmapViewController is responsible for responding to the
users fling gestures, to change the currently selected page in the
BitmapView. The reason for this division of responsibility is so
that I could, in the future, easily reuse the BitmapView. e.g. If I
wanted to do a photo album browser, (or a web comic viewer) all that
would be needed is writing a new BitmapViewController that obtains
the correct bitmaps in response to fling gestures.

Linking a BitmapView and BitmapViewController together is done
by the following code.

The BitmapView is actually a very simple class. It derives from
view, and overrides onDraw()
to show the currently selected image (or
part thereof) to the user. Getting the View to react to scroll,
fling, and zoom and gestures is slightly complicated because the View
does not receive these gestures as events directly. Instead, its
onTouchEvent() is called with MotionEvents, and you need to analyse
these events to determine the gesture(s) the user is making.
However, you can use an android.view.GestureDetector to do this
analysis work for you. There are three steps involved.

First, create an anonymous class that derives from
android.view.GestureDetector.SimpleOnGestureListener.
This class has a set of methods that are called when the
GestureDetector determines a gesture occurs. e.g. onDoubleTap(),
onScroll(), etc. For each gesture you want to handle, you override that
function and implement the functionality to handle the gesture.

A minor complication is that the GestureDetector does not handle
"pinch to zoom", In order to do that, you need to use a
ScaleGestureDetector, and its matching
SimpleOnScaleGestureListener, in addition to the GestureDetector.

As previously mentioned, the BitmapView does not directly handle
converting flings to "turn the page" actions, this is done
by the BitmapViewController. However, as flings are detected by the
GestureDetector, when they occur, the BitmapView passes them onto the
BitmapViewController. There is (yet another) minor issue
in that we want the user to be able to do both scroll and fling
gestures and the GestureDetector sometimes interprets a small scroll
movement as a fling. Or adds a fling to the end of scroll movement.
So, to avoid a page turn when user is just doing a scroll, we check
that the fling exceeds threshold criteria for length and speed.
Note, the thresholds were determined by experimentation, and may not
be suitable for all users. Ideally, we'd provide settings, so that
each user can adjust the thresholds to a value that works best for
them.

Beyond setting up the GestureDetectors, most of the BitmapView
code is keeping track of the area of bitmap that should be shown on
screen, and maths to adjust the area in response to zoom and scroll
requests.

Viewing list of comic book files

ListComicsActivity.java provides the UI that allows a user to to
choose the comic to view. i.e. It provides this UI.

Thus, this class does three tasks:

Find the available .cbz files.

Show the found files to the user, in a way that allows the
user to select one of them

Return the selection to the main activity.

Finding the .cbz files is a cheat. As this is a minimal viewer, it
just lists all the files in the "Downloads" directory on
the SD card. This should really be done via a content provider. (A
possible future feature.) The code to get a list of the files is
isMediaAvailable() and listComicFiles(), which load mFileNames with a
list of the comic book files.

ListComicsActivity derives from ListActivity, and uses the
ListActivity to provide the UI. For the basics of how to use a
ListActivity see
this article.

The major additional points of interest in this class are using a
background thread to populate the thumbnail on the menu, and
returning the comic selected by the user to the MainActivity.

A background thread is used to load the thumbnail because this
operation could potentially take a long time, so should not be run on
the UI thread. This is implemented by the LoadThumbnailsTask class,
which derives from android.os.AsyncTask. AsyncTask is well covered by
this document by Google, so I won't discuss it further.

Returning the comic selected

The ListActivity is an activity, and we want it to return a
result. So, to get it to appear, it's launched from the main activity
by calling startActivityForResult().

To return information from ListComicsActivity, you create an
Intent, add the desired information to the intent, call setResult(),
and then call finish() to end ListComicsActivity and return to the
activity that launched ListComicsActivity.

When ListComicsActivity ends, onActivityResult() in the activity
that launched it is called, with the intent from setResult(). So, we
override onActivityResult() and extract the information from the intent.

Bookmark

The final feature of this application is the ability to set and
restore a bookmark. The most common scenario being, just before
shutting down the application, the user should be able to tell the
application to remember the currently displayed comic and page.
Later, when the application is restarted, it should return to the
comic and page. Bookmark.java
is responsible for saving/loading this
persistent information.

Note, if desired, the application could automatically record the
current position on shutdown by overriding MainActivity.onPause().
It could also store multiple bookmarks, one per comic. But to keep
things simple at this time, a bookmark is set by the user selecting
the "set bookmark" menu item.

As detailed by Google,
there are several ways of storing persistent information. The
simplest is Shared Preferences. Here's how the Bookmark saves
and loads the state information using Shared Preferences

In addition to storing state persistently, in order to handle the
screen orientation changing (i.e. going from landscape to portrait
and vice versa), we also need to be able to save the state to a
Bundle. This is because, when the device is rotated, Android expects
you to save the state to bundle. Android then restarts your app,
passing in the bundle, which your app uses to restore its state.

In slightly more detail, when the device is rotated,
onSaveInstanceState() in you activity is called. You need to
override this function and save any state you need persisted into the
supplied Bundle. In our case, the state information we want is the
comic and page currently being viewed.

After calling onSaveInstanceState, the OS will change the
orientation and restart your application, calling onCreate() with the bundle from
onSaveInstanceState(). Note, onCreate() is also called when your
application starts. But, when it's starting, bundle is null. Thus,
the standard implementation of onCreate() should check if the bundle
is null or not. If it's not null, then the app should restore its
state, using the information in the bundle.

Here's how the Bookmark saves and loads state information to a
bundle, note how the code is almost identical to that used for Shared
Preferences. (Oddly, SharedPreferences and Bundles are not related.)

Main Activity

MainActivity.java is, well, the application's main activity. It's
the activity that is first started when the application starts. It
uses the BitmapView as its view, creates the main menu and responds
to user selecting menu actions, and responding to user changing the
screen between landscape and portrait.

Comments and Discussions

I'd like to first tell you that it's really easy to use, and works pretty fast. Thumbs up!

Though, I have to ask you just one thing, when I switch the device orientation to Landscape mode, the image is centered, and looks very small. I want the image to be displayed in its actual size when I shift to Landscape orientation.

The newer code uses matrix math for scaling/computing the size of the bitmap and fixes some bugs in the older version. Unfortunately, it's been a while, and I can't remember exactly what the bugs were. (And I didn't keep a record of them either.) It may have fixed your issue.
For details on the matrix stuff, see: Simple Gestures on Android[^]

Finally, when load a new page (or toggle between portrait and landscape modes) the viewer is supposed to calculate the scaling so that the whole image can be seen. Thus, it scales the image to fill the available screen either vertically or horizontally. The calculations are done by the function computeInitialMatrix() in the file BitmapView.java. This is what you'd need to modify. However, as I'm not sure what you mean by "displayed in its actual size", I can't advise you how to change it.

Thanks a lot for the details provided... i got enough information from the section..I need one information on this.... i tried to implement the code in Android environment, in the Android EMU, it says no comics found. where should i keep the comic book for the app to read it.. could you please help on this

I love your project. I am learning a lot from it. I have a question about why you want the images to appear random though. If you are reading through a comic, don't you want the pages to appear in order? I would think you would want to read the images in order e.g. image1, image2, image3.Hope you can help

Usually the images files in the zip are given names like image001.jpg, image002.jpg, image003.jpg etc.Doing this means most zip file utilities will place pack the images into the zip in this order. e.g. image001.jpg will be the first image in the zip, image002.jpg will be the second, etc. Thus, the images are in the desired reading order.

If you can send me a link to the .cbz you're having problems with, I'll have a look at it and see if I can tell what the problem is.

Thank you for the feedback. But you say to use ZipFile.ZipFile so that we get a random selection of the images. That is the part that is confusing me. You say to not use ZipInputSteam because it "allows access to the contents of the file in a serial fashion, not what we want." My files are zipped up in order, but it looks like I'm scrolling through them randomly.I hope this clarifies my question.

Let me try again.http://en.wikipedia.org/wiki/Sequential_accesshttp://en.wikipedia.org/wiki/Random_accessBy Random access, I mean that we can retrieve any image we want from the zip file with a single request.e.g. If we've bookmarked the 200th image in the zip file, then when we tell the reader to go to the bookmark, we would like the reader to say (to the ZIP file) "give me image #200". We don't want the reader to have to say "give me the first image", then "give me the next image" 200 times. This is even more important when you're dealing with SD cards that are slow, only a few Megabytes per second.

Now I agree that normally we want to read a comic book in serial fashion. Start at the first page then move onto the next. However, there are occasions when we want a different order. For example, when we want to go backwards, to a previous page. Or to jump to a bookmark.

That said, this reader assumes the images have been loaded into the zip in the same sequence they are expected to be read in. Normally, this is done by giving the images names such that when the names are sorted into alphabetical order, then the names will be in the expected sequential reading order.This is because most Zip packers can be made to pack the files based on this alphabetical order.Now, if the viewer is not showing the images in sequential order, this indicates that the images have not been packed into the zip in sequential order. I can see two obvious ways this might happen.1. The images have not been named in alphabetical order.2. The zip tool used to create the ZIP file did not use alphabetical order to pack the files. (Possibly they've been packed in chronological order.)

If you can send me a link to the zip file, I can have a look at it and see what has happened. (Or you can examine the file yourself.)

Additional comment.Assuming the images in the zip file HAVE been named in an alphabetical order that matches the desired reading order, but have been packed into the zip in a different order, this can be easily fixed. Just sort the list of image names into alphabetical order.

Which can be done by adding this line to the CbzComic constructor, after obtaining the list of image names.

On reflection, I may have misunderstood your question.You're asking, "why do we want to be able to read pages (images) in the comic book without having to start at the beginning?"There's a number of reasons.1. Bookmarks. If you bookmark a page, you don't want the reader to have to go through every page to get to the bookmark.2. When reading, sometimes you want to go back and look at a previous page. And again, you'd like the reader to go straight to the page.3. Chapters. A CBZ may contain multiple chapters (or multiple issues of a comic) As the reader, you may be interested in re-reading only certain chapters, so again, you'd like to skip the ones you're not interested in.

Thank you again for your insight. I have learned a lot from your lesson. Everything works good for me. I'll try to see if it makes sense to use a zipped file of images compared to storing indivdual ones in my app.