Home

Android Indoor Maps

Back in May of this year, Google released an update to their Play Services. Up until then, it wasn't possible to use all the features of indoor maps found in Google Maps for Android. Strangely, iOS has had this feature for some time now. One of the those features was being able to manipulate the floor picker. To start using indoor maps properly, you will need to create a new project and configure it to use Google Play Services. The guide can be found here. Since the indoor maps is an added feature to Google Maps, there isn't a lot of additional coding that needs to be done. I've created a simple mobile application that takes advantage of the new Indoor API functions.

The Manifest is pretty self-explanatory and while the layout file is also self explanatory, I would like to explain the spinner found in the layout. Having a spinner, which controls the active floor is redundant, I only added it to demonstrate how to manipulate the map.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paulusworld.androidindoormaps"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    <!--
			The following two permissions are not required to use
			Google Maps Android API v2, but are recommended.
    -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="THIS_IS_WHERE_YOU_PUT_YOUR_API_KEY" />

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.paulusworld.androidindoormaps.MainActivity" >

    <fragment
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="45"
        class="com.google.android.gms.maps.SupportMapFragment" />

    <TextView
        android:id="@+id/textCoordinates"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="5"
        android:text="@string/btn_load_floors"
        android:textAlignment="center"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textIsSelectable="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="5"
        android:orientation="horizontal" >

        <Spinner
            android:id="@+id/spinnerFloors"
            android:layout_width="0dp"
            android:layout_height="wrap_content" 
            android:layout_weight="70" />
        
    </LinearLayout>

</LinearLayout>
package com.paulusworld.androidindoormaps;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnIndoorStateChangeListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.IndoorBuilding;
import com.google.android.gms.maps.model.IndoorLevel;
import com.google.android.gms.maps.model.LatLng;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

public class MainActivity extends FragmentActivity {

	private Context mContext;
	private GoogleMap mMap;
	private Spinner mSpinnerFloors;
	private IndoorBuilding mBuilding;
	private ArrayAdapter<String> mBuildingFloors;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mContext = this.getBaseContext();

		mSpinnerFloors = (Spinner) findViewById(R.id.spinnerFloors);
		mSpinnerFloors.setOnItemSelectedListener(new OnItemSelectedListener() {

			@Override
			public void onItemSelected(AdapterView<?> parent, View view,
					int position, long id) {
				try {
					mMap.getFocusedBuilding().getLevels().get(position)
							.activate();
				} catch (NullPointerException npe) {
					// In a production application, we would do something with
					// the exception.
				}

			}

			@Override
			public void onNothingSelected(AdapterView<?> parent) {

			}

		});

	}

	@Override
	protected void onResume() {
		super.onResume();
		setupMapIfNeeded();
	}

	private void setupMapIfNeeded() {
		if (mMap == null) {
			mMap = ((SupportMapFragment) getSupportFragmentManager()
					.findFragmentById(R.id.map)).getMap();
			if (mMap != null) {
				setupMap();
			}
		}
	}

	private void setupMap() {
		// Pick a location that has indoor maps and zoom in far enough to show
		// the floor plans.
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(43.040715,
				-87.921467), 18));

		// Add a listener for when the indoor state changes.
		mMap.setOnIndoorStateChangeListener(new OnIndoorStateChangeListener() {

			@Override
			public void onIndoorBuildingFocused() {
				if (mBuilding != mMap.getFocusedBuilding()) {

					mBuilding = mMap.getFocusedBuilding();
					mBuildingFloors = getBuildingFloors(mBuilding);
					mSpinnerFloors.setAdapter(mBuildingFloors);

				}
			}

			@Override
			public void onIndoorLevelActivated(IndoorBuilding building) {

				mSpinnerFloors.setSelection(building.getActiveLevelIndex());

			}

		});

		mMap.setOnCameraChangeListener(new OnCameraChangeListener() {

			private float mCurrentZoom = -1;

			@Override
			public void onCameraChange(CameraPosition position) {
				try {
					if (position.zoom != mCurrentZoom) {

						mCurrentZoom = position.zoom;

						if (mMap.getFocusedBuilding() == null) {

							mBuildingFloors.clear();
							mBuildingFloors.notifyDataSetChanged();

						}
					}
				} catch (NullPointerException npe) {
					// In a production application, we would do something with
					// the exception.
				}
			}
		});
	}

	private ArrayAdapter<String> getBuildingFloors(IndoorBuilding indoorBuilding) {

		ArrayAdapter<String> floors = new ArrayAdapter<String>(mContext,
				android.R.layout.simple_spinner_dropdown_item);

		if (indoorBuilding != null) {
			indoorBuilding.getLevels()
					.get(indoorBuilding.getDefaultLevelIndex()).activate();

			for (IndoorLevel level : indoorBuilding.getLevels()) {
				floors.add(level.getName());
			}
		}

		return floors;
	}
}

