Написание виджета для Android

Написание виджета под Android

Виджеты для андроид — это маленькие приложения, которые могут быть размещены на рабочем столе вашего Android-устрйства. Виджет периодически получает новые данные и обновляет свой вид. Существует два метода обновления виджета, один из них основан на конфигурационных XML файлах, а другой основан на использовании Android-сервиса AlarmManager. Итак, сегодня мы пишем приложение для Android. Приступим.

В соображениях безопасности виджет не имеет собственного процесса, а запускается как часть рабочего стола. Поэтому виджеты используют RemoteViews для создания интерфейса. RemoteViews может быть запущен другим процессом с такими же правами как и обычное приложение. Таким образом, код виджета будет выполнен с правами объявляющего его приложения, а не с правами рабочего стола. После прочтения данной статьи рекомендую также ознакомиться с тем, как происходит создание анимации на Андроид.

Для создания виджета Android вам нужно:
1. Создать XML файл со слоем, в котором описывается внешний вид виджета.
2. Создать XML файл, в котором описываются свойства виджета (AppWidgetPrividerInfo). Например, размер или частота обновления
3. Создать BroadcastReceiver, который будет использован для обновления виджетов. Этот приёмник расширяет AppWidgetProvider, который обеспечивает жизненный цикл виджета.
4. Изменения в файле AndroidManifest.xml

Для регистрации виджета вы создаёте BroadcastReceiver, который фильтрует действия android.appwidget.action.APPWIDGET_UPDATE. Вы также можете указать мета-данные в атрибуте android:name=»android.appwidget.provider».
BroadcastReceiver также получает метку и иконку, которые будут доступны в списке виджетов на Android-утсройстве.
Ссылка на ресурс содержит ссылку на XML-файл с начальными установками для виджета. Конфигурация виджета согласно этим установка происходит до первого вызова функции update().

Виджет может использовать ограниченный набор элементов интерфейса. Для спецификации слоёв можно использовать FrameLayout, LinearLayout and RelativeLayout. Из элементов управления можно использовать AnalogClock, Button, Chromometer, ImageButton, ImageView, ProgressBar и TextView.
В Android 3.0 доступны также GridView, ListView, StackView, ViewFlipper и AdapterViewFlipper.
Взаимодействие с элементами возможно только в обработчике OnClickListener.

Виджет будет занимать несколько ячеек на вашем рабочем столе. Ячейка обычно используется для размещения иконки одного приложения. Существует формула подсчёта размера, который будет занимать виджет: ((количество столбцов/строки)*74) — 2.
В Android 3.1 виджеты могут иметь изменяемый размер, например, вы сможете их уменьшить или увеличить. Для включения этой возможности используйте атрибуты: android:resizeMode=»horizontal|vertical».

В конфигурационном файле виджета можно указать интервал обновления. Система будет просыпаться после этого интервала и вызывать ваш broadcast receiver для обновления виджета. Самый маленький интервал — это 180000 миллисекунд (30 минут).
AlarmManager позволяет более эффективно использовать ресурсы и имеет более высокую частоту обновления.

Ваш BroadcastReceiver расширяет AppWidgetProvider. AppWidgetProvider — класс, который реализует метод onReceive(), извлекает требуемую информацию и вызывает соответствующий метод жизненного цикла виджета.
onEnabled() — вызывается один раз, когда виджет был добавлен на рабочий стол
onDisabled() — вызывается, когда последний экземпляр виджета был удалён с рабочего стола
onUpdate() — вызывается при каждом обновлении виджета.
onDeleted() — вызывается, когда виджет удаляется с рабочего стола

