Senin, 26 Mei 2014

Making a Flashlight

    I'm pretty sure you can't call yourself an Android developer without making at least one flashlight app, right? Let's just go ahead and get that out of the way then. The source code is available on GitHub.

    The first thing that needs to be done is to declare permissions in the manifest

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

    I also modify the activity to handle configuration changes so that the activity doesn't destroy itself on rotate. This prevents the flashlight from turning off and then back on when the device orientation is changed.

<activity
android:name="com.ptrprograms.flashlight.MainActivity"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

    The layout for this demo is a simple toggle button located in the center of the screen. That button is initialized in MainActivity, and the click listener verifies that the device has the camera flash feature.

private void initFlashlightButton() {
ToggleButton button = (ToggleButton) findViewById( R.id.button_flashlight );
button.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
if( getPackageManager().hasSystemFeature( PackageManager.FEATURE_CAMERA_FLASH ) )
{
if( mFlashlightOn )
activateFlashlight();
else
deactivateFlashlight();
}
}
});
}

    The activateFlashlight method gets the parameter information from the device camera and adds a parameter to turn on the flash in torch mode, meaning the flash stays on until deactivated, rather than 'flashing.'

private void activateFlashlight()
{
if( mCamera == null )
mCamera = Camera.open();

mParameters = mCamera.getParameters();
mParameters.setFlashMode( Camera.Parameters.FLASH_MODE_TORCH );
mCamera.setParameters( mParameters );
mFlashlightOn = true;
}

    The deactivateFlashlight method does the same thing, but in reverse. Instead of setting the flash mode to FLASH_MODE_TORCH, it uses Camera.Parameters.FLASH_MODE_OFF.

private void deactivateFlashlight()
{
if( mCamera == null || mParameters == null )
return;

mParameters = mCamera.getParameters();
mParameters.setFlashMode( Camera.Parameters.FLASH_MODE_OFF );
mCamera.setParameters( mParameters );
mFlashlightOn = false;
}

    The final part to building a simple flashlight app is releasing the camera resource on destroy. This turns the light off when the app is exited.

@Override
protected void onDestroy() {
super.onDestroy();
if( mCamera == null )
return;

mCamera.release();
}

    And that's that, a simple flashlight app to add to the toolbox.
Read More..

Minggu, 25 Mei 2014

Using the Navigation Drawer with Otto

    Navigation is one of the most important things to consider when planning the architecture of an app. One type of navigation that has seen a good deal of success is the Navigation Drawer, officially released in the support library during the summer of 2013. The drawer is a swipeable section that can be populated with a view or a fragment, allowing for a lot of freedom when designing what sort of navigations you would want to include in your app, be it a standard list, a spinner, image buttons, etc. that can trigger events in your app, such as displaying a new fragment or activity.

Drawer with items for selecting a new fragment
    For this demo, I have put together a drawer with a list view that changes which fragment is displayed in the main activity on click. These navigation clicks fire events through Otto, a third party event bus library from Square, that lets the main activity know what fragment should be swapped in. All of the code for this working example can be found on GitHub.

    To start, you should declare the DrawerLayout widget in your activity layout file as the root element.

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

<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<com.ptrprograms.navigationdrawer.views.DrawerListView
android:id="@+id/drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/black"
android:dividerHeight="0dp"
android:background="@android:color/background_light" />

</android.support.v4.widget.DrawerLayout>

    The first child element within the drawer layout will be the main layout for your activity, while the second element will be the item within the drawer. In this case I am using a custom ListView. The width should be set to a specific value, such as 240dp, so that it only expands to a certain point on the device screen. The gravity and height for the drawer item should also be set to start and match_parent, respectively, as the drawer by convention should be on the left side of the screen. The other xml properties I have included in my DrawerListView element are simply style specific.

    Now that the layout for the drawer activity is set up, we need to programmatically configure the drawer to work in our main activity. The first step is to enable the home button on the action bar with these two commands:

getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);

    It should be noted that for this demo I am using the ActionBarCompat support classes, but getSupportActionBar() can be changed to getActionBar() for projects supporting Android 3.0+. Next we need to create the ActionBarDrawerToggle and listener for when the drawer opens and closes:

private void initDrawer() {
mDrawerToggle = new ActionBarDrawerToggle( this, mDrawerLayout,
R.drawable.ic_navigation_drawer, R.string.drawer_open_title, R.string.drawer_close_title ) {

@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
if( getSupportActionBar() == null )
return;

getSupportActionBar().setTitle( R.string.drawer_close_title );
invalidateOptionsMenu();
}

@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
if( getSupportActionBar() == null )
return;

getSupportActionBar().setTitle( R.string.drawer_open_title );
invalidateOptionsMenu();
}
};

mDrawerLayout.setDrawerListener(mDrawerToggle);

}


    The toggle is created with a context, the DrawerLayout object, a drawable for the action bar top left icon (generally a hamburger icon, and can be generated through Android Asset Studio), and a set of action bar titles to display when the drawer is open or closed. ActionBarDrawerToggle also acts as a listener that supports onDrawerClosed and onDrawerOpened. This is where you can set the action bar titles and hide or show any menu items in the action bar.

Application with closed drawer title and extended hamburger icon (dark purple, top left)
    Two more functions must be added to the activity in order to ensure that the drawer stays open or closed on configuration changes, such as rotating the device:

@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mDrawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}

    The final part of our main activity is a method that uses Otto to subscribe to particular events on the event bus. A separate method is then declared with a @Subscribe declaration to listen for a DrawerNavigationItemClickedEvent. The event has a String value called section that is used for selecting what fragment to display next. Once the fragment is selected and displayed, the drawer is programmatically closed.

@Subscribe
public void onDrawerNavigationClickedEvent( DrawerNavigationItemClickedEvent event ) {
if( !mCurFragmentTitle.equalsIgnoreCase(event.section) ) {
if (getString(R.string.fragment_image).equalsIgnoreCase(event.section)) {
getSupportFragmentManager().beginTransaction().replace(R.id.container, ImageFragment.getInstance()).commit();
} else if (getString(R.string.fragment_text).equalsIgnoreCase(event.section)) {
getSupportFragmentManager().beginTransaction().replace(R.id.container, TextFragment.getInstance()).commit();
} else if (getString(R.string.fragment_number_list).equalsIgnoreCase(event.section)) {
getSupportFragmentManager().beginTransaction().replace(R.id.container, NumberListFragment.getInstance()).commit();
}
mCurFragmentTitle = event.section;
}
mDrawerLayout.closeDrawers();
}

    Now that the main activity is set up, let's go over Otto and how it's used to control navigation through the drawer. In the main activity's onCreate method we receive an instance of our navigation bus, which is created using a singleton pattern, and register it in onStart. This is the NavigationBus singleton class:

public class NavigationBus extends Bus {
private static final NavigationBus navigationBus = new NavigationBus();

public static NavigationBus getInstance() {
return navigationBus;
}

private NavigationBus() {

}

}

    In the custom ListView for the drawer, an OnItemClickListener is implemented and the title of the clicked item is pasted in an event over the NavigationBus.

@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
String drawerText = ( (DrawerItem) adapterView.getAdapter().getItem( position ) ).getDrawerText();
NavigationBus.getInstance().post( new DrawerNavigationItemClickedEvent( drawerText ) );
}

    The DrawerNavigationItemClickedEvent is the same one that is listened for in our main activity, and that event is declared like so:

public class DrawerNavigationItemClickedEvent {

public String section;

public DrawerNavigationItemClickedEvent( String section ) {
this.section = section;
}

}

    As can be seen here, Otto allows an application to fire events from any component and catch it in any other component that is listening for that specific event. This makes things such as navigation and passing data from dialogs incredibly simple, saving development time and headaches.

    And with that, we now have a working navigation drawer that allows for changing fragments in our application and providing a proper drawer user experience. If there are any questions, please feel free to comment, otherwise I hope this tutorial is helpful for other developers out there.
Read More..

Sabtu, 24 Mei 2014

Creating a Native Video Player Activity

    One of the major uses for mobile devices is to watch videos. While YouTube is great for this, not all content is posted there. Given that sometimes proprietary online content will need to be played, I decided to make a simple native video player to play content from a URL. All code for this demo is available on GitHub.

Video player with an online video in landscape orientation
    The MainActivity for this application is a simple button that, when pressed, launches an intent with a URL for the native video player activity.

private void launchVideoPlayer() {
Intent i = new Intent( this, VideoPlayerActivity.class );
i.putExtra( VideoPlayerActivity.EXTRA_VIDEO_URL, "http://www.pocketjourney.com/downloads/pj/video/famous.3gp" );
startActivity( i );
}

    The layout for the video player activity consists of a VideoView and an indeterminate ProgressBar spinner. The spinner is shown while the video is buffered.