Lines 31 - 59 set up the interface. On line 37, we're adding an OnItemSelectedListener to the spinner. The listener has two methods, but we're only using the onItemSelected. The onItemSelected is asking the map if there is a focused building. If there is an active building, get the levels and set the active level of the building to the same level the user selected from the spinner control. 

Line 80 moves the camera to a predefined location and zooms in. Indoor maps are only visible at certain levels. Therefore, to display the floor picker, we're setting the zoom level to 18. 

Line 84 adds a OnIndoorStateChangeListener to the map object. This listener has two methods, onIndoorBuildingFocused and onIndoorLevelActivated. The onIndoorBuildingFocused is looking to see if the building has changed. if it has, then we want to get the floors for the new building and populate them in the spinner. 

Line 100 updates the spinner whenever the user selects a different floor from the floor picker. 

Lines 106 - 130 is a feature in which spinner is cleared if the user zooms out far enough, ultimately losing focus of the building. 

getBuildingFloors which is defined on line 132 is a helper function. The function takes a IndoorBuilding object as a parameter, which is used to build an ArrayAdapter containing the names of the floors in the building. 

Objective-C Classes

If you’ve been programming in one of the more popular object oriented programming languages such as C++ or Java, Objective-C may be a bit confusing at first. With Objective-C, you send objects messages opposed to calling methods. With C++ and Java, calling a method name means that, in most cases, it is bound to a section of code in the target class by the compiler. However, with Objective-C, the target of the message is resolved at runtime.

At the cost of speed and multiple inheritance, Objective-C is able to support dynamic binding by default. This means that messages can go unimplemented or be defined at runtime. However, implementation is still required for the method to be called in the derived object.

Creating a class is typically done using two files. The first being the header file (*.h). Within this file, is where you declare your class with its instance variables and methods/messages. To indicate that you are declaring a class, you use the @interface keyword to begin the class declaration and the @end keyword to end the declaration.

@interface Event : NSObject
{
    // Instance variables
    @public
    NSString* public_var;
    @protected
    NSString* protected_var;
    @private
    NSString* private_var;
}

@property NSInteger* type;
@property NSString* name;
@property NSDate* startTime;
@property NSDate* endTime;
@property NSString* description;

// Class methods
+ (id) newInstance;

// Instance methods
- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description;

@end

The second file that is typically used is a ‘.m’ which originally meant “messages.” The .m file is similar to a .c or .cpp file which where you implement your code.

#import "Event.h"

@implementation Event
{
    // Instance variables can also be declared here.
}

// The plus (+) indicates that this is a class message.
+ (id) newInstance
{
    return [[Event alloc] init];
}

- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description
{
    self = [super init];
    if(self)
    {
        _name = name;
        _type = type;
        _startTime = start;
        _endTime = end;
        _description = description;
    }
    
    return self;
}
@end

In Objective-C there are two types of keywords, the standard C/C++ keywords, such as int, break, continue, etc. The second set starts with a ‘@‘ (at) symbol. The latter is used to differentiate Objective-C keywords from C/C++ keywords. It’s important to note that the ‘@‘ has multiple meanings based on the context it is used. This symbol is also used for starting strings:

NSString* str = @“Hello World!”

It is also used as literals:

NSArray* array = @[ obj1, obj2, obj3 ];
NSDictionary* dictionary = @{ key1 : obj1, key2 : obj2, key3 : obj3 };
NSNumber* number = @(2 + 2);

Instance Variables

Instance variables can be defined in either the @interface or @implementation as seen in the example code above. When declaring instance variables, the variables are private opposed to properties, which are public. However, you can change the scope by using @public, @private, or @protected.

@interface Event : NSObject
{
	@public
	NSString* name;
	@protected
	NSDate* time;
	@private
	NSString* description;
}
// properties and messages
@end

@implementation Event
{
	@public
	NSString* name;
	@protected
	NSDate* time;
	@private
	NSString* description;
//message declarations
@end

Properties

When declaring properties in an Objective-C class, you are declaring variable as public, giving external classes access to that property. It is possible to limit the access to read only by declaring the property as readonly. Additionally, properties can be declared as: 

  • atomic - By default all properties are atomic, which means they are locked when they are accessed or set. This is not the same as thread safety. This means that if a value is being set, the getter must wait for the value to fully be set before the value can be retrieved.
  • nonatomic - Prevents the property from being locked.
  • readonly - Declaring a property as readonly may be provided with storage semantics such as assign, copy, or retain.
    • assign - Used to set the property’s pointer to the address of the object without retaining it. This is used with scalar types of data (I.e., int, float, char, etc.)
    • copy - the property will maintain a copy of the original value. When specifying a property to keep a copy of the value, it must support NSCopying.
    • retain - Default, system will manage the retain count on its own.
  • getter = getterName: and setter = setterName: - By specifying a getterName and setterName name, you override the getter and setter messages generated when synthesizing properties. If you do not specify getter and setter names, the compiler automatically generates these for you. The getter and setter messages follow a specific naming convention:
    • The getter message is the same property name. If you declare a property of name, you would use the getter as such:
[object name];
    • The setter used to set the property’s value pre-appends the word ‘set’ and capitalizes the first character of the property if it’s not already capitalized. If we have a property of firstName, the setter would be setFirstName.
[object setFirstName:@“Paulus”];
  • strong- By default properties are implicitly declared as having a strong relationship. Meaning that the property will remain in memory until the object is set to nil and it’s not owned by anything else.
  • weak - Used if you don’t want control over the object’s life. The most frequently use cases are when there is a two way relationship between objects. Using a weak reference avoids the possibility of retain cycles.

When a property is declared, an instance variable is created and identified by the property’s name with a pre-appended underscore (_). In the above example, name and age all have an instance variable called _name and _age. However, We defined a different instance variable for address. Instead of _address, the instance variable is called instanceAddress.

@interface Person : NSObject 
@property(copy) NSString* name;
@property(readonly) NSInteger* age;
@property(setter = move:) NSString* address;
// .. messages
@end

@implementation Person
@synthesize name;
@synthesize age;
@synthesize address = instanceAddress;
// .. message
@end

Messages

The syntax for messages are a lot different than methods in C/C++, Java, and PHP. With methods and functions, the object needs to respond or have code to execute. Essentially, you’re jumping to a location in memory to execute code. However, Objective-C objects may choose to not respond or forward the message. This is what makes Objective-C a more dynamic language.

You define messages using labels, argument types, and argument names. If the message does not require parameters, then the message only needs one label is the name of the message:

+ (id) newInstance

Messages with a single parameter are pretty straight forward:

- (void) setName:(NSString *)name;

setName is the name of the message, and name is the parameter you are passing. When dealing with messages that take multiple arguments, the message, in my opinion, becomes difficult to read.

- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description

Again, initWithName is the name of the message and also the label for the first parameter. (NSString *) is the type of parameter we’re going to be passing. withType, withStartTime, withEndTime, and withDescription are all labels for the parameters that immediately follow; start, end, and description, respectively.

Categories & Class Extensions

Categories allow you to add messages to an existing class, even if you do not have the original source. For example, if you wanted to add a helper message to the NSObject class, you can.

Categories are typically declared and implemented in two files; a header file (.h) and a messages file (.m). To differentiate the class and category files, a different naming convention is employed by categories. These files are named by taking the name of the class, category, and concatenating them with a plus sign (+):

MyClass+MyCategory.h
MyClass+MyCategory.m
NSObject+MyCategory.h
NSObject+MyCategory.m

To add an existing class to a category you would do the following:

#import "Event.h"

@interface Event (ChildrensEvent)
- (NSInteger) totalHeadCount;
@end

Note the (ChildrensEvent) — This means that you are adding the event class to the ChildrensEvent category. If a category doesn’t exist already, it will be created automatically. In order to use the messages created in the category, you will need to include the header file where ever you wish to use the messages of the category.

#import 
#import "Event.h"
#import "Event+ChildrensEvent.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        
        Event* event = [Event newInstance];
        
        [event setName:@"New Event Name"];
        
        NSLog(@"\n%@", [event name]);
        
        [event setChildCount:2];
        
        NSLog(@"\nNumber of Children: %d", [event totalHeadCount]);
        
    }
    return 0;
}

Categorizing your own classes to add new messages won’t be a problem. When adding existing classes from another framework to a category you may run into name clashes, so it’s advised to add a three letter prefix to the message names. If there is another message with the same name, it’s impossible to know which one will be called.

Categories will not allow you to add instance variables and properties to existing classes. However, you can add instance variables and properties by using class extensions, which are also known as anonymous categories. Unlike categories, where you don’t need the source to add messages, class extensions require that you have the original source. If you add any messages to a class extension, you must implement them.

A useful trick you can do with class extensions is making messages and properties private, meaning that they are only accessible to the class itself. You could only declare a property as readwrite or readonly, which is an all or nothing situation. To get around this, you would declare the property as readonly and create a class extension that has the same property name, but as readwrite:

@interface Event : NSObject
@property (readonly) NSInteger eventId;
@end

#import "Event.h"

@interface Event ()
{
    // Instance variables
}
@property NSInteger eventId;
@property NSInteger childrenThreeToFive;
@property NSInteger childrenSixToEight;

@end

Like classes and categories, class instances are declared and implemented in two different files. The naming convention for class instances is the class name you wish to extend and the name of the class extension concatenated by an underscore (_):

MyClass_MyClassExtension.h
MyClass_MyClassExtension.m
Event_ChildrensEvent.h
Event_ChildrensEvent.m

Protocols

Protocols are used to declare instance and class messages as well as properties that are independent of any specific class, unlike class interfaces. To declare a protocol, use the Objective-C protocol keyword in place of the interface or implementation keywords. Like classes, protocols can inherit from other protocols. The EventDataSource example below shows that the protocol is inheriting from NSObject. If you would like to inherit from another protocol, substitute NSObject with the protocol of your choosing.

@protocol EventDataSource 
- (NSUInteger) totalHeadCount;
- (NSUInteger) numberOfChildren;
- (NSUInteger) numberOfAdults;
@end

When working with data source objects, it’s important to declare the property as weak to avoid strong reference cycles:

@property (weak) id  dataSource;

Using the basic type of id, will allow the class handle any object type as long as it conforms to the protocol. The protocol that we expect the object to conform to is specified between the less than (<) and greater than (>) signs. If you were to try to assign a different object that doesn’t follow the required protocol, the compiler will produce an error.

Protocols can have optional messages which can be specified by using the optional key word:

@protocol EventDataSource 
// The following three messages are required.
- (NSUInteger) totalHeadCount;
- (NSUInteger) numberOfChildren;
- (NSUInteger) numberOfAdults;
@optional
- (void) optionalMessage:(NSString*) message;
// Every thing that follows is optional until the @required keyword is used.
@required
- (void) requiredMessage:(NSString*) message;
// Every thing that follows is now required.
@end

Methods/messages that are marked as optional must be checked to see if an object implements it before calling it:

if([self.dataSource respondsToSelector:@selector(optionalMessage:)]) {
	[self optionalMessage:@"ponies"];
}

Following protocols is very straight forward, which uses the angle brackets. Multiple protocols can be listed within the brackets but are delimited by a coma.

@interface MyClass : NSObject <eventdatasource, anotherprotocol="">
…
@end

Android NavigationDrawer with Sliding Tabs

Both the NavigationDrawer and NavigationTabs are great for allowing a user to navigate your app. There are times that you may need to use them in conjunction and possible. However, you can not use the ActionBar Navigation Drawer and ActionBar Navigation Tabs together. In order to use tabs, you must implement them manually in your fagments. 

NavigationDrawer

Google has documented how to implement the NavigationDrawer here. Download the sample application found on the documentation page because there are some files that you will need:

  • res/drawable/xhdpi/ic_drawer.png
  • res/drawable/xhdpi/drawer_shadow.9.png
  • res/values/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">DrawerNavigationTabs</string>
    <string name="action_settings">Settings</string>
    <string name="hello_world">Hello world!</string>

    <string-array name="drawer_titles">
        <item>Tabs</item>
        <item>WebView</item>
    </string-array>

    <string name="drawer_open">Open navigation drawer</string>
    <string name="drawer_close">Close navigation drawer</string>
    <string name="title_section1">Section 1</string>
    <string name="title_section2">Section 2</string>
    <string name="title_section3">Section 3</string>


</resources>

<!-- A DrawerLayout is intended to be used as the top-level content view using match_parent for both width and height to consume the full space available. -->
<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- As the main content view, the view below consumes the entire
         space available using match_parent in both dimensions. -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- android:layout_gravity="start" tells DrawerLayout to treat
         this as a sliding drawer on the left side for left-to-right
         languages and on the right side for right-to-left languages.
         The drawer is given a fixed width in dp and extends the full height of
         the container. A solid background is used for contrast
         with the content view. -->
    <ListView
        android:id="@+id/left_drawer"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="#111"/>
</android.support.v4.widget.DrawerLayout>

Most of the time, a ListView will be enough to accomplish what you want to display in the drawer.

package com.paulusworld.drawernavigationtabs;

import android.os.Bundle;
import android.content.res.Configuration;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends FragmentActivity {

	private static final String TAG = MainActivity.class.getSimpleName();
	
	private DrawerLayout mDrawerLayout;
	private ListView mDrawerList;
	private ActionBarDrawerToggle mDrawerToggle;
	
	private CharSequence mDrawerTitle;
	private CharSequence mTitle;
	private String[] mDrawerItmes;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		mTitle = mDrawerTitle = getTitle();
		
		mDrawerItmes = getResources().getStringArray(R.array.drawer_titles);
		mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
		mDrawerList = (ListView) findViewById(R.id.left_drawer);
		
		// set a custom shadow that overlays the main content when the drawer opens
		mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow,  GravityCompat.START);
		
		// Add items to the ListView
		mDrawerList.setAdapter(new ArrayAdapter<String>(this, R.layout.drawer_list_item, mDrawerItmes));
		// Set the OnItemClickListener so something happens when a 
		// user clicks on an item.
		mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
		
		// Enable ActionBar app icon to behave as action to toggle the NavigationDrawer
		getActionBar().setDisplayHomeAsUpEnabled(true);
		getActionBar().setHomeButtonEnabled(true);
		
		mDrawerToggle = new ActionBarDrawerToggle(
				this, 
				mDrawerLayout, 
				R.drawable.ic_drawer, 
				R.string.drawer_open, 
				R.string.drawer_close
				) {
			public void onDrawerClosed(View view) {
				getActionBar().setTitle(mTitle);
				invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu
			}
			
			public void onDrawerOpened(View drawerView) {
				getActionBar().setTitle(mDrawerTitle);
				invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu
			}
		};
		
		mDrawerLayout.setDrawerListener(mDrawerToggle);
		
		// Set the default content area to item 0
		// when the app opens for the first time
		if(savedInstanceState == null) {
			navigateTo(0);
		}
	
	}
	
	/*
	 * If you do not have any menus, you still need this function
	 * in order to open or close the NavigationDrawer when the user 
	 * clicking the ActionBar app icon.
	 */
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		if(mDrawerToggle.onOptionsItemSelected(item)) {
			return true;
		}
		
		return super.onOptionsItemSelected(item);
	}
	
	/*
	 * When using the ActionBarDrawerToggle, you must call it during onPostCreate()
	 * and onConfigurationChanged()
	 */
	
	@Override
	protected void onPostCreate(Bundle savedInstanceState) {
		super.onPostCreate(savedInstanceState);
		mDrawerToggle.syncState();
	}
	
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		mDrawerToggle.onConfigurationChanged(newConfig);
	}
	
	private class DrawerItemClickListener implements OnItemClickListener {
		@Override
		public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
			navigateTo(position);
		}
	}
	
	private void navigateTo(int position) {
		
		switch(position) {
		case 0:
			getSupportFragmentManager()
				.beginTransaction()
				.replace(R.id.content_frame, TabbedFragment.newInstance(), TabbedFragment.TAG).commit();
			break;
		case 1:
			getSupportFragmentManager()
				.beginTransaction()
				.replace(R.id.content_frame,
						WebViewFragment.newInstance(),
						WebViewFragment.TAG).commit();
			break;
		}
        mDrawerLayout.closeDrawer(mDrawerList);
	}
	
	@Override
	public void setTitle(CharSequence title) {
		mTitle = title;
		getActionBar().setTitle(mTitle);
	}
	
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
package com.paulusworld.drawernavigationtabs;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;

public class WebViewFragment extends Fragment {

	public final static String TAG = WebViewFragment.class.getSimpleName();
	
	public WebViewFragment() {
		// TODO Auto-generated constructor stub
	}

	public static WebViewFragment newInstance() {
		return new WebViewFragment();
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setRetainInstance(true);
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		return inflater.inflate(R.layout.fragment_webview, container, false);
	}
	
	@Override
	public void onViewCreated(View view, Bundle savedInstanceState) {
		super.onViewCreated(view, savedInstanceState);
		WebView webView = (WebView) view.findViewById(R.id.webView);
		webView.loadUrl("http://www.paulusworld.com");
	}
}

The easiest way to accomplish adding a tabbed activity or fragment in this case, is to create a new activity and selecting the type of navigation you want. In this case, I've chosen Scrollable Tabs + Swipe. Once the activity was created, I had to modify from a FragmentActivity to a Fragment:

  • Remove the <activity> tag for the newly added Activity in the AndroidManifest.xml file
  • Change the onCreate() function to what you see here.
  • Add the onCreateView() function
  • Updated the names to classes and layouts.
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TabbedFragment" >

    <!--
    This title strip will display the currently visible page title, as well as the page
    titles for adjacent pages.
    -->

    <android.support.v4.view.PagerTitleStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#33b5e5"
        android:paddingBottom="4dp"
        android:paddingTop="4dp"
        android:textColor="#fff" />

</android.support.v4.view.ViewPager>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".TabbedActivity$DummySectionFragment" >

    <TextView
        android:id="@+id/section_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>
package com.paulusworld.drawernavigationtabs;

import java.util.Locale;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class TabbedFragment extends Fragment {

	public static final String TAG = TabbedFragment.class.getSimpleName();
	SectionsPagerAdapter mSectionsPagerAdapter;
	ViewPager mViewPager;

	
	public static TabbedFragment newInstance() {
		return new TabbedFragment();
	}
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		View v = inflater.inflate(R.layout.fragment_tabbed, container, false);
		mSectionsPagerAdapter = new SectionsPagerAdapter(
				getChildFragmentManager());
		
		mViewPager = (ViewPager) v.findViewById(R.id.pager);
		mViewPager.setAdapter(mSectionsPagerAdapter);
		
		return v;
	}
	
	public class SectionsPagerAdapter extends FragmentPagerAdapter {

		public SectionsPagerAdapter(FragmentManager fm) {
			super(fm);
		}

		@Override
		public Fragment getItem(int position) {
			Fragment fragment = new TabbedContentFragment();
			Bundle args = new Bundle();
			args.putInt(TabbedContentFragment.ARG_SECTION_NUMBER, position + 1);
			fragment.setArguments(args);
			return fragment;
		}

		@Override
		public int getCount() {
			return 3;
		}

		@Override
		public CharSequence getPageTitle(int position) {
			Locale l = Locale.getDefault();
			switch (position) {
			case 0:
				return getString(R.string.title_section1).toUpperCase(l);
			case 1:
				return getString(R.string.title_section2).toUpperCase(l);
			case 2:
				return getString(R.string.title_section3).toUpperCase(l);
			}
			return null;
		}
	}

	public static class TabbedContentFragment extends Fragment {

		public static final String ARG_SECTION_NUMBER = "section_number";

		public TabbedContentFragment() {
		}

		@Override
		public View onCreateView(LayoutInflater inflater, ViewGroup container,
				Bundle savedInstanceState) {
			View rootView = inflater.inflate(R.layout.fragment_tabbed_content,
					container, false);
			TextView dummyTextView = (TextView) rootView
					.findViewById(R.id.section_label);
			dummyTextView.setText(Integer.toString(getArguments().getInt(
					ARG_SECTION_NUMBER)));
			return rootView;
		}
	}

}
File Attachment: 

Pages