Теперь перейдём от теории к практике. Мы напишем виджет, который будет выводить новое случайное число при каждом нажатии на него.
Виджет для Android

  1. Создаём новый проект widget.com.com
  2. Создаем в каталоге «/res/drawable-mdpi» новый XML-файл с именем myshape.xml. В нём будет описываться задний фон нашего виджета.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <?xml version="1.0" encoding="UTF-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle" >
     
        <stroke
            android:width="2dp"
            android:color="#FFFFFFFF" />
     
        <gradient
            android:angle="225"
            android:endColor="#DD2ECCFA"
            android:startColor="#DD000000" />
     
        <corners
            android:bottomLeftRadius="7dp"
            android:bottomRightRadius="7dp"
            android:topLeftRadius="7dp"
            android:topRightRadius="7dp" />
    </shape>
    <?xml version="1.0" encoding="UTF-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle" >
    
        <stroke
            android:width="2dp"
            android:color="#FFFFFFFF" />
    
        <gradient
            android:angle="225"
            android:endColor="#DD2ECCFA"
            android:startColor="#DD000000" />
    
        <corners
            android:bottomLeftRadius="7dp"
            android:bottomRightRadius="7dp"
            android:topLeftRadius="7dp"
            android:topRightRadius="7dp" />
    </shape>
  3. В каталоге «res/layout» объявим внешний вид и элементы управления в файле widget_layout.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dip"
        android:background="@drawable/myshape" >
     
        <TextView
            android:id="@+id/update"
            style="@android:style/TextAppearance.Medium"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:gravity="center_horizontal|center_vertical"
            android:layout_margin="4dip"
            android:text="Static Text" >
        </TextView>
    </LinearLayout>
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dip"
        android:background="@drawable/myshape" >
    
        <TextView
            android:id="@+id/update"
            style="@android:style/TextAppearance.Medium"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:gravity="center_horizontal|center_vertical"
            android:layout_margin="4dip"
            android:text="Static Text" >
        </TextView>
    </LinearLayout>
  4. Создадим файл с настройками виджета «widget_info.xml», выбрав File -> New -> Android -> Android XML File
    AppWidgetProviderAppWidgetProvider

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider 
            xmlns:android="http://schemas.android.com/apk/res/android"
        android:initialLayout="@layout/widget_layout"
        android:minHeight="72dp"
        android:minWidth="300dp"
        android:updatePeriodMillis="300000" >
     
    </appwidget-provider>
    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider 
        	xmlns:android="http://schemas.android.com/apk/res/android"
        android:initialLayout="@layout/widget_layout"
        android:minHeight="72dp"
        android:minWidth="300dp"
        android:updatePeriodMillis="300000" >
    
    </appwidget-provider>
  5. Создадим BroadcastReeceiver, который будет вызываться для обновления.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    package widget.com.com;
     
    import android.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
     
    public class MyWidgetProvider extends AppWidgetProvider {
        private static final String LOG = "widget.com.com";
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                int[] appWidgetIds) {
            Log.w(LOG, "onUpdate method called");
            // Get all ids
            ComponentName thisWidget = new ComponentName(context,
                    MyWidgetProvider.class);
            int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
     
            // Build the intent to call the service
            Intent intent = new Intent(context.getApplicationContext(),
                    UpdateWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);
     
            // Update the widgets via the service
            context.startService(intent);
        }
    }
    package widget.com.com;
    
    import android.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
    
    public class MyWidgetProvider extends AppWidgetProvider {
    	private static final String LOG = "widget.com.com";
    	@Override
    	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
    			int[] appWidgetIds) {
    		Log.w(LOG, "onUpdate method called");
    		// Get all ids
    		ComponentName thisWidget = new ComponentName(context,
    				MyWidgetProvider.class);
    		int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
    
    		// Build the intent to call the service
    		Intent intent = new Intent(context.getApplicationContext(),
    				UpdateWidgetService.class);
    		intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);
    
    		// Update the widgets via the service
    		context.startService(intent);
    	}
    }
  6. Изменим файл AndroidManifest.xml, указав, что виджет будет принимать событие «android.appwidget.action.APPWIDGET_UPDATE», иметь настройки «@xml/widget_info» и обрабатываться сервисом «.UpdateWidgetService».
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="widget.com.com"
        android:versionCode="1"
        android:versionName="1.0" >
     
        <uses-sdk android:minSdkVersion="15" />
     
        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >
            <receiver android:name="MyWidgetProvider" >
                <intent-filter >
                    <action 
                        android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                </intent-filter>
     
                <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/widget_info" />
            </receiver>
            <service android:name=".UpdateWidgetService"></service>
        </application>
     
    </manifest>
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="widget.com.com"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk android:minSdkVersion="15" />
    
        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >
    		<receiver android:name="MyWidgetProvider" >
                <intent-filter >
                    <action 
                        android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                </intent-filter>
    
                <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/widget_info" />
            </receiver>
            <service android:name=".UpdateWidgetService"></service>
        </application>
    
    </manifest>
  7. Создадим новый класс MyWidgetProvider, который является наследником AppWidgetProvider. В нём будет происходить обработка событий жизненного цикла виджета.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    package widget.com.com;
     
    import android.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
     
    public class MyWidgetProvider extends AppWidgetProvider {
        private static final String LOG = "widget.com.com";
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                int[] appWidgetIds) {
            Log.w(LOG, "onUpdate method called");
            // Get all ids
            ComponentName thisWidget = new ComponentName(context,
                    MyWidgetProvider.class);
            int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
     
            // Build the intent to call the service
            Intent intent = new Intent(context.getApplicationContext(),
                    UpdateWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);
     
            // Update the widgets via the service
            context.startService(intent);
        }
    }
    package widget.com.com;
    
    import android.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
    
    public class MyWidgetProvider extends AppWidgetProvider {
    	private static final String LOG = "widget.com.com";
    	@Override
    	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
    			int[] appWidgetIds) {
    		Log.w(LOG, "onUpdate method called");
    		// Get all ids
    		ComponentName thisWidget = new ComponentName(context,
    				MyWidgetProvider.class);
    		int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
    
    		// Build the intent to call the service
    		Intent intent = new Intent(context.getApplicationContext(),
    				UpdateWidgetService.class);
    		intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);
    
    		// Update the widgets via the service
    		context.startService(intent);
    	}
    }
  8. Создадим класс UpdateWidgetService. Он будет сервисом, в котором будут генерироваться случайные числа и выводится на сам виджет.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    
    package widget.com.com;
     
    import java.util.Random;
     
    import android.app.PendingIntent;
    import android.app.Service;
    import android.appwidget.AppWidgetManager;
    import android.content.ComponentName;
    import android.content.Intent;
    import android.os.IBinder;
    import android.util.Log;
    import android.widget.RemoteViews;
     
    public class UpdateWidgetService extends Service {
        private static final String LOG = "de.vogella.android.widget.example";
     
        @Override
        public void onStart(Intent intent, int startId) {
            Log.i(LOG, "Called");
            // Create some random data
     
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this
                    .getApplicationContext());
     
            int[] allWidgetIds = intent
                    .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
     
            ComponentName thisWidget = new ComponentName(getApplicationContext(),
                    MyWidgetProvider.class);
            int[] allWidgetIds2 = appWidgetManager.getAppWidgetIds(thisWidget);
            Log.w(LOG, "From Intent" + String.valueOf(allWidgetIds.length));
            Log.w(LOG, "Direct" + String.valueOf(allWidgetIds2.length));
     
            for (int widgetId : allWidgetIds) {
                // Create some random data
                int number = (new Random().nextInt(100));
     
                RemoteViews remoteViews = new RemoteViews(this
                        .getApplicationContext().getPackageName(),
                        R.layout.widget_layout);
                Log.w("WidgetExample", String.valueOf(number));
                // Set the text
                remoteViews.setTextViewText(R.id.update,
                        "Random: " + String.valueOf(number));
     
                // Register an onClickListener
                Intent clickIntent = new Intent(this.getApplicationContext(),
                        MyWidgetProvider.class);
     
                clickIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
                clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
                        allWidgetIds);
     
                PendingIntent pendingIntent = PendingIntent.getBroadcast(
                        getApplicationContext(), 0, clickIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);
                remoteViews.setOnClickPendingIntent(R.id.update, pendingIntent);
                appWidgetManager.updateAppWidget(widgetId, remoteViews);
            }
            stopSelf();
     
            super.onStart(intent, startId);
        }
     
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }
    package widget.com.com;
    
    import java.util.Random;
    
    import android.app.PendingIntent;
    import android.app.Service;
    import android.appwidget.AppWidgetManager;
    import android.content.ComponentName;
    import android.content.Intent;
    import android.os.IBinder;
    import android.util.Log;
    import android.widget.RemoteViews;
    
    public class UpdateWidgetService extends Service {
    	private static final String LOG = "de.vogella.android.widget.example";
    
    	@Override
    	public void onStart(Intent intent, int startId) {
    		Log.i(LOG, "Called");
    		// Create some random data
    
    		AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this
    				.getApplicationContext());
    
    		int[] allWidgetIds = intent
    				.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
    
    		ComponentName thisWidget = new ComponentName(getApplicationContext(),
    				MyWidgetProvider.class);
    		int[] allWidgetIds2 = appWidgetManager.getAppWidgetIds(thisWidget);
    		Log.w(LOG, "From Intent" + String.valueOf(allWidgetIds.length));
    		Log.w(LOG, "Direct" + String.valueOf(allWidgetIds2.length));
    
    		for (int widgetId : allWidgetIds) {
    			// Create some random data
    			int number = (new Random().nextInt(100));
    
    			RemoteViews remoteViews = new RemoteViews(this
    					.getApplicationContext().getPackageName(),
    					R.layout.widget_layout);
    			Log.w("WidgetExample", String.valueOf(number));
    			// Set the text
    			remoteViews.setTextViewText(R.id.update,
    					"Random: " + String.valueOf(number));
    
    			// Register an onClickListener
    			Intent clickIntent = new Intent(this.getApplicationContext(),
    					MyWidgetProvider.class);
    
    			clickIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    			clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
    					allWidgetIds);
    
    			PendingIntent pendingIntent = PendingIntent.getBroadcast(
    					getApplicationContext(), 0, clickIntent,
    					PendingIntent.FLAG_UPDATE_CURRENT);
    			remoteViews.setOnClickPendingIntent(R.id.update, pendingIntent);
    			appWidgetManager.updateAppWidget(widgetId, remoteViews);
    		}
    		stopSelf();
    
    		super.onStart(intent, startId);
    	}
    
    	@Override
    	public IBinder onBind(Intent intent) {
    		return null;
    	}
    }

    Запущенный однажды сервис будет обновлять все виджеты. Вы можете кликать по одному или по нескольким виджетам.