Loading spinner for the video player
    When the video player is created, MediaPlayer listeners and the URL that was passed through the intent are attached to the VideoView. 

mVideoView = (VideoView) findViewById( R.id.video_view );
mVideoView.setOnCompletionListener( onCompletionListener );
mVideoView.setOnErrorListener( onErrorListener );
mVideoView.setOnPreparedListener( onPreparedListener );

if( mVideoView == null ) {
throw new IllegalArgumentException( "Layout must contain a video view with ID video_view" );
}

mUri = Uri.parse( getIntent().getExtras().getString( EXTRA_VIDEO_URL ) );
mVideoView.setVideoURI( mUri );

    A MediaController object is then created to add playback controls for the VideoView.

mMediaController = new MediaController( this );
mMediaController.setEnabled( true );
mMediaController.show();
mMediaController.setMediaPlayer( mVideoView );

    The OnCompletionListener resets the video to its initial position, pauses it and shows the playback controls.

protected MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
mVideoView.seekTo( 0 );
if( mVideoView.isPlaying() )
mVideoView.pause();

if( !mMediaController.isShowing() )
mMediaController.show();
}
};

    The OnPreparedListener is triggered when the video URL has been found and buffered. This is where the progress spinner is hidden and the media controller is attached to the VideoView, and the video is started.

protected MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
if( mediaPlayer == null )
return;
mMediaPlayer = mediaPlayer;
mediaPlayer.start();
if( mSpinningProgressBar != null )
mSpinningProgressBar.setVisibility( View.GONE );

mVideoView.setMediaController( mMediaController );
}
};

    Finally the OnErrorListener simply shows an AlertDialog that closes the video activity when the dialog is closed. Once the basic infrastructure is put together for playing the remote video, handling rotation and leaving/coming back to the app should be considered. In regards to hitting home and coming back to the app, the onPause and onResume methods are used to save the video position, restore it and set the spinner to visible. Given that the OnPreparedListener is still attached to the video view, that listener will be triggered when the video is ready to be played again and the app can continue as normal.

@Override
protected void onResume() {
super.onResume();
mVideoView.seekTo( mPosition );
mSpinningProgressBar.setVisibility( View.VISIBLE );
}

@Override
protected void onPause() {
super.onPause();
mPosition = mVideoView.getCurrentPosition();
}

    For device rotation, we take advantage of the activity lifecycle and onSaveInstanceState and onRestoreInstanceState to save whether the video is playing and its position on rotation.

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);

if( mVideoView == null || mVideoView.getCurrentPosition() == 0 )
return;

outState.putInt( EXTRA_CURRENT_POSITION, mVideoView.getCurrentPosition() );
outState.putBoolean( EXTRA_IS_PLAYING, mVideoView.isPlaying() );
mVideoView.pause();
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if( mVideoView == null || savedInstanceState == null )
return;

if( savedInstanceState.getBoolean( EXTRA_IS_PLAYING, false ) ) {
mVideoView.seekTo(savedInstanceState.getInt(EXTRA_CURRENT_POSITION, 0));
mVideoView.start();
}
}

Video in portrait orientation
    And with that, we have a simple native video player for remote content using native controls and some versatility. There's still a whole lot more that can be done with the video player, such as customizing UI controls, that I haven't touched on, but can add a good deal of polish to any media app.
Read More..

Senin, 19 Mei 2014

To wrap up the current set of posts about Android notifications, I will go over the Developers Preview for Wear. Wear is a standard Android based OS for wearable devices, namely wristwatches in this early stage, that will hopefully be expanded to other devices in the near future. More information on what Wear is can be found on Google's site.

That's cool and all, but what are the capabilities of Wear? So far the developer preview only allows for notifications to be sent to the watch emulator, and for some predefined interactions. Google has shown some currently unavailable features such as speech to text, and Wear is able to send messages back to an Android device through the use of lock screen remote view buttons (a subject deserving of its own post that I hope to get to) and intents. Given that a wristwatch device can trigger events on a phone, the possibilities become endless when matches up with cloud services or additional external hardware, such as Android Open Accessories.

Remote View Notification Button for controlling an audio service (awesome, right?)
The first thing that needs to be done in order to start using the Wear preview is to sign up here. Once that's all set, you'll receive an email within a day or two letting you know if you're in the beta, and where to download the Android Wear Preview app. Once the app is installed, go into the Android SDK manager and make sure you're using the latest Support Library, then create an Android Wear emulator using API 19+. You'll also need to download the wearable support library jar from Google (though it's also in the libs folder in the source code for this post). When that's done, connect your Android device to your computer through USB and in a terminal (CMD prompt for you Windows folks) type the following from your SDK platform-tools folder:

adb -d forward tcp:5601 tcp:5601

Assuming you opened the preview app and turned on allowing Wear to receive notifications, then we're ready to move on to the fun part of this post! As with my other posts, all of the source that I'll be talking about is available on GitHub.

All Wear notifications are essentially normal notification builder wrapped by WearableNotifications.Builder, which can then have additional features added to them. It should be noted that the original notification builder must use the Android support library version: NotificationCompat.Builder. Here is the code for wrapping the standard Builder with the WearNotifications Builder:

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( getActivity() )
.setSmallIcon( R.drawable.ic_launcher )
.setLargeIcon( BitmapFactory.decodeResource( getResources(), R.drawable.batman_punching_shark ) )
.setContentText( getString( R.string.big_content_summary ) )
.setContentTitle( getString( R.string.notification_basic ) );

Notification notification =
new WearableNotifications.Builder( notificationBuilder )
.setHintHideIcon(true)
.build();


mNotificationManager.notify( notificationId, notification );

Basic Android Wear Notification
Now that we're able to show notifications on Wear, let's move on to something a bit more useful: sending intents from Wear actions. This uses the same method as a standard Android notification for sending an intent: addAction. By calling addAction with a PendingIntent on the NotificationCompat.Builder, an additional screen is added to the Wear notification that sends the intent when clicked. In this example, the intent is a standard ACTION_VIEW that opens a browser to my blog.

        Intent intent = new Intent( Intent.ACTION_VIEW );
intent.setData( Uri.parse( "http://ptrprograms.blogspot.com" ) );
PendingIntent pendingIntent = PendingIntent.getActivity( getActivity(), 0, intent, 0 );

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( getActivity() )
.setSmallIcon( R.drawable.ic_launcher )
.setLargeIcon( BitmapFactory.decodeResource( getResources(), R.drawable.batman_punching_shark ) )
.setContentText( getString( R.string.big_content_summary ) )
.setContentTitle( getString( R.string.notification_add_action ) )
.addAction( R.drawable.ic_launcher, "Launch Blog", pendingIntent );

Notification notification =
new WearableNotifications.Builder( notificationBuilder )
.setHintHideIcon( true )
.build();

mNotificationManager.notify( notificationId, notification );

Notification Action Item
The next kind of notification is called Quick Reply. By creating a RemoteInput object with a Strings array, and then calling addRemoteInputForContentIntent when building the WearableNotification, you can provide a list of up to five items to allow the user to easily respond to a notification.

        Intent intent = new Intent( getActivity(), MainActivity.class );
PendingIntent pendingIntent = PendingIntent.getActivity( getActivity(), 0, intent, 0 );

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( getActivity() )
.setSmallIcon( R.drawable.ic_launcher )
.setLargeIcon( BitmapFactory.decodeResource( getResources(), R.drawable.batman_punching_shark ) )
.setContentText( getString( R.string.big_content_summary ) )
.setContentTitle( getString( R.string.notification_quick_replies ) )
.setContentIntent( pendingIntent );

String replyLabel = "Transportation";
String[] replyChoices = getResources().getStringArray( R.array.getting_around );

RemoteInput remoteInput = new RemoteInput.Builder( "extra_replies" )
.setLabel(replyLabel)
.setChoices(replyChoices)
.build();

Notification notification =
new WearableNotifications.Builder( notificationBuilder )
.setHintHideIcon( true )
.addRemoteInputForContentIntent( remoteInput )
.build();

mNotificationManager.notify( notificationId, notification );
Action for Quick Replies
Quick Replies
The next kind of Wear notification uses multiple pages to present information to the user. The first page notification is built like the other notifications by constructing the Notification builder, and then additional notification pages can be constructed and built with optional styles.

        NotificationCompat.BigTextStyle additionalPageStyle = new NotificationCompat.BigTextStyle();
additionalPageStyle.setBigContentTitle( "Page 2" );

Notification secondPageNotification =
new NotificationCompat.Builder( getActivity() )
.setStyle( additionalPageStyle )
.build();

The additional notifications are then added to a List of Notification objects, and added to the WearableNotification during construction with the .addPages( list ) method.

Page 2 of 4

The final type of notification for Android Wear are stackable notifications. These notifications are created by building WearNotifications, like the other examples, but with the additional .setGroup method with a standardized tag as the first parameter and a stack id value for the second parameter that must be different for each notification. At least one of the notifications should be a summary notification with an id of WearableNotifications.GROUP_ORDER_SUMMARY. Once all of these notifications are built, they can be posted using the NotificationManager.

Notification notification2 =
new WearableNotifications.Builder( notificationBuilder )
.setHintHideIcon(true)
.setGroup( EXTRA_STACKED_GROUP, ++stackedId )
.build();

Notification summaryNotification = new WearableNotifications.Builder( notificationBuilder )
.setGroup( EXTRA_STACKED_GROUP, WearableNotifications.GROUP_ORDER_SUMMARY )
.build();


Assuming Android Wear stays similar on release, it should be pretty straight forward and easy to integrate into any app, and the additional features of voice replies will make it even more useful.

Read More..

Minggu, 27 April 2014

Since Android Jelly Bean, notifications have had the ability to be expanded into a larger view with a custom layout. One of the most common uses for this comes from media applications, such as Pandora and YouTube, that allow the user to interact with buttons in the custom layout in order to control their media services without being in the app. Given the usefulness of this technique, I have put together a demo service that creates a notification with a custom layout containing buttons, and added listeners for those buttons in order to call functions within the service. As with all of my other posts, the demo project can be found on my GitHub account here.

The entry point for this demo (MainActivity) uses a simple layout containing a single button that creates an intent with an action to start the background service that will be doing all of the work in our example.

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.activity_main );

mLaunchNotificationButton = (Button) findViewById( R.id.launch_notification );
mLaunchNotificationButton.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ) {
Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
intent.setAction( CustomNotificationService.ACTION_NOTIFICATION_PLAY_PAUSE );
startService( intent );
}
});
}

Once the intent for the service is fired, onStartCommand is called in the service, which is where I pass the intent to a function that handles filtering out the action and calling the appropriate methods.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent( intent );
return super.onStartCommand(intent, flags, startId);
}

private void handleIntent( Intent intent ) {
if( intent != null && intent.getAction() != null )
{
if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_PLAY_PAUSE ) )
{
mIsPlaying = !mIsPlaying;
showNotification(mIsPlaying);
} else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_FAST_FORWARD ) )
{
//fast forward function
} else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_REWIND ) )
{
//rewind action
}
}
}

If the action is to play or pause the service, then the service would perform these actions and then display a new notification with the updated UI from the showNotification function.

private void showNotification( boolean isPlaying ) {
Notification notification = new NotificationCompat.Builder( getApplicationContext() )
.setAutoCancel( true )
.setSmallIcon( R.drawable.ic_launcher )
.setContentTitle( getString( R.string.app_name ) )
.build();

if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
notification.bigContentView = getExpandedView( isPlaying );

NotificationManager manager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
manager.notify( 1, notification );
}

The code here follows the same convention as my previous post for standard notifications, with the exception of the Jelly Bean code to create a bigContentView Remote Views. The code for pre-Jelly Bean devices creates a notification that looks like this:


The getExpandedView function returns a RemoteViews object that consists of an inflated custom view that has PendingIntents associated with each of the buttons that will be sent to the existing service in order to control the operations within with the service. The layout is fairly standard with a fixed size of 128dp and three buttons under some custom text:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_expanded_height">

<ImageView
android:id="@+id/large_icon"
android:layout_width="@dimen/notification_expanded_height"
android:layout_height="@dimen/notification_expanded_height"
android:scaleType="centerCrop"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
/>
<LinearLayout
android:id="@+id/buttons_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/large_icon"
android:orientation="horizontal">

<ImageButton
android:id="@+id/ib_rewind"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />

<ImageButton
android:id="@+id/ib_play_pause"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />

<ImageButton
android:id="@+id/ib_fast_forward"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />
</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/app_name"
android:textSize="@dimen/notification_text_size"
android:layout_gravity="center"
android:gravity="center"
android:layout_toRightOf="@+id/large_icon"
android:layout_above="@+id/buttons_row"/>

</RelativeLayout>

which in turn looks like this when it is created:


In the getExpandedView method, each image in the notification is set using the setImageViewResource method

customView.setImageViewResource( R.id.ib_rewind, R.drawable.ic_rewind );

and each button has a pending intent with action associated with it

Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
intent.setAction( ACTION_NOTIFICATION_PLAY_PAUSE );
PendingIntent pendingIntent = PendingIntent.getService( getApplicationContext(), 1, intent, 0 );
customView.setOnClickPendingIntent( R.id.ib_play_pause, pendingIntent );

Since each of these pending intents goes back to the service, they are filtered through the handleIntent method and actions can be carried out based on the button clicks from this notification. Since the notification and operations are controlled through a service, the notification can control media when the app has been closed out or the Android device is locked. As this demo is not using an actual audio service to determine what controls should be shown, the mIsPlaying flag is set and passed to the custom view creation method in order to show either the play or pause button.


And that's how simple it is to create a custom expanded view with buttons! 

Read More..

Senin, 14 April 2014

Notifications Part 1: Introduction

One of the most useful techniques for any developer's mobile toolkit is building notifications. They allow you to quickly get information to your user, bring them into your app, provide controls for media and do a variety of other pretty cool things. They are also the basis for interacting with the new Google Wear hardware. The first part of my notification posts will go over the basics of using the NotificationCompat builder, which is compatible back to Android v4, to display a notification in the status drawer, enable vibrations, show icons and fire an intent to open a specified activity. As with the other posts, all source code for this project can be found on GitHub.

First and foremost, the demo project that I made allows the user to populate different information and enable different features in a notification, as seen here:


The title, content text, subtext and content info are straight forward and demonstrated here:


 and the ticker text is the text that is displayed in the status bar when the notification comes in:


Notifications are created using the builder pattern with NotificationCombat.Builder. Each characteristic is then added to the notification through a series of functions, followed by returning the built notification to the NotificationManager. An example notification can be built as simply as this:

NotificationCompat.Builder builder = new NotificationCompat.Builder( this );
builder.setContentTitle(getString(R.string.app_name));
builder.setContentText(mNotificationTextEditText.getText());
builder.setSmallIcon( R.drawable.ic_launcher );
NotificationManager manager =
(NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
manager.notify( 1, builder.build() );

where the '1' in the notify function is an int that can be incremented to stack notifications, or use the same number to replace any currently active notifications from your activity.


When only the small icon is set, it fills the roll of the large icon on the left. If both the large and small image are defined, then the large image is the left image, and the image next to the content info is the small image. 

Notification sounds can be triggered using the builder.setSound method. Unless you have a compelling reason, you should use the notification sound defined by the user if your notification is to be audible.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

Vibrations can also be set for the notification using the builder.setVibrate method. This method takes an array of longs where every even index is the number of milliseconds that the device should not vibrate, and every odd index is the number of milliseconds that the device should vibrate. In the demo, the notification will vibrate for half of a second, pause for a quarter of a second, and then vibrate a second time for half of a second. The 0 at index 0 means that the vibration will start as soon as the notification is received, rather than waiting.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

One of the more useful features of Android notifications is that they can be set with an intent that allows the user to open an activity on click, and then the notification can be cleared from the notification drawer.

Intent intent = new Intent( this, MainActivity.class );
TaskStackBuilder stackBuilder = TaskStackBuilder.create( this );
stackBuilder.addNextIntent( intent );
PendingIntent resultIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent( resultIntent );
builder.setAutoCancel( true );

The time section of the notification can be overwritten to count the time since the notification posted by using the builder.setUsesChronometer method.


The last feature I want to go over that comes with basic notifications is the ability to use some predefined styles. In the demo project, I apply the Big Picture style and apply an image for the picture area

NotificationCompat.BigPictureStyle style = new NotificationCompat.BigPictureStyle();
style.bigPicture(BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher));
builder.setStyle(style);

This creates a notification that contains a large image under all of the standard information:


Aside from the basic features that I have just gone over, notifications allow for custom views that can contain items, such as buttons, to perform special actions. They can also be used with the new Google Wear hardware. I plan to go over these features in a later post, as notifications are one of the most powerful tools in the Android SDK.

Read More..

Minggu, 02 Maret 2014

Making a Gallery

    During my last summer of college, I had an interview for a possible internship that asked me how I would allow users to see a series of images quickly before allowing them to see a better quality one. Given that I knew a lot less then than I do now (which still isn't much :)), I gave an answer that described how it would work with a view pager, without knowing what a view pager was. While this was close, it didn't cover the 'quick' aspect and didn't cover any of the implementation details. Since I didn't get the job (which in hindsight is awesome, as I ended up in gorgeous Boulder, Colorado, with a great team rather than heading down to Los Angeles), I decided to at least work out how I would solve this problem.  As with all of my posts, the source code for this project is available on my GitHub.

    For this project I am going to use online images so that their URLs can be passed to the app from a JSON stream with some additional data. This JSON stream is then parsed using GSON into a Gallery model object that contains an ArrayList of Image objects. Each Image object contains a caption for the image, the URL for the higher resolution version of the image, and a thumbnail for the image. The thumbnails are displayed in a GridView, and the higher resolution images with captions are displayed in a separate activity with a ViewPager. The final product with very little styling (mind you, if this were to be a production app then more styling would be a necessity) will look like this:


    As can be seen in the first image, the main activity is simply a grid of images. Instead of using a standard ImageView, I have a SquareImageView class that takes the images and displays them in a square in order to keep the rows uniform. This is done by calling setMeasuredDimension to make the image height equal the width when the view calls onMeasure

SquareImageView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int dimension = getDefaultSize( getSuggestedMinimumWidth(), widthMeasureSpec );
setMeasuredDimension(dimension, dimension);
}

    The feed is pulled down in MainActivity using Volley and GSON to put the JSON data into the model objects:

MainActivity.java
private void loadFeed() {
String feedUrl = getString( R.string.feed_url );
GsonRequest<Gallery> request = new GsonRequest<Gallery>( Request.Method.GET,
feedUrl, Gallery.class, successListener(), errorListener() );
Volley.newRequestQueue( getApplicationContext() ).add( request );
}
public Response.Listener successListener()
{
return new Response.Listener<Gallery>()
{
@Override
public void onResponse( Gallery gallery ) {
mGallery.setImages( gallery.getImages() );
mGallery.setDescription( gallery.getDescription() );
setupUI();
}
};
}

   setupUI() adds all of the images from the gallery into an ArrayAdapter for the GridView, and adds a click listener to open an activity for viewing the full resolution images. The adapter for the grid uses the viewholder pattern and the Picasso library to load the thumbnail images into the grid, as can be seen in GalleryGridAdapter.java. When an image is clicked in the grid, all images are passed to the next activity as well as the clicked position, allowing the ImageDetailsActivity to display the ViewPager at the correct location

MainActivity.java
mGridView.setOnItemClickListener( new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int position, long id) {
Intent intent = new Intent( getApplicationContext(), ImageActivity.class );
intent.putExtra( ImageActivity.EXTRA_IMAGE_LIST,
(ArrayList) mGallery.getImages() );
intent.putExtra( ImageActivity.EXTRA_CUR_IMAGE, position );
startActivity( intent );
}

ImageDetailsActivity.java
if( getIntent() == null || getIntent().getExtras() == null )
return;

List<Image> tmpList = getIntent().getExtras()
.getParcelableArrayList( EXTRA_IMAGE_LIST );
mCurrentImagePosition = getIntent().getExtras().getInt( EXTRA_CUR_IMAGE, 0 );
mAdapter = new ImageStateViewPager( getSupportFragmentManager(), tmpList );

    ImageActivity takes the images and loads them into a FragmentStatePagerAdapter which returns a new instance of ImageDetailsFragment with passed Image object from getItem.

ImageStateViewPager.java
@Override
public Fragment getItem( int position ) {
return( position < 0 || position > ( mImageList.size() - 1 ) ) ? null :
ImageFragment.newInstance( mImageList.get( position ) );
}

    This fragment displays a progress spinner until Picasso has loaded the external image, and displays a caption that can have visibility toggled by tapping on the image. By using a ViewPager, the user is able to swipe left or right to view additional images while only keeping a max of three images in memory at a time.

    And with that, we have a simple gallery component for Android. There's a lot of modifications that can be made to this, such as adding progress spinners for the grid as the initial feed is loaded, and spinners for each image object as Picasso loads them into the grid, as well as styles to show overlays and change spacing in the grid. Overall the gallery is a useful component for displaying graphical content to your users, and makes a great addition to any Android developer's arsenal of UI components.
Read More..