The Panoramio web site has a great collection of photos from all over the world, and they also have a very convenient web API. I thought it would be a lot of fun to use Android to access this content while you are out walking around – and especially to have Android find interesting photos based on your current location. The resulting open source application is now available in the apps-for-android project.
The application starts by showing your current location in a custom MapView. You can pan and zoom this map to choose the area you want to search.
Once you have selected a search area, the application downloads thumbnails of the most popular photos taken within that area:
You can tap on an item to see more details.
From here you can use the menu to find more pictures by the same photographer or to see the original photo on the Panoramio site. My two favorite features, though, help you find the location of the photo in the real world. You can show the location on the map along with your current location:
Or, you can bring up a radar view that uses the compass and GPS to show you the location of the photo in relation to your own location:
I thought that other applications might want to use the radar view for their own purposes ("find me the nearest taqueria"), so I split that into its own package. You can find the source for that in the Radar project. Of course, you don't need the source in order to use this feature. Because of Android's component architecture, all you need to do us use an Intent:
Intent i = new Intent("com.google.android.radar.SHOW_RADAR"); i.putExtra("latitude", 37.422f); i.putExtra("longitude", -122.084f); startActivity(i);
The code for these applications is not very long, but they are a good example of how to use a number of Android's location APIs:
Using an embedded MapView along with a MyLocationOverlay to plot your current location. The is also a custom class which extends Overlay to draw the Panoramio pushpin.
BioWallet is a biometric authentication system for the Android platform that performs the identification of a user based on something that he/she is, other than something that he/she knows or owns. Currently, it supports iris and handwritten signature based authentication. BioWallet is also a password and secrets manager that stores the sensitive information using strong biometric encryption techniques and allows the user to recover them without remembering any password.
BioWallet is basically a “safe wallet” that can store sensitive information (passwords, certificates, documents, conversations, notes, pictures, etc.). This information can only be accessed to by the user resenting his/her iris or signing in the touch screen. The data is kept secret even if the device is lost or stolen using strong biometric encryption techniques.
The user doesn’t need to worry anymore remembering a lot of different and difficult passwords or using the same simple password for everything.
Advanced users can also digitally sign their ocuments or encrypt the communications.
HIGHLY SECURE
The information is encrypted with military grade security (AES 256 bits) and only can be decrypted using your biometric characteristics. The encryption is even resistant to brute-force and dictionary based attacks because the internal keys used are generated long enough and completely random to prevent them.
EASY TO USE
The application has been designed to be both powerful and easy to use by non technical people. Two different profiles will give you what you want: power and flexibility for “advanced” users and ease to be used by people that “just want it to work”.
PROTECTS YOUR PRIVACY
Your biometric data are never sent outside your mobile device or stored, so they are safe even if the phone is lost or stolen.
Years ago I was addicted to a simple game that I played on my then state-of-the-art Pentium-75. In this game, balls would bounce around, and I would try to partition them into small enough spaces so that I could go to the next level where more and more balls would be added. As of a couple of months ago, for the life of me, I couldn't remember the name of this game. So when I sat down to write an application for Android in my 20% time, I thought, why not try to recreate something similar? After completing most of the game and showing it to some of my friends at work, one of them said, "Oh, this reminds me of JezzBall!" Eureka! If working on this game does nothing more than reveal the name of one of the favorite games of my youth, I'll call it a success, but in the meantime, I'm happy to announce that the source of this application, named Divide and Conquer, is now available on apps-for-android.
The game starts with instructions:
and begins simply enough with one ball bouncing around. You drag your finger in a horizontal or vertical gesture on the screen to initiate a line that extends until it reaches the edges:
In each level, once you've shaved off 80% of the original area, you move to the next level. Here's a screen shot of level 6:
If a ball hits a line in progress, you lose a life:
Once you are out of lives, it is game over:
While this game isn't going to win any awards for cutting edge graphics, it demonstrates use of several Android features and APIs:
custom drawing and animation
touch input based gesture detection
overriding the default behavior of the back key in some situations (to pause the game)
DivideAndConquerView is a custom View that implements its own onDraw method using the Canvas methods, and gesture detection using onTouchEvent and a helper class DirectionPoint. It keeps track of the state of the game using BallEngine, and reports relevant events back to the main activity of the application, which, in turn, keeps track of and controls the state of the game. The application is configured to be full screen in its AndroidManifest.xml file.
I've posted three new open source samples to the apps-for-android project: Triangle, SpriteText and Downloader.
The first two samples, Triangle and SpriteText, show techniques that would be useful to anyone using the OpenGL ES 3D graphics APIs to write Android applications. The samples contain several reusable classes that may eventually be incorporated (in some form) into the SDK. Chief among these is the GLView class, which abstracts the OpenGL ES book-keeping code from the rest of the application. GLView helps handle the extra work OpenGL ES applications have to do when the activity is paused and resumed, and when the display goes to sleep and wakes up. In the Pause/Resume case the OpenGL surface has to be recreated. In the display sleep / wake-up case the entire OpenGL context has to be recreated.
Triangle
The first sample, Triangle, shows how to use the GLView class and the OpenGL ES 3D library to display a spinning textured triangle. Think of it as the "hello, world" of OpenGL ES apps. Because it's relatively simple, it's a good place to start when experimenting with the OpenGL ES API.
SpriteText
The second sample, SpriteText, shows how to efficiently display screen-aligned text using the GL11Ext.glDrawTexiOES method. SpriteText contains a reusable LabelMaker class for drawing static text and screen-aligned images, as well as a Projector class for finding the 2D screen coordinates corresponding to a 3D point, and a MatrixTrackingGL class for keeping track of the current transformation matrix. Finally, it shows how to use these classes to display a milliseconds per frame counter. A ms/f counter can be helpful for tuning graphics performance.
Downloader
The third sample, Downloader, shows how to add a downloader activity to your application. The downloader activity runs at the beginning of your application and makes sure that a set of files have been downloaded from a web server to the phone's SD card. Downloader is useful for applications that need more local data than can fit into an .apk file. For example a game could use Downloader to download the game's artwork, sound effects, and level data. The Downloader activity is designed to be a drop-in addition to your application. You customize it by supplying the URL of an XML configuration file which lists the data files that need to be downloaded.
You may remember way back when (e.g., July 2008) when Building ‘Droids featured a six-post series on creating fancy ListView implementations, culminating in a CheckListView widget that could be used as a drop-in replacement for ListView.
They’re ba-ack!
Specifically, today, let’s take a look at the 0.9 SDK’s impact on the Fancy ListView series, and update one of the examples to take advantage of a new widget: the RatingBar.
In many respects, the code and techniques introduced in the Fancy ListViews series are still valid and relevant in the world of the 0.9 SDK. Perhaps the biggest change is that ViewInflate became LayoutInflater, requiring some search-and-replace edits of your older M5 source code. But the concepts of supplying custom views, of recycling views using the ViewHolder/ViewWrapper pattern, and the like are still very useful, even after the 0.9 SDK.
Of course, as promised, the 0.9 SDK has its own take on having checkable ListView widgets; this will be covered in a future blog post.
For now, though, let’s revisit the last sample from the Fancy ListViews series and update it to eschew the checkbox, switching to the new RatingBar.
RatingBar is a widget designed to users to provide a rating to something, such as rating a music track or a blog post or an Android development book up on Amazon.com (*cough*). A rating is a float, from 0 to a specified maximum (android:numStars). You can specify what the granularity of the rating is (android:stepSize) and whether it is merely an indicator (android:isIndicator) or if it is a user-input element to allow users to set the rating by sliding their finger across the stars.
As with just about any moderately-sized widget, the RatingBar can be used as part of a row in a ListView. In fact, replacing the CheckBox in CheckListView with a RatingBar is fairly straight-forward:
Create and configure the RatingBar when it is lazy-instantiated in getView()
Hook in a RatingBar.OnRatingBarChangeListener to update a float[] of ratings, rather than a boolean[] of checkbox states
Update the rating in the RatingBar when it is recycled
Rename all the classes to something more logical (e.g., CheckableWrapper becomes RateableWrapper)
Unfortunately, the stock style for RatingBar is a bit big for lists:
You can use a ratingBarStyleSmall style to shrink the size, but then the RatingBar becomes read-only, meaning you would need some other means to let people specify the rating itself.
Earlier today the latest Android 0.9 SDK was released, and it’s packed full of wonderful changes. As you play around, you might see ListViews split into sections using separating headers. (Example shown on the right is the browser settings list.)
There isn’t an easy way of creating these separated lists, so I’ve put together SeparatedListAdapter which does it quickly. To summarize, we’re creating a new BaseAdapter that can contain several other Adapters, each with their own section headers.
First let’s create some simple XML layouts to be used for our lists: first the header view, then two item views that we’ll use later for the individual lists. (Thanks to Romain Guy for helping me find existing styles to keep these XML layouts nice and tidy.)
Now let’s create the actual SeparatedListAdapter class which provides a single interface to multiple sections of other Adapters. After using addSection() to construct the child sections, you can easily use ListView.setAdapter() to present the now-separated list to users.
As for the Adapter internals, to correctly find the selected item among the child Adapters, we walk through subtracting from the original position until we find either a header (position = 0) or item in the current child Adapter (position <>
public class SeparatedListAdapter extends BaseAdapter { public final Map sections = new LinkedHashMap(); public final ArrayAdapter headers; public final static int TYPE_SECTION_HEADER = 0; public SeparatedListAdapter(Context context) { headers = new ArrayAdapter(context, R.layout.list_header); } public void addSection(String section, Adapter adapter) { this.headers.add(section); this.sections.put(section, adapter); } public Object getItem(int position) { for(Object section : this.sections.keySet()) { Adapter adapter = sections.get(section); int size = adapter.getCount() + 1; // check if position inside this section if(position == 0) return section; if(position < total =" 0;" total =" 1;" type =" 1;" adapter =" sections.get(section);" size =" adapter.getCount()" position ="=" sectionnum =" 0;" adapter =" sections.get(section);" size =" adapter.getCount()" position ="=">
As expected, it correctly prevents the section headers from being selected, and seamlessly stiches together the various Adapters.
This approach also uses convertView correctly as long as the child Adapters return getItemViewType() and getViewTypeCount() normally. No special changes are needed for an Adapter to become a child.
Now let’s use SeparatedListAdapter in some example code. We use the XML layouts defined earlier to create an ArrayAdapter and an advanced two-row SimpleAdapter, and then add both as sections to our SeparatedListAdapter.
public class ListSample extends Activity { public final static String ITEM_TITLE = "title"; public final static String ITEM_CAPTION = "caption"; public Map createItem(String title, String caption) { Map item = new HashMap(); item.put(ITEM_TITLE, title); item.put(ITEM_CAPTION, caption); return item; } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); List
The resulting interface behaves just like the browser preferences list, and you could easily create other custom Adapters to insert into the various sections, such as including icons or checkboxes.
These section headers can really help separate out otherwise-cluttered activities. I used them several places in my CompareEverywhere application which lets you easily compare prices and read reviews for any product with a ba
We’ve all seen the Apple app that lets you control iTunes remotely, and there totally needs to be something like this for Android. I’m an avid fan of Amarok, so I’ve whipped together a simple remote control that runs on Android. It only took about 3 hours tonight, and I’m releasing everything GPLv3 here. First some details on the architecture:
Amarok can easily be controlled via DCOP, including fetching currently playing information and album art. DCOP is a local, safe IPC architecture that usually comes already enabled with KDE applications. Just a reminder that DCOP is being phased out in KDE 4 with D-Bus taking its place. Speaking of the D-Bus future, there is an awesome standard called Media Player Remote Interface Specification (MPRIS) that is being put together by the folks at XMMS, VLC, Amarok, and others. It doesn’t seem to be in stable releases yet, but will be soon. Back to the present, I’m going to just focus on getting Amarok 1.4 working with older DCOP calls.
First, I built a Python bridge that offers a simple HTTP REST-like API that will help us relay pre-approved DCOP commands from Android to Amarok. The Python is pretty simple:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
print'started amarokremote server on port %s' % (port)
server.serve_forever()
except KeyboardInterrupt:
server.socket.close()
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import pydcop, re port = 8484 allowed = ["status", "trackCurrentTime", "trackTotalTime", "album", "artist", "title", "next", "playPause", "prev", "volumeDown", "volumeUp", "coverImage", "seek"] resafe = re.compile("[^A-Za-z0-9/]") class AmarokHandler(BaseHTTPRequestHandler): def do_GET(self): # pull out action and simple variable safe = resafe.sub('', self.path) blank, action, var = tuple(safe.split('/')) # skip if action has not been approved if not action in allowed: return # check if image request if action == "coverImage": self.send_response(200) self.send_header('Content-type', 'image/jpeg') self.end_headers() cover = open((pydcop.DCOPMethod("amarok", "player", "coverImage"))()) self.wfile.write(cover.read()) cover.close() return # make dcop call over to amarok if len(var) > 0: reply = (pydcop.DCOPMethod("amarok", "player", action))(int(var)) else: reply = (pydcop.DCOPMethod("amarok", "player", action))() # write back any dcop response self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(reply) return try: server = HTTPServer(('', port), AmarokHandler) print 'started amarokremote server on port %s' % (port) server.serve_forever() except KeyboardInterrupt: server.socket.close()
Essentially we are using a URL of the form http://ipaddress:port/command/variable. For example, in the Android emulator you could call http://10.0.2.2:8282/volumeUp/ to increase the volume.
Carefully note that we are screening the commands against an “allowed” list before running off to Amarok with them. We’re also scrubbing the incoming calls to prevent any buffer overflows. Usually we are just calling the Amarok DCOP method and returning any result. One exception is for coverImage requests, because Amarok just returns a local path. We help that image over the Python bridge by sending the local JPEG as the response.
On the Android side of things, we have a simple screen layout and are hooking up the various API calls to buttons. There’s also a background thread that keeps Android in-sync with what Amarok is currently playing. Finally, we’re using some simple preferences to store the server string and update interval. (Tap the menu button to change these settings.)
Here’s a tarball of the Eclipse project, along with the APK that’s ready to run on the new 0.9 SDK. Start the Python server above on your the computer running Amarok, change the IP address if needed, and you should be ready to go.
Also, a reminder that 10.0.2.2 is magic on the Android emulator because it points to the loopback adapter of the parent computer. So, if you’re running the emulator and Amarok on the same computer, this IP address will work perfectly.
Inevitably, you’ll get the question “hey, can we change this font?” when doing application development. The answer depends on what fonts come with the platform, whether you can add other fonts, and how to apply them to the widget or whatever needs the font change.
Android is no different. It comes with some fonts plus a means for adding new fonts. Though, as with any new environment, there are a few idiosyncrasies to deal with.
Android natively knows three fonts, by the shorthand names of “sans”, “serif”, and “monospace”. These fonts are actually the Droid series of fonts, created for the Open Handset Alliance by Ascender.
For those fonts, you can just reference them in your layout XML, if you choose, such as:
This layout builds a table showing short samples of four fonts. Notice how the first three have the android:typeface attribute, whose value is one of the three built-in font faces (e.g., “sans”).
The three built-in fonts are very nice. However, it may be that a designer, or a manager, or a customer wants a different font than one of those three. Or perhaps you want to use a font for specialized purposes, such as a “dingbats” font instead of a series of PNG graphics.
The easiest way to accomplish this is to package the desired font(s) with your application. To do this, simply create an assets/ folder in the project root, and put your fonts (in TrueType, or TTF, form) in the assets. You might, for example, create assets/fonts/ and put your TTF files in there.
Then, you need to tell your widgets to use that font. Unfortunately, you can no longer use layout XML for this, since the XML does not know about any fonts you may have tucked away as an application asset. Instead, you need to make the change in Java code:
public class FontSampler extends Activity { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); TextView tv=(TextView)findViewById(R.id.custom); Typeface face=Typeface.createFromAsset(getAssets(), "fonts/HandmadeTypewriter.ttf"); tv.setTypeface(face); } }
Here, we grab the TextView for our “custom” sample, then create a Typeface object via the static createFromAsset() builder method. This takes the application’s AssetManager (from getAssets()) and a path within your assets/ directory to the font you want.
Then, it is just a matter of telling the TextView to setTypeface(), providing the Typeface you just created. In this case, we are using the Handmade Typewriter font.
The results?
Note that Android does not seem to like all TrueType fonts. I spent a lot of time trying to figure out what I was doing wrong with this sample, before switching to a different font, and everything “just worked”. When Android dislikes a custom font, rather than raise an Exception, it seems to substitute Droid Sans (”sans”) quietly. So, if you try to use a different font and it does not seem to be working, it may be that the font in question is incompatible with Android, for whatever reason.
Also note that TrueType fonts can be rather pudgy, particularly if they support an extensive subset of the available Unicode characters. The Handmade Typewriter font used here runs over 70KB; the DejaVu free fonts can run upwards of 500KB apiece. Even compressed, these add bulk to your application, so be careful not to go overboard with custom fonts, lest your application take up too much room on your users’ phones.
Again, there was a bit of a break in the blog. One reason is personal: I moved to London to take a new job. The other reason is that this time I went into the hairy issue which is call handling in Android.
I worked with the m3-rc37a version of the emulator and I can tell you that emulator crashes are really common if you play with the telephony stuff in this version. Don't be surprised if the example program distributed with this blog entry does not show the behaviour I promise but crashes instead. It just happens with this emulator version.
The task I gave myself is catching incoming and outgoing calls which is easy. There needs to be just an IntentReceiver defined in the manifest and the onReceiveIntent method implemented in the intent receiver class. The manifest entry declares the filter for the IntentReceiver (again, the XML is mangled due to limitations of the blog engine).
PHONE_STATE is a general intent for signalling all sorts of phone events. The example program dumps the event bundle when the event comes in. In case of outgoing call, the event is delivered with state=OFFHOOK when the call starts then state=IDLE when the call finishes (state is a key in the intent bundle ("extras") and its string values are OFFHOOK and IDLE). In case of the incoming call (you can simulate it by telnet localhost 5554 then issuing the gsm call phone_number command on the console) the state transition is RINGING then IDLE again.
So far so good. Having a notification on incoming and outgoing calls is less useful, however, if the caller or the called number is not known. This turned out to be a really hairy issue for which I did not find the answer (just desperate help requests). I remembered the good old Series60 days and went for the call log.
The call log is surprisingly co-located in the database of the Contacts application. Try this:
adb shell #cd /data/data/com.google.android.providers.contacts/databases # sqlite3 contacts.db SQLite version 3.5.0 Enter ".help" for instructions sqlite> .dump ... CREATE TABLE calls (_id INTEGER PRIMARY KEY,number TEXT,number_key TEXT,number_type TEXT,date INTEGER,duration INTEGER,type INTEGER,person INTEGER,new INTEGER); INSERT INTO "calls" VALUES(1,'+44444','44444+','Home',1200263250247,0,3,NULL,0); INSERT INTO "calls" VALUES(2,'+4444445','5444444+','Home',1200263859817,5,2,NULL,1); INSERT INTO "calls" VALUES(3,'+1234','4321+','Home',1200263990161,2,2,NULL,1);
There is the log. Unfortunately (or fortunately? :-)) Android enforces strict database separation for applications so the Contacts database cannot be accessed from within another Android application. Luckily, the Contacts application decided to share the data as Content Provider. There is even a facilitator class: CallLog.Calls. Dumping the call log is relatively easy after that (look at the PhoneIntent class which is an Activity that makes this dump on pressing a button).
Now the only thing remained to see how the database is updated with regards to phone calls. It turned out that the number is not visible when the call goes out or comes in (RINGING or OFFHOOK state) but is accessible through the content provider when the IDLE transition event comes in. I was not able, therefore, to capture the number of the currently ongoing call but I was able to capture the number of the call that has just finished.
Testing the example application was a pain for me. When working with calls, the emulator tended to crash on my PC without installing any applications. In order to monitor the event transitions, you have to bring up the phone application because it does not come to the front by itself. You can do that by pressing some digits at the emulator's opening screen. If you don't bring the phone app to front manually, you won't be able to answer or reject the call.
I propose that you don't run adb logcat while the call goes on - it tends to increase the chance of emulator crash. You can examine whether the number was retrieved succesfully from the call log by running adb logcat after the call was finished.
Even if you have a phone with excellent screen resolution, the physical screen size is still rarely over 3″x5″, since most people want phones that can fit in a pocket, purse, pouch, or poncho.
This means your Android activities can only display so much stuff at one time without individual widgets or text getting too small to be easily read. There are any number of ways to handle this:
You could break the one activity into several…but this can get cumbersome
You can use ScrollView to allow users to scroll through a longer activity…but this may or may not be easy for the user depending on the device (e.g., a haptic interface using a swipe for scrolling assumes there’s a place clear in the UI for them to actually swipe without accidentally toggling a button or something)
You can use TabView, as described in the previous post…but you are locked into a particular presentation pattern (e.g., tabs)
You can use ViewFlipper and work out a UI pattern that’s right for you
Not surprisingly, today, we’re going to look at the latter option.
ViewFlipper inherits from FrameLayout, just like we used to describe the innards of a TabView in that previous post. However, initially, it just shows the first child view. It is up to you to arrange for the views to flip, either manually by user interaction, or automatically via a timer.
For example, here is a layout for a simple activity using a Button and a ViewFlipper:
Notice that the layout defines three child views for the ViewFlipper, each a TextView with a simple message. Of course, you could have very complicated child views, if you so chose.
To manually flip the views, we need to hook into the Button and flip them ourselves when the button is clicked:
public class FlipperDemo extends Activity { ViewFlipper flipper; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); flipper=(ViewFlipper)findViewById(R.id.details); Button btn=(Button)findViewById(R.id.flip_me); btn.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { flipper.showNext(); } }); } }
This is just a matter of calling showNext() on the ViewFlipper, like you can on any ViewAnimator class.
The result is a trivial activity: click the button, and the next TextView in sequence is displayed, wrapping around to the first after viewing the last:
This, of course, could be handled more simply by having a single TextView and changing the text and color on each click. However, you can imagine that the ViewFlipper contents could be much more complicated, like the contents you might put into a TabView.
As with the TabView in the previous post, sometimes, your ViewFlipper contents may not be known at compile time. As with TabView, though, you can add new contents on the fly with ease.
For example, let’s look at another sample activity, using this layout:
Notice that the ViewFlipper has no contents at compile time. Also note that there is no Button for flipping between the contents — more on this in a moment.
For the ViewFlipper contents, we will create large Button widgets, each containing one of the random words used in many an Android tutorial. And, we will set up the ViewFlipper to automatically rotate between the Button widgets, using an animation for transition:
After getting our ViewFlipper widget from the layout, we first set up the “in” and “out” animations. In Android terms, an animation is a description of how a widget leaves (”out”) or enters (”in”) the viewable area. Animations are a complex beast, worthy of a blog post or two in their own right. For now, realize that animations are resources, stored in res/anim/ in your project. For this tutorial, we are using a pair of animations supplied by the SDK samples, available under the Apache 2.0 license. As their names suggest, widgets are “pushed” to the left, either to enter or leave the viewable area.
After iterating over the funky words, turning each into a Button, and adding the Button as a child of the ViewFlipper, we set up the flipper to automatically flip between children (flipper.setFlipInterval(2000);) and to start flipping (flipper.startFlipping();).
The result is an endless series of buttons, each appearing, then sliding out to the left after 1.5 seconds, being replaced by the next button in sequence, wrapping around to the first after the last has been shown:
In this case, the above screenshot shows the ViewFlipper in mid-transition, animating away from one button to the next.
The auto-flipping ViewFlipper is useful for status panels or other situations where you have a lot of information to display, but not much room. The key is that, since it automatically flips between views, expecting users to interact with individual views is dicey — the view might switch away part-way through their interaction.
The TabWidget in Android is set up to allow you to easily define tabs at compile time. However, sometimes, you want to add tabs to your activity during runtime. For example, imagine an email client where individual emails get opened in their own tab, for easy toggling between messages. In this case, you don’t know how many tabs or what their contents will be until runtime, when the user chooses to open a message.
Fortunately, Android also supports adding tabs dynamically at runtime, and this blog post will show you how.
For compile-time-defined tabs, in your Java code, to set up tabs with contents provided in layout XML, you simply create a TabHost.TabSpec object and call setContent() on it, with the widget ID of the tab’s contents:
Adding tabs dynamically at runtime works much the same way, except you use a different flavor of setContent(), one that takes a TabHost.TabContentFactory instance. This is just a callback that will be invoked — you provide an implementation of createTabContent() and use it to build and return the View that becomes the content of the tab.
Let’s take a look at an example.
First, here is some layout XML for an activity that sets up the tabs and defines one tab, containing a single button:
In our button’s setOnClickListener() callback, we create a TabHost.TabSpec object and give it an anonymous TabHost.TabContentFactory. The factory, in turn, returns the View to be used for the tab — in this case, just an AnalogClock. The logic for constructing the tab’s View could be much more elaborate, such as using ViewInflate to construct a view from layout XML.
Initially, when the activity is launched, we just have the one tab:
But, if we click the button three times, though, we get three new tabs, each containing a clock:
While in this case we added new tabs based on button clicks, you could use any sort of event as the trigger for adding a tab: long-tap on a listbox entry, option menu selection, etc.