Это всё. Если будут вопросы или предложения по исправлению и дополнению, то обязательно отпишитесь в комментариях.

ЗЫ: пост написан под воздействием горячего шоколада и статьи http://www.vogella.de/articles/AndroidWidgets/article.html.
ЗЫ2: исходный код создания виджета под Android

Bookmark the permalink.

9 Responses to Написание виджета для Android

  1. Z says:

    ошибка в UpdateWidgetService.java на строке super.onStart(intent, startId);

  2. driwnd says:

    Спасибо за статью, запустить не удалось.

  3. driwnd says:

    • Ошибка в коде AndroidManifest.xml (см. после закрывающегося тэга xml).
    • Компилятор ругается — AndroidManifest.xml must be well-formatted
    • >Создадим BroadcastReeceiver, который будет вызываться для обновления.
    Ошибка в слове «BroadcastReeceiver»

  4. Павел says:

    Интересно, а создавал кто либо виджеты позволяющие выводить на рабочий стол содержимое текстовых файлов, а в идеале xls. Поясню идею. Вот у вас написана хитрая страница в xls которая способна посчитать Бог его знает что и привязать это к определенной дате. Тоесть — есть виджет, он активирует в вычисляемое состояние файл формата xls и передает файлу в несколько ячеек оговоренных ранее текущие дату и время и еще один или два параметра. Далее исчисляемая книга xls находит в себе необходимые данные и передает их из заранее оговоренных ячеек обратно в виджет.

    Ведь если запретить активацию VBA из xls ников, то это может быть вполне безопасным. А с вычислительной точки зрения позволит красиво выводить на рабочий стол хоть текущее состояние сферического коня в вакууме вычисленное по времени и дате.

    Опять же сколько прекрасных написанных на коленке эксель книг станут активно использоваться.

  5. Pingback: Жесты в Android | Программирование для мобильных устройств

  6. Pingback: Intel+Android=? | Программирование для мобильных устройств

Добавить комментарий для Павел Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *