Часто встречающаяся задача — создание виджета со счетчиком непрочитанных сообщений/звонков и т.п. Однако в Android нет стандартного класса для создания таких виджетов.

Как же все-таки создать такой виджет?
О том как создавать виджеты и как они работают можно найти в этой и этой статьях, потому не будем останавливаться на деталях создания виджетов как таковых.
Основная задача, которую нам необходимо выполнить — создать виджет, максимально похожий на стандартные иконки и виджеты стандартных приложений.
Попытки найти уже готовое решение ни к чему не привели. Потому пришлось взять в руки APK Manager (спасибо статье про реверс-инжиниринг) и приложение Google Reader for Android — надеялся, что их виджет выглядит стандартно. Оказалось, что это не так:

Немного похоже, конечно, но не совсем то, что нужно. Пришлось доделывать:
Высота виджета — 82dip — больше, чем рекомендуемая. Но иначе наш виджет будет меньше стандартной иконки.
Указанный в коде выше @drawable/shortcut_selector — xml-файл, описывающий, как виджет будет реагировать на нажатия/выделения:
Тут мы сталкиваемся с первой проблемой — pressed_application_background и focused_application_background — изображения фона. Но в разных телефонах фоны разные. Например, в стандартном Android — фон оранжевый, в HTC — зеленый. Решением была бы возможность обратиться к стандартным ресурсам Android'a — благодаря сайту Android R Drawables, можно даже найти id этих ресурсов:
Но эти ресурсы не public и использовать их в своем приложении нельзя. Можно попробовать считывать значение android.os.Build и определять тип OS. Но я решил, что для виджета это не так важно, потому оставил оранжевый фон (кстати, в виджете Google Reader сделано также).
Вторая проблема — аналогична первой. Фон круга с числом непрочитанных сообщений на стандартном Android — красный, на HTC — зеленый. Но тут можно обратиться к стандартному ресурсу — @android:drawable/ic_notification_overlay. Это не круг, но выглядит очень похоже (изображение будет чуть ниже).
Подсветка фона текста взята из виджета Google Reader без изменений (файл appwidget_text_background.xml):
В моем приложении (думаю, как и в других приложениях с таким счетчиком) нет необходимости периодически обновлять значение счетчика. Потому updatePeriodMillis=«0» и его обновление происходит лишь в двух случаях:
При этом для обновления виджета используется следующий код:
Если число непрочитанных сообщений равно нулю, то круг с числом скрывается:
Вот что получилось в итоге:

Выглядит неидеально, но по-моему очень похоже. В эмуляторе выглядит тоже очень похоже на оригинал.

Как же все-таки создать такой виджет?
О том как создавать виджеты и как они работают можно найти в этой и этой статьях, потому не будем останавливаться на деталях создания виджетов как таковых.
Основная задача, которую нам необходимо выполнить — создать виджет, максимально похожий на стандартные иконки и виджеты стандартных приложений.
Попытки найти уже готовое решение ни к чему не привели. Потому пришлось взять в руки APK Manager (спасибо статье про реверс-инжиниринг) и приложение Google Reader for Android — надеялся, что их виджет выглядит стандартно. Оказалось, что это не так:

Немного похоже, конечно, но не совсем то, что нужно. Пришлось доделывать:
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout android:orientation="vertical" android:id="@+id/widget" android:background="@drawable/shortcut_selector" android:focusable="true" android:clickable="true" android:layout_width="74.0dip" android:layout_height="82.0dip" xmlns:android="http://schemas.android.com/apk/res/android"> <FrameLayout android:layout_width="fill_parent" android:layout_height="0.0dip" android:layout_weight="1.0" android:paddingTop="3dip"> <FrameLayout android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon" android:scaleType="center" /> <TextView android:textSize="14.0dip" android:textStyle="bold" android:textColor="#ffffffff" android:gravity="center" android:layout_gravity="bottom|right|center" android:id="@+id/widget_counter" android:background="@android:drawable/ic_notification_overlay" android:layout_width="24dip" android:layout_height="24dip" android:singleLine="true" android:shadowColor="#ff000000" android:shadowRadius="1.0"/> </FrameLayout> </FrameLayout> <TextView android:textSize="13.0dip" android:textColor="#ffffffff" android:ellipsize="marquee" android:gravity="center_horizontal" android:layout_gravity="center_horizontal" android:id="@android:id/text1" android:background="@drawable/appwidget_text_background" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="1dip" android:paddingBottom="1dip" android:singleLine="true" android:shadowColor="#ff000000" android:shadowRadius="2.0" android:layout_weight="0.0" android:text="@string/app_name" /> </LinearLayout>
Высота виджета — 82dip — больше, чем рекомендуемая. Но иначе наш виджет будет меньше стандартной иконки.
Указанный в коде выше @drawable/shortcut_selector — xml-файл, описывающий, как виджет будет реагировать на нажатия/выделения:
<?xml version="1.0" encoding="UTF-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/pressed_application_background" /> <item android:state_focused="true" android:state_window_focused="true" android:drawable="@drawable/focused_application_background" /> <item android:state_focused="true" android:state_window_focused="false" android:drawable="@android:color/transparent" /> </selector>
Тут мы сталкиваемся с первой проблемой — pressed_application_background и focused_application_background — изображения фона. Но в разных телефонах фоны разные. Например, в стандартном Android — фон оранжевый, в HTC — зеленый. Решением была бы возможность обратиться к стандартным ресурсам Android'a — благодаря сайту Android R Drawables, можно даже найти id этих ресурсов:
- pressed_application_background_static
- focused_application_background_static
Но эти ресурсы не public и использовать их в своем приложении нельзя. Можно попробовать считывать значение android.os.Build и определять тип OS. Но я решил, что для виджета это не так важно, потому оставил оранжевый фон (кстати, в виджете Google Reader сделано также).
Вторая проблема — аналогична первой. Фон круга с числом непрочитанных сообщений на стандартном Android — красный, на HTC — зеленый. Но тут можно обратиться к стандартному ресурсу — @android:drawable/ic_notification_overlay. Это не круг, но выглядит очень похоже (изображение будет чуть ниже).
Подсветка фона текста взята из виджета Google Reader без изменений (файл appwidget_text_background.xml):
<?xml version="1.0" encoding="UTF-8"?> <shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#b2191919" /> <padding android:left="5.0dip" android:top="1.0dip" android:right="5.0dip" android:bottom="1.0dip" /> <corners android:radius="8.0dip" /> </shape>
В моем приложении (думаю, как и в других приложениях с таким счетчиком) нет необходимости периодически обновлять значение счетчика. Потому updatePeriodMillis=«0» и его обновление происходит лишь в двух случаях:
- когда происходит обновление в базе данных;
- когда приложение становится невидимым (onStop()).
При этом для обновления виджета используется следующий код:
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); int[] a = appWidgetManager.getAppWidgetIds(new ComponentName(this.getPackageName(), "WidgetProvider")); List<AppWidgetProviderInfo> b = appWidgetManager.getInstalledProviders(); for (AppWidgetProviderInfo i : b) { if (i.provider.getPackageName().equals(this.getPackageName())) { a = appWidgetManager.getAppWidgetIds(i.provider); new WidgetProvider().onUpdate(this, appWidgetManager, a); } }
Если число непрочитанных сообщений равно нулю, то круг с числом скрывается:
if (unreadRecordsCount == 0) { views.setViewVisibility(R.id.widget_counter, View.INVISIBLE); views.setTextViewText(R.id.widget_counter, ""); } else { views.setTextViewText(R.id.widget_counter, Long.toString(unreadRecordsCount)); views.setViewVisibility(R.id.widget_counter, View.VISIBLE); }
Вот что получилось в итоге:

Выглядит неидеально, но по-моему очень похоже. В эмуляторе выглядит тоже очень похоже на оригинал.