четверг, 18 ноября 2010 г.

Android widget, part 0.1

Итак, сейчас у проекта тестовое активити, которое показывается при старте и показывает внешний вид нашего будущего виджета; в окончательном виде его разметка выглядит примерно так:

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:background="@drawable/bg"> // для красоты это фон окна

    <LinearLayout
            android:layout_alignParentTop="true"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="18dip"
            >

        <ImageView
                android:src="@drawable/l" // закругление выделения текущего дня
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:id="@+id/cur_day" // имя контрола для доступа в дальнейшем
                android:text="empty_day" // текст-заглушка для тестирования
                android:textSize="14sp"  // размер шрифта, подобран методом тыка
                android:textColor="#FFFFFF" // до соответствия картинке-макету
                android:gravity="center_vertical|left" 
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/blackbg" 
                />
        <ImageView
                android:src="@drawable/r"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:text="other days"
                android:id="@+id/days"
                android:textSize="13sp" // размер шрифта неактивных дней поменьше
                android:textColor="#989898" // тут можно поменять цвет текста
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />

    </LinearLayout>

    <LinearLayout  // это часть с крупными цифрами
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="36dip" // фиксированная высота 36 пикселей
            android:layout_centerVertical="true"
            android:layout_centerInParent="true"
            >
        <LinearLayout  // это место для 2-х цифр для даты
                android:id="@+id/btnDate" // область прижимается к левому краю
                android:orientation="horizontal"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:paddingLeft="5dip" // для красоты отступаем немного от края
                >

            <ImageView // первая цифра - десятки дней
                    android:id="@+id/date1"
                    android:src="@drawable/d1" // тестовая дата - "12"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
            <ImageView
                    android:id="@+id/date2"
                    android:src="@drawable/d2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
        </LinearLayout>
        <LinearLayout // область для цифр и ":" между часами-минутами
                android:id="@+id/btnTime"
                android:orientation="horizontal"
                android:layout_width="fill_parent" // занимает остаток ширины виджета
                android:layout_height="wrap_content"
                android:gravity="right" // и выравнивает содержимое вправо
                android:layout_gravity="center_vertical"
                android:paddingRight="5dip" // ну и отступ для красоты
                >

            <ImageView
                    android:id="@+id/hour1"
                    android:src="@drawable/d0"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
            <ImageView
                    android:id="@+id/hour2"
                    android:src="@drawable/d0"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
            <ImageView
                    android:src="@drawable/dd" // для ":" id не нужен
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical" // выравниваем по центру
                    />
            <ImageView
                    android:id="@+id/min1"
                    android:src="@drawable/d0"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
            <ImageView
                    android:id="@+id/min2"
                    android:src="@drawable/d0"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    />
        </LinearLayout>

    </LinearLayout>

    <LinearLayout // то же, что и с днями
            android:layout_alignParentBottom="true"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="18dip"
            >
        <ImageView
                android:src="@drawable/l"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:id="@+id/cur_month"
                android:text="empty_month"
                android:textSize="14sp"
                android:textColor="#FFFFFF"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/blackbg"
                />
        <ImageView
                android:src="@drawable/r"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:text="other months"
                android:id="@+id/months"
                android:textSize="13sp"
                android:textColor="#989898"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />
    </LinearLayout>

</LinearLayout>

Похоже это окончательный вариант разметки виджета:

w3

по высоте ровно 72 пиксела, все хорошо видно.. Можно приступать собственно к виджету

Виджет описывается xml-файлом в папке res/xml, скажем widget.xml:

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

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dip"
    android:minHeight="72dip"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/main"
/>

Параметров немного – максимальный размер (высчитанный по формуле из предположения, что займет 4 область 4 * 1 клетки на экране), время между обновлениями (не может быть меньше 30 минут, так что бесполезный параметр в данном случае) и файл с описанием разметки виджета (который уже сделан и протестирован).

Затем надо создать новый класс, скажем с именем SimpleClockClass, и добавить его описание в манифест:

    <receiver android:name=".SimpleClockClass" android:label="Simple Clock Widget">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
                   android:resource="@xml/widget"/>
    </receiver>
здесь говорится: “есть класс SimpleClockClass, который реагирует на сообщение APPWIDGET_UPDATE, данные о нем – в файле res/xml/widget, имейте его в виду, когда система будет обновлять свои виджеты”.. Ну что-то типа такого..

Создаем новый класс:

package info.hamster.simpleclock.widget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;

public class SimpleClockClass extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
}

Тут все очень простенько – наш класс наследуется от AppWidgetProvider и пока ничего не делает. Зато виджет можно скомпилировать, загрузить в телефон/эмулятор и добавить на рабочий стол и посмотреть, что все работает:

w4

Упс.. что-то не так с высотой, да и фон не украшает ни разу.. Не знаю, в чем там дело с высотой, в разметке стоит “по содержимому”, меняем на точный размер, ну и удаляем фон, как-то так:

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

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="72dip">

    <LinearLayout ...

w5

Так лучше; тут я логику не понял: я как-будто бы сказал системе, что я не вылезу за 294*72 прямоугольник, логично как-бы, что wrap_content должен вписывать лайоут в эти рамки.. а вписывает черти-как..

Дальше – пора удалять тестовое активити, которое помогало отладить разметку виджета; удалить надо сам java файл и упоминание о нем в манифесте; вместо этого добавим “фичу”, которая обычно встречается – при добавлении виджета на рабочий стол покажем экран настроек виджета. Делается просто: сначала разметка – это будет типичный экран настроек, такой, как у самого телефона, создаем файл в res/xml, называем его скажем config.xml:

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

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
            android:title="Отображение">
        <CheckBoxPreference
                android:title="12/24 формат времени"
                android:defaultValue="false"
                android:summary="Использовать 24-часовой формат"
                android:key="timeFormat"/>
        </PreferenceCategory>
    <PreferenceCategory
            android:title="Поведение">
        <ListPreference
                android:title="Тап по часам"
                android:summary="Действие при тапе по часам"
                android:key="clockAction"
                android:defaultValue="0"/>
        <ListPreference
                android:title="Тап по дате"
                android:summary="Действие при тапе по дате"
                android:key="dateAction"
                android:defaultValue="0"/>

    </PreferenceCategory>
</PreferenceScreen>

Что тут: 2 раздела (красоты ради), в “Отображении” настройка показа времени – в 12 или 24-часовом формате; в “Поведении” 2 списка – какую программу запускать при тапе на дате и времени. Делаем новую активити, с именем класса ConfigClass:

package info.hamster.simpleclock.widget;

import android.os.Bundle;
import android.preference.PreferenceActivity;

public class ConfigClass extends PreferenceActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.config);
    }
}

Класс наследуется от PreferenceActivity – это даст нужный внешний вид и поведение, разметка берется из config.xml. В манифест добавляется соотв. запись:

        <activity android:name=".Settings" android:label="Settings Activity">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
                <action android:name="android.intent.action.VIEW"/>
            </intent-filter>
        </activity>

что значит – активити является настроечной для виджета, кроме этого в widget.xml надо добавить строчку с указанием, какой класс отвечает за конфигурирование:

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

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="294dip"
    android:minHeight="72dip"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/main"
    android:configure="info.hamster.simpleclock.widget.ConfigClass"
/>

Компилируем, загружаем в телефон/эмулятор, добавляем виджет на рабочий стол – оп:

w6

Правда не стоит тапать по спискам – надо допиливать еще, а так будет “Sorry.. blahblahblah has stopped”. И еще надо придумать механизм для завершения настройки

Завершение настройки – тут просто, надо добавить в Config меню, с единственным пунктом Save settings или типа того (ну не обязательно пункт должен быть один – About, Visit us.. тоже могут присутствовать). В res/menu создаем xml файл с описанием меню:

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

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/save_settings"
          android:icon="@android:drawable/ic_menu_preferences"
          android:title="Сохранить настройки" />
</menu>

в ConfigClass переопределяем метод:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

проверяем в эмуляторе – добавляем виджет и в окне настроек жмем кнопку Menu:

w7

Иконка на кнопке – стандартная (потому что в menu.xml указали android:icon="@android:drawable/ic_menu_preferences")

Теперь делаем следующее: когда экран настроек создается, надо запомнить id виджета (потому что в теории их ведь может быть несколько одинаковых на рабочем слоте), и установить результат создания активити как Cancelled, а после выбора “Сохранить настроки” установить результат работы активити как RESULT_OK и закрыть ее; так что ConfigClass меняем:

    int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.config);

        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        }
        setResult(RESULT_CANCELED);
    }
...
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.save_settings:
                Intent resultValue = new Intent();
                resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
                setResult(RESULT_OK, resultValue);
                finish();
            default:
                return super.onOptionsItemSelected(item);
        }
    }

Проверяем – добавляем виджет на рабочий стол, появляется экран настроек, вызываем меню, выбираем “Сохранить настройки” – виджет появляется на столе, если же нажать на Home или Back – нет; как и задумано

Теперь надо заполнить списки приложениями, которые можно запускать по тапу, ну и сохранить выбранные значения; PreferenceActivity автоматом сохраняет выбранные в нем значения в переменные с одноименными (с контролами в окне настроек) именами в SharedPreferences, хранилище настроек (ну и вообще данных) для приложения. Код:

  final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());

  final ListPreference listClockActions = (ListPreference) findPreference("clockAction");
  final ListPreference listDateActions = (ListPreference) findPreference("dateAction");
  // это элементы управления из окна настроек, нашли их по 
  // имени, заданному в xml файле

  final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
  mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
  final List<ResolveInfo> pkgAppsList = this.getPackageManager().queryIntentActivities(mainIntent, 0);

  // тут список приложений; для запуска и отображения мне надо 3 параметра
  // Красивое название приложения
  // package name - имя приложения собственно для запуска
  // activity name - активити, которое надо вызвать
  // тупо делаю 3 массива
        my_list = new String[pkgAppsList.size()];
        my_list2 = new String[pkgAppsList.size()];
        my_list3 = new String[pkgAppsList.size()];

        int i = 0;
        PackageManager pm = this.getPackageManager();

        for (ResolveInfo info : pkgAppsList) {
            my_list[i] = info.loadLabel(pm).toString();
            my_list2[i] = info.activityInfo.packageName;
            my_list3[i++] = info.activityInfo.name;
        }

        listClockActions.setEntries(my_list);
        listClockActions.setEntryValues(my_list2);
        listDateActions.setEntries(my_list);
        listDateActions.setEntryValues(my_list2);
  // выбранное значение из списка сохранится автоматически, а вот 
  // имя активити - нет, надо его сохранить отдельно в том же хранилище
        listClockActions.setOnPreferenceChangeListener(
                new Preference.OnPreferenceChangeListener() {
                    public boolean onPreferenceChange(Preference preference, Object o) {

                        int i = ((ListPreference) preference).findIndexOfValue(o.toString());

                        // для записи в preferences создаем редактор и коммитим изменения потом
                        SharedPreferences.Editor editor = prefs.edit();
                        editor.putString("clockActivity", my_list3[i]);
                        editor.commit();

                        return true;
                    }
                }
        );

        listDateActions.setOnPreferenceChangeListener(
                new Preference.OnPreferenceChangeListener() {
                    public boolean onPreferenceChange(Preference preference, Object o) {

                        int i = ((ListPreference) preference).findIndexOfValue(o.toString());

                        SharedPreferences.Editor editor = prefs.edit();
                        editor.putString("dateActivity", my_list3[i]);
                        editor.commit();

                        return true;
                    }
                }

        );

Ну что, работает – из списка можно выбрать значения, если удалить виджет и добавлять еще раз, то убеждаемся, что выбранные значения запомнились

Это все делалось с ConfigClass.java; возвращаемся к SimpleClockClass.java, Пока в нем не делается ничего полезного, обьявлен к переопределению один метод, onUpdate; дописываем:

@Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

        final int N = appWidgetIds.length;
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

        // настроки у виджета общие (если будет неск. экземпляров на рабочем столе), так что
        // перебираем все
        for (int i = 0; i < N; i++) {
            int appWidgetId = appWidgetIds[i];

            // читаю из хранилища значение для clockAction - так называется
            // список в окне настроек, в котором выбираем программу для запуска после
            // тапа по времени
            final String clockActionPackage = prefs.getString("clockAction", "");
            Intent intent = null;

            intent = new Intent(Intent.ACTION_MAIN);
            // clockActivity мы сохранили "вручную"
            ComponentName name = new ComponentName(clockActionPackage, prefs.getString("clockActivity", ""));

            // теперь магия - до конца еще не осознал ее
            // делаем "отложенное намерение" - запустить программу по имени пакета и активити
            intent.addCategory(Intent.CATEGORY_LAUNCHER);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
            intent.setComponent(name);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            // ищем "окно" нашего виджета таким вот способом
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.main);
            // и говорим - "если будет клик по элементу btnTime (а это лайоут в лайоуте в лайоуте,
            // а в нем самом - 5 "картинок" с изображением цифр и ":" между часами и минутами)
            // то хочу наконец выполнить давно отложенное намерение".. ну типа того
            views.setOnClickPendingIntent(R.id.btnTime, pendingIntent);

            final String dateActionPackage = prefs.getString("dateAction", "");

            intent = new Intent(Intent.ACTION_MAIN);
            name = new ComponentName(dateActionPackage, prefs.getString("dateActivity", ""));

            intent.addCategory(Intent.CATEGORY_LAUNCHER);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
            intent.setComponent(name);
            pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            views.setOnClickPendingIntent(R.id.btnDate, pendingIntent);

            // ну и обновляем виджет по id
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }

Проверяем – запускаем, настраиваем, применяем, тапаем, смотрим – запускается.. Ну и нормально

Проект в текущем состоянии - здесь

4 комментария:

  1. Мне кажется у вас ошибка.
    Класс настроек называется ConfigClass, а в манифест вы прописали Settings.

    ОтветитьУдалить
  2. та очень может быть, что и ошибка :)

    а в проекте из архива, ссылка на который в самом конце - что? проект должен работать, так что как там - так и правильно ;)

    ОтветитьУдалить
  3. ааа капут(( застрял на том что после добавления ConfigClass виджет перестал загружаться... Хотел посмотреть исходный код проекта, но по ссылке его уже нет.... Можете перезалить код?

    ОтветитьУдалить
  4. мда..

    Due to a critical failure with our third party hosting provider, we regret to inform you that your files are no longer accessible. All exhaustive attempts to recover your files have been unsuccessful. We realize how important your files are to you and sincerely apologize for this major disruption.

    я с рапидшары файлы перенес на это хранилище, потому что они там через время удалялись - а тут така фигня..

    нет у меня исходников :( уже: тему про андроид давно подзабросил - неинтересно стало, переключился на iOS и прочие забавки

    ОтветитьУдалить