Custom Notification
With Android 3.0 “Honeycomb” new notification have have become customizable. This allows to display rich content within the notification panel. While this is of course one of the features to be used with care — not to overload the GUI — it can be quite useful.
There are two two options open:
-
Designing a
android.widget.RemoteViews
to display the notification data. This is done by callingandroid.app.Notification.Builder.setContent
. (available since Android 3.0). -
Use a notification style (
android.app.Notification.Style
) to create a view which can switch between a standard views and expanded view (available since Android 4.2).
After evaluating them both — including some try error and abandon what does not work at all my suggestions are:
-
If your data fits in any of the new styles use them. Preferably using
android.support.v4.app.NotificationCompat.Builder
so backward compatibility is ensured. -
If it does not fit into one of the new styles then don’t try create a custom style by sub-classing
android.app.Notification.Style
. To much of the class, its factory companionandroid.app.Notification.Builder
as well as the associated resources are declared private. So you eventually end up reimplementing the whole notification styles framework. -
If you wish to create a true custom notification then create an
android.widget.RemoteViews
.
In rest of the Article I show how you create a custom notification which can display two lines of text — instead of just one line like it is possible with the normal notification.
Layout
As always the first thing you need is a layout. Remember that a remote-view can not use very few selected layouts type. I have chosen a RelativeLayout which quite flexible without the need of nested layouts.
You want your notification to fit into the overall system. Fro this you have to make sure that certain elements are at certain places, use specific sizes and android:ids. I could explain it all but I think it is better offer a full working sample:
1 xml version="1.0" encoding="utf-8"?> 2 3 14 15 <RelativeLayout 16 android:background='@drawable/notification_bg' 17 android:layout_height='wrap_content' 18 android:layout_width='match_parent' 19 internal:layout_maxHeight='unbounded' 20 internal:layout_minHeight='65dp' 21 tools:ignore='NewApi,UnusedResources' 22 xmlns:android='http://schemas.android.com/apk/res/android' 23 xmlns:internal='http://schemas.android.com/apk/prv/res/android' 24 xmlns:tools='http://schemas.android.com/tools' 25 > 26 <ImageView 27 android:background='@drawable/notification_template_icon_bg' 28 android:contentDescription='@string/Two_Line_Style_Icon' 29 android:id='@android:id/icon' 30 android:layout_alignParentTop='true' 31 android:layout_height='@android:dimen/notification_large_icon_height' 32 android:layout_marginRight='@dimen/Horizontal_Padding' 33 android:layout_width='@android:dimen/notification_large_icon_width' 34 android:scaleType='center' 35 >ImageView> 36 <TextView 37 android:ellipsize='marquee' 38 android:fadingEdge='horizontal' 39 android:id='@android:id/title' 40 android:layout_alignParentEnd='false' 41 android:layout_alignParentTop='true' 42 android:layout_gravity='top' 43 android:layout_height='wrap_content' 44 android:layout_marginRight='@dimen/Horizontal_Padding' 45 android:layout_marginTop='@dimen/Vertical_Padding' 46 android:layout_toRightOf='@android:id/icon' 47 android:layout_width='match_parent' 48 android:singleLine='true' 49 android:text='@string/Dummy' 50 android:textAppearance='@android:style/TextAppearance.StatusBar.EventContent.Title' 51 >TextView> 52 <TextView 53 android:id='@android:id/text1' 54 android:layout_below='@android:id/title' 55 android:layout_height='wrap_content' 56 android:layout_marginRight='@dimen/Horizontal_Padding' 57 android:layout_toRightOf='@android:id/icon' 58 android:layout_width='match_parent' 59 android:text='@string/Dummy' 60 android:textAppearance='@android:style/TextAppearance.StatusBar.EventContent' 61 style='style/Big_Text' 62 >TextView> 63 <TextView 64 android:id='@android:id/text2' 65 android:layout_below='@android:id/text1' 66 android:layout_height='wrap_content' 67 android:layout_marginRight='@dimen/Horizontal_Padding' 68 android:layout_toRightOf='@android:id/icon' 69 android:layout_width='match_parent' 70 android:text='@string/Dummy' 71 android:textAppearance='@android:style/TextAppearance.StatusBar.EventContent' 72 style='style/Big_Text' 73 >TextView> 74 RelativeLayout>
It is good praxis not to include literal sizes in your layout — with the exception of the mythical and undocumented internal:layout_maxHeight and internal:layout_minHeight. So here the dimensions you need to make this layout working:
1 12 <resources> 13 <dimen name='Big_Text_Size'>16spdimen> 14 <dimen name='Horizontal_Padding'>8dpdimen> 15 <dimen name='Vertical_Padding'>2dpdimen> 17 resources>
1 13 14 <resources> 15 <style parent='@android:style/Widget.TextView' name='Big_Text'> 16 <item name='android:singleLine'>trueitem> 17 <item name='android:textSize'>@dimen/Big_Text_Sizeitem> 18 <item name='android:typeface'>monospaceitem> 19 <item name='android:maxLines'>1item> 20 <item name='android:ellipsize'>enditem> 21 style> 42 resources>
A last little trick of me: Fill in some dummy text into every display field which would normally be empty at application start. That you can evaluate the design better in the Android-Designer or Preview windows. And yes, I fill in the description field for bitmaps as well — as prescribed by the accessibility (for the disabled) guidelines.
1 10 <resources xmlns:android="http://schemas.android.com/apk/res/android"> 11 <string name = "Dummy">«Dummy»string> 12 <string name = "Two_Line_Style_Icon">iconstring> 19 resources>
Programm Code
In our case coding could not be simpler. Notification are not dynamically updated but replaced with a new notification. Which means that there is no need for anything but a constructor:
1 /* 2 * ********************************************************* {{{1 ************ 3 * Copyright © 2013 … 2013 Noser Engineering 10 * ********************************************************** }}}1 *********** 11 */ 12 13 package com.noser.ui; 14 15 import org.jetbrains.annotations.NotNull; 16 17 /** 18 * 19 * Notification layout to display two lines of results 20 * 21 * 22 * @authormartin.krischik 23 * @version 2.6 24 * @since 2.6 25 */ 26 @android.annotation.TargetApi (11) 27 public class TwoLineNotification 28 extends 29 android.widget.RemoteViews 30 { 31 /** 32 * 33 * TAG as class name for logging 34 * 35 */ 36 private static final String TAG; 37 38 static 39 { 40 TAG = TwoLineNotification.class.getName (); 41 } // static 42 43 /** 44 * Create a new RemoteViews object that will display the views contained in the specified 45 * layout file. 46 * 47 * @parampackageName 48 * Name of the package that contains the layout resource 49 * @paramlayoutId 50 * The id of the layout resource 51 * @paramtitle 52 * Notification title text 53 * @paramtext 54 * 2 line of text to display 55 * @paramiconId 56 * ID of the notification icon. 57 */ 58 public TwoLineNotification ( 59 @NotNull final String packageName, 60 final int layoutId, 61 @NotNull final CharSequence title, 62 @NotNull final CharSequence[] text, 63 final int iconId) 64 { 65 super (packageName, layoutId); 66 74 com.noser.Required.range (text, 2, 2); 75 76 this.setImageViewResource (android.R.id.icon, iconId); 77 this.setTextViewText (android.R.id.title, title); 78 this.setTextViewText (android.R.id.text1, text [0]); 79 this.setTextViewText (android.R.id.text2, text [1]); 80 82 return; 83 } // TwoLineNotification 84 } // TwoLineNotification
That is all what is needed. Sure you can make it more complicated with set and get methods and some factory pattern. But what for?
One more thing: the range command is used as well and I won’t withhold that:
1 /** 2 * 3 * test if a array lenght is inside the range min … max 4 * 5 * 6 * @paramvalue 7 * value to test 8 * @parammin 9 * minimum allowed value 10 * @parammax 11 * maximum allowed value 12 * @throwsIllegalArgumentException 13 * when value is outside min … max 14 */ 15 public static void range ( 16 @NotNull final Object[] value, 17 final int min, 18 final int max) 19 { 20 if ((min > value.length) || (value.length > max)) 21 { 22 final IllegalArgumentException Exception = 23 new IllegalArgumentException ("length of value “" + java.util.Arrays.toString (value) + "” is not in length range " + 24 min + " … " + max); 25 android.util.Log.e (Required.TAG, "LOG00145:", Exception); 26 throw Exception; 27 } // if 28 } // range
Yes, I am a great fan of defensive programming.
How to use
That too is pretty simple. You use an instance of android.app.Notification.Builder
to create your notification. Within android.app.Notification.Builder
there is a method called setContent
. Use that to set your new remote view as content of your notification. The following sample also contains all backward compatibility checks:
1 /** 2 * 3 * unique id for the foreground notification — just a random int. (0x1f59b12) 4 * 5 */ 6 private static final int Foreground_Notification_ID = 32873234; 7 46 /** 47 * 48 * icon id to display on the notification. Note: No notification is shown wenn set to 0. 49 * 50 */ 51 private int foregroundNotificationIconId; 52 53 /** 54 * 55 * tile for to display on the notification. Note: No notification is shown wenn set to null. 56 * 57 */ 58 @Nullable 59 private CharSequence foregroundNotificationTitle; 60 121 /** 122 * 123 * create a notification builder to display the give two line of text. 124 * 125 * 126 * @paramactionClass 127 * activity class to start when the notification is clicked 128 * @paramtext 129 * two lines of text to display 130 * @returna builder to create a notification 131 */ 132 @SuppressWarnings ("FeatureEnvy") 133 @android.annotation.TargetApi (16) 134 private final android.app.Notification createNotification ( 135 @Nullable final Class actionClass, 136 @NotNull final CharSequence[] text) 137 { 142 com.noser.Required.isTrue (text.length == 2, "text.length must be 2"); 143 com.noser.Required.notNull (text [0], "text [0]"); 144 com.noser.Required.notNull (text [1], "text [1]"); 145 146 final android.app.Notification.Builder retval = 147 new android.app.Notification.Builder (this); 148 149 if (actionClass != null) 150 { 151 final android.content.Intent openAction = 152 new android.content.Intent (this, actionClass); 153 final android.app.PendingIntent pendingOpenAction = 154 android.app.PendingIntent.getActivity ( 155 /* context => */this, 156 /* requestCode => */0, 157 /* intent => */openAction, 158 /* flags => */0); 159 retval.setContentIntent (pendingOpenAction); 160 } 161 162 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) 163 { 164 assert this.foregroundNotificationTitle != null : "caller will make sure it is so."; 165 166 final Package layoutPackage = com.noser.apklib.R.class.getPackage (); 167 final com.noser.ui.TwoLineNotification Notification_Layout = 168 new com.noser.ui.TwoLineNotification ( 169 /* packageName => */getPackageName(), 170 /* layoutId => */com.noser.apklib.R.layout.two_line_notification, 171 /* title => */this.foregroundNotificationTitle, 172 /* text => */text, 173 /* iconid => */this.foregroundNotificationIconId); 174 175 retval.setContent (Notification_Layout); 176 } 177 else 178 { 179 retval.setContentTitle (this.foregroundNotificationTitle); 180 retval.setContentText (text [0].toString () + '|' + text [1].toString ()); 181 } // if 182 183 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) 184 { 185 retval.setPriority (android.app.Notification.PRIORITY_DEFAULT); 186 } // if 187 188 retval.setSmallIcon (this.foregroundNotificationIconId); 189 retval.setOngoing (true); 190 193 return retval.getNotification (); 194 } // createNotification
Result
The Result looks like this: