Блог о Gentoo и около-линуксовым штукам

30 июля 2017 г.

Виджет для Android (без конфигуратора и с IntentService)

07:23 Опубликовал Дмитрий Исаенко , Нет комментариев
Сегодня мы будем создавать виджет, который может обновлять текст в нём по нажатию на одну из его четырёх кнопок. По нажатию на первые две будет вызываться MainActivity и обновлять текст на тот, что был передан в зависимости от того, какая из двух кнопок нажата. По нажатию на третью и четвёртую текст просто будет обновляться с помощью IntentSerive службы.
Код проекта доступен по ссылке https://github.com/developersu/MyWidgetNoConf/ .

Создадим новый проект с именем MyWidgetNoConf.

Опишем в манифесте все наши классы
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.blogspot.developersu.mywidgetnoconf">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <receiver android:name=".myWidget">
            <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=".MyIntentService"
            android:exported="false">
        </service>
    </application>

</manifest>
Теперь добавим XML с описанием возможнотей нашего виджета.

res→xml→widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minHeight="180dp"
    android:minWidth="180dp"
    android:updatePeriodMillis="1800000"
    android:resizeMode="horizontal|vertical"
    />
Добавим layout-файл для виджета:
res→layout→my_widget_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/widgetText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="0" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <Button
            android:id="@+id/widgetBtn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button 1" />
        <Button
            android:id="@+id/widgetBtn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button 2" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="horizontal">
        <Button
            android:id="@+id/widgetBtn3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 3" />
        <Button
            android:id="@+id/widgetBtn4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Button 4" />
    </LinearLayout>
</LinearLayout>
Получится следующее:
Добавим классы MyIntentService и myWidget в основной package.
java→com.blogspot.developersu.mywidgetnoconf
→MyIntentService
→myWidget

Перейдём собственно к коду. Начнём с описания функционала виджета. Мы будем использовать onUpdate и onEnabled плюс функцию для определения функций клавиш. Сохранением данных тестовом поле пренебрежем.

Итак, для начала код:
package com.blogspot.developersu.mywidgetnoconf;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;

public class myWidget extends AppWidgetProvider {

    private void setWidgetIntents(Context context, AppWidgetManager appWidgetManager, int appWidgetIds[]){

        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.my_widget_layout);

        for (int appWidgetId : appWidgetIds) {
            Intent intentBtn = new Intent(context, MainActivity.class);
            intentBtn.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intentBtn.putExtra("signature", "Button 1");
            intentBtn.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP );
            PendingIntent pi = PendingIntent.getActivity(context, appWidgetId+0, intentBtn, PendingIntent.FLAG_UPDATE_CURRENT); 
            rv.setOnClickPendingIntent(R.id.widgetBtn, pi);
            /*===================================================================*/
            // Set the same for button 2
            Intent intentBtn2 = new Intent(context, MainActivity.class);
            intentBtn2.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intentBtn2.putExtra("signature", "Button 2");
            intentBtn2.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP );
            PendingIntent pi2 = PendingIntent.getActivity(context, appWidgetId+1, intentBtn2, PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setOnClickPendingIntent(R.id.widgetBtn2, pi2);
            /*===================================================================*/
            Intent intentBtn3 = new Intent(context, MyIntentService.class);
            intentBtn3.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intentBtn3.putExtra("signature", "Button 3");
            PendingIntent pi3 = PendingIntent.getService(context, appWidgetId+2, intentBtn3, PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setOnClickPendingIntent(R.id.widgetBtn3, pi3);
            /*===================================================================*/
            Intent intentBtn4 = new Intent(context, MyIntentService.class);
            intentBtn4.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intentBtn4.putExtra("signature", "Button 4");
            PendingIntent pi4 = PendingIntent.getService(context, appWidgetId+3, intentBtn4, PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setOnClickPendingIntent(R.id.widgetBtn4, pi4);
            appWidgetManager.updateAppWidget(appWidgetId, rv);
        }
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        setWidgetIntents(context, appWidgetManager, appWidgetIds);
    }

    @Override
    public void onEnabled(Context context) {
        AppWidgetManager awm = AppWidgetManager.getInstance(context);
        ComponentName compName = new ComponentName(context, myWidget.class);
        int[] widgetIds = awm.getAppWidgetIds(compName); // избыточно, но оставим для наглядности
        setWidgetIntents(context, awm, widgetIds);
    }
}
У нас есть функция setWidgetIntents которая принимает context, AppWidgetManager и множество ID среди которых есть и ID нашего виджета.
onUpdate — все данные у нас сразу имеются, так что просто передаём их в функцию
onEnabled — создаём объект  AppWidgetManager используя AppWidgetManager.getInstance(context), создаём ComponentName, который нам понадобится для вытаскивания передавая ему context и имя класса нашего виджета. IDs получаем через awm.getAppWidgetIds(compName).
setWidgetIntents — опеределим “RemoteViews rv” чтобы через него присваивать кнопкам действия. Далее создадим цикл для всех полученных widget ID.
Присваивать каждой кнопке действия следует используя PendingIntent. Для него требуется Intent в который в первом и втором случае обращён к MainActivitiy.class, а в третьем и четвёртом — к  MyIntentService.class.
В каждый такой Intent мы помещаем значение кнопки, которое будет передаваться. Это будет строка с именем “signature” и значением “Button N”, где N номер кнопки. Также передаём в него ID виджета, который инициировал вызов Intent.
Кроме того, в первых двух случаях мы определяем флаг  Intent.FLAG_ACTIVITY_SINGLE_TOP который говорит системе, что Activity не должен запускаться, если экземпляр уже существует в стеке. Таким образом, если activity уже запущен, то приниматься Intent с этим флагом будет в методе onNewIntent(). Альтернативой этому может быть установка в манифесте launchMode в singleTop (тогда этот флаг не нужен):
<activity android:name=".MainActivity" android:launchMode="singleTop">
Но этот вариант мы опустим. Трогать манифест не будем :)

Для обращения  к сервису этот флаг нам не потребуется, т.к. сервисы работают совсем не так как обычные Activity.
В конце мы “создаём” PendingIntent обращаясь к getActivity или getService. Оба этих метода принимают 4 значения: context, requestCode, Intent и флаги. В requestCode мы будем заносить ID нашего виджет плюс какую-то цифру, чтобы определить каждый PendingIntent отдельно. Их стандартное поведение может поначалу ввести в заблуждение, но, если коротко, то для разных PendingIntent лучше бы иметь разные коды. Используемый флаг PendingIntent.FLAG_UPDATE_CURRENT говорит системе, что если описываемый PendingIntent уже существует, то следует лишь заменить его extra-данные (те, что мы добавляли используя метод addExtra).

Далее присваеваем каждой кнопке действие и “применяем измененеия”
rv.setOnClickPendingIntent(R.id.widgetBtn, pi);
appWidgetManager.updateAppWidget(appWidgetId, rv);
Перейдём теперь к классу MyIntentService.
Класс наследует  IntentService и реагирует ловит входящие Intent в onHandleIntent. При запуске вызыватся конструктор, порождается новая нить, которая после выполнения метода  onHandleIntent() завершается.

Переходим к коду. Ничего, чтобы ещё хотелось выделить отдельно у меня нет.
package com.blogspot.developersu.mywidgetnoconf;

import android.app.IntentService;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RemoteViews;


public class MyIntentService extends IntentService {

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent != null) {
            Bundle bndle = intent.getExtras();
            int wID = bndle.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
            String btnPressed = bndle.getString("signature");

            RemoteViews rv = new RemoteViews(getPackageName(), R.layout.my_widget_layout);
            AppWidgetManager awm = AppWidgetManager.getInstance(getApplicationContext());
            rv.setTextViewText(R.id.widgetText, btnPressed);
            awm.updateAppWidget(wID, rv);
        }
    }
}
Перейдём к MainActivity.

Тут как не было элементов управления так и нет. При получении Intent будет вызываться или onCreate() (если приложение закрыто) или onNewIntent() (если экземляр уже существует). Определением того, что мы получили в прилетевшем Intent будет заниматься метод handleIntent(), который принимает Intent.
В случае, если Intent не пустой и содержит хоть какие-то extras, будем пытаться(!) брать из него предполагаемые данные, т.е. строку “signature” и ID виджета.
Сразу после этого, выведем тост (Toast) с полученой информацией. Если пользователь просто запускает приложение, то ему покажется уведомление с widget ID = 0 и string = null. Далее приложение проверит эти поля и если они отличны от вышеприведённых значений, то присвоит строку в TextView виджета. За присвоение отвечает метод  sendBackToWidget().
package com.blogspot.developersu.mywidgetnoconf;

import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private void handleIntent(Intent i){
        String recievedString;
        Bundle bndle = i.getExtras();
        if (bndle != null) {
            int awID = bndle.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
            recievedString = bndle.getString("signature");
            Toast.makeText(getApplicationContext(), "Got intent from: " + Integer.toString(awID) + " " + recievedString, Toast.LENGTH_SHORT).show();
            if (awID != 0 && recievedString != null)
                sendBackToWidget(awID, recievedString);
        }
    }

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

        handleIntent(getIntent());
        /* Почему мы вызываем handleIntent прямо тут? Потому, что если приложение закрыто а не работает в фоне, то при запуске его из виджета будет вызван
         * onCreate. Очевидно, что если это приложение уже будет находиться в стеке, то вызываться будет что-то другое :) */
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        //setIntent(intent);                    // Удобно использовать, если бы наш Intent был объявлен глобальной переменной. Тогда бы мы просто переопределили его через setintent().
        handleIntent(intent);
    }

    private void sendBackToWidget(int widgetID, String str){
        AppWidgetManager awManager = AppWidgetManager.getInstance(getApplicationContext());
        RemoteViews rv = new RemoteViews(getPackageName(), R.layout.my_widget_layout);
        rv.setTextViewText(R.id.widgetText, str);
        awManager.updateAppWidget(widgetID, rv);
    }
}
Код не идеален и оставляет множество мест для оптимизаций. Впрочем, нет ничего хуже, чем преждевременная оптимизация, так что оставлю это вам.

Посмотрим на то, что получилось.

0 коммент.:

Отправить комментарий