0 komentarzy

AppWidgets – wstęp do widżetów ekranu głównego

Sierpień 6, 2011 AppWidget Tutoriale UI

AppWidgets są elementami, które z założenia powinny dostarczać użytecznych informacji najmniejszym możliwym kosztem. W związku z tym podczas ich projektowania powinniśmy mieć na uwadze ilość potrzebnych im zasobów oraz ilość miejsca jaką zajmują (które to oczywiście powinniśmy ograniczyć do niezbędnego minimum).
Widżety mogą być zarówno rozszerzeniem naszego programu jak i samodzielną aplikacją.W związku z założeniami o minimalnej ilości zasobów – widżety mają kilka ograniczeń narzuconych z góry. Najważniejszym z nich jest rodzaj elementów interfejsu z jakich możemy je budować. Oto ich krótka lista:

  • FrameLayout,
  • LinearLayout,
  • RelativeLayout,
  • AnalogClock,
  • Button,
  • Chronometr,
  • ImageButton,
  • ImageView
  • ProgressBar
  • TextView.

Niestety podczas projektowania UI nie możemy używać ani potomków powyższych elementów, ani co ciekawe pola EditText (wszelkie wyszukiwarki zrealizowane są tylko na podstawie powyższych dozwolonych elementów).

Przykładowa aplikacja

Zbudujemy teraz prostą aplikację-widżet, demonstrującą podstawowe możliwości tego komponentu. W skład aplikacji, oprócz widżetu, wchodzą również jedna Usługa oraz jedna Aktywność. Warto jednak pamiętać, że możemy zbudować aplikację składającą się tylko z samego widżetu.

Layout widżetu

Wygląd widżetu definiujemy dokładnie tak samo jak wygląd całej aplikacji, czyli w pliku XML. W związku z tym w folderze /res/layout/ tworzymy nowy plik (np. poprzez prawy klik na projekt -> new -> Other -> Andorid XML File -> nazwa widget.xml, typ Layout).
Jako tło dla naszego Widżetu wykorzystamy poniższy obrazek zapisany w technice NinePatch:

Oto cały kod źródłowy pliku widget.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:background="@drawable/widget_bg" android:padding="10dp">
	<TextView
		android:id="@+id/tvWidgetText"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:text="AppWidget Text"
		android:textColor="#000000"
		android:paddingBottom="5dp"
		android:paddingLeft="10dp"
		android:paddingTop="5dp"
		android:textSize="11sp" />
</LinearLayout>

Konfiguracja widżetu

Kolejnym ważnym ale już nie tak typowym elementem jest plik opisujący podstawowe ustawienia widżetu. Powinien on znajdować się w katalogu /res/xml/. Jedynym elementem jaki będzie posiadał jest <appwidget-provider>. Aby go wygenerować: (prawy klik na projekcie -> new -> Other -> Andorid XML File -> nazwa widget_provider.xml, typ AppWidget Provider).

Kiedy otworzymy nasz plik XML dla AppWidget Provider w edytorze graficznym (zakładka Structure na dole okna edytora lub prawy klik na pliku -> Open with -> Android Xml Resources Editor) po kliknięciu na element AppWidget Provider powinniśmy ujrzeć kilka atrybutów odpowiedzialnych za konfigurację widżetu:

  • Min width oraz Min height – są to atrybuty odpowiedzialne za ilość miejsca jaki zajmuje widżet. Parametry te są o tyle ciekawe, że pozwalają nam ustalić nie tyle dokładną wielkość widżetu co ilość pól jakie zajmuje nasz widżet w głównym oknie telefonu (standardowa wielkość ekranu to 4×4 pola). Jest nawet dokładny wzór, który definiuje wymiary:
    Min size = (Ilość pól * 74dp) – 2dp
    Min size to oczywiście zarówno Min width jak i Min height. i teraz uwaga – radzę stosować się do powyższego wzoru, ponieważ system i tak, niezależnie od ustawionej wielkości będzie wypełniał naszym widżetem całe pola (widżet nie może zajmować np. pół „kratki”).
  • Update period milis – czas pomiędzy aktualizacjami widżetu (który ma odpowiednią metodę służącą do autoaktualizacji). Jest podany w milisekundach.
    Należy pamiętać by czas ten był możliwie największy (zalecane jest 2-3 razy dziennie, a już tak całkiem super – raz dziennie). Dolną granicą jest tutaj 180000 milisekund, czyli 30minut.
  • Initial layout – wskazuje na plik w którym zdefiniowaliśmy wygląd widżetu.
  • Configure - Atrybut, który opcjonalnie może wskazywać na Aktywność, która powinna być uruchomiona wraz z dodaniem widżetu do głównego ekranu. Dzięki temu możemy np. uruchomić Aktywność odpowiedzialną za konfigurację widżetu.

Oto przykładowa konfiguracja, która zostanie wykorzystana w naszym przykładzie:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:minWidth="72dp"
	android:initialLayout="@layout/widget"
	android:minHeight="72dp"
	android:updatePeriodMillis="3600000">
</appwidget-provider>

Innymi słowy nasz widżet powinien zajmować jedno pole (kratkę) ekranu i aktualizować się co godzinę. Layout na który wskazujemy to plik widget.xml, który skonfigurowaliśmy w poprzednim akapicie.

Rejestracja widżetu w systemie

Aby nasz widżet zadziałał powinniśmy zarejestrować go poprzez plik AndroidManifest.xml. Widżet jest elementem typu Receiver, dzięki czemu możemy przypisać mu filtry intencji, za pomocą których możemy aktualizować jego zawartość.

<receiver android:name=".MyAppWidget">
	<intent-filter>
		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
	</intent-filter>
	<meta-data
		android:name="android.appwidget.provider"
		android:resource="@xml/widget_provider" />
</receiver>

Aby wszystko działało tak jak należy powinniśmy spełnić następujące warunki:

  • Należy ustawić przynajmniej jeden filtr intencji, który powinien obsługiwać  android.appwidget.action.APPWIDGET_UPDATE (ACTION_APPWIDGET_UPDATE).
  • Receiver powinien posiadać również znacznik <meta-data> z atrybutem android:name=”android.appwidget.provider” oraz android:resource wskazującym na plik konfiguracyjny naszego widżetu.

Kod źródłowy widżetu

Do budowy widżetu wykorzystywana jest klasa AppWidgetProvider, po której powinniśmy dziedziczyć podczas jego tworzenia. Jedną z metod, jakie posiada jest onUpdate(…) wywoływane w momencie aktualizacji widżetu (która odbywa się cyklicznie, zgodnie z ustawionym czasem).

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

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
	}
}

Operacje na elementach UI widżetu

O ile w Aktywności wszelkie operacje na elementach interfejsu sprowadzają się do wywoływania metod na ich obiektach tak w widżecie czynność ta jest nieco bardziej skomplikowana. Ma to związek z dwoma czynnikami:

  • Ze względów wydajnościowych widżet nie posiada własnego procesu, tylko uruchomiony jest w procesie naszego ekranu głównego. Dlatego też do komunikacji z elementami UI wykorzystuje się obiekt RemoteViews. Daje to procesowi ekranu głównego możliwość wykonywania operacji na UI z takimi samymi prawami jakie miałaby oryginalna aplikacja.
  • W związku z tym, że system Android pozwala na wyświetlanie kilku kopii widżetu na jednym ekranie, należy w jakiś sposób poradzić sobie z aktualizacją ich wszystkich na raz. W tym wypadku z pomocą przychodzi nam obiekt klasy AppWidgetManager.

Cykliczna aktualizacja widżetu

Zajmiemy się teraz implementacją metody onUpdate(…). Będzie ona aktualizowała zawartość widżetu wypełniając go szczęśliwą liczbą. :)

public class MyAppWidget extends AppWidgetProvider {
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		super.onUpdate(context, appWidgetManager, appWidgetIds);
		for (int i = 0; i < appWidgetIds.length; i++) {
			int appWidgetId = appWidgetIds[i];
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			String luckyNumber = "Your lucky number is: " + getLuckyNumber();
			views.setTextViewText(R.id.tvWidgetText, luckyNumber);
			appWidgetManager.updateAppWidget(appWidgetId, views);
		}
	}

	private int getLuckyNumber() {
		Random r = new Random();
		return r.nextInt(100);
	}

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
	}
}

Idąc kolejno:

  • Linijka 15 - zaczynamy iterację po wszystkich wyświetlanych widżetach. Ich identyfikatory przechowywane są w tablicy appWidgetIds dostarczonej jako argument metody.
  • Linika 17 – tworzymy obiekt RemoteViews.
  • Linijka 19 – wykorzystując jedną z metod set…() obiektu RemoteViews ustawiamy wartość pola tekstowego.
  • Linijka 20 – przekazujemy nasz obiekt RemoteViews z zapisanymi operacjami do obiektu appWidgetManager, który na podstawie tych danych zaktualizuje widżet.

Aktualizacja widżetu na rządanie

Drugą rzeczą którą się zajmiemy jest ręczne aktualizowanie widżetu. Dzięki temu treść będzie mogła być aktualizowana w dowolnym momencie, przez jakikolwiek komponent, który potrafi wysyłać Intencje.

Zaczniemy od zdefiniowania naszej własnej akcji dla Intencji, ustawienia jej w filtrze i obsłużenia w metodzie onReceive() naszego widżetu.

Wewnątrz pliku AndroidManifest.xml dodajemy:

<receiver android:name=".MyAppWidget">
	<intent-filter>
		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
		<action android:name="pl.froger.hello.widget.UPDATE_ON_DEMAND"/>
	</intent-filter>
	<meta-data
		android:name="android.appwidget.provider"
		android:resource="@xml/widget_provider" />
</receiver>

W klasie naszego widżetu nadpisujemy onReceive():

public class MyAppWidget extends AppWidgetProvider {
	public static final String UPDATE_ON_DEMAND = "pl.froger.hello.widget.UPDATE_ON_DEMAND";

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		super.onUpdate(context, appWidgetManager, appWidgetIds);
		for (int i = 0; i < appWidgetIds.length; i++) {
			int appWidgetId = appWidgetIds[i];
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			String luckyNumber = "Your lucky number is: " + getLuckyNumber();
			views.setTextViewText(R.id.tvWidgetText, luckyNumber);
			appWidgetManager.updateAppWidget(appWidgetId, views);
		}
	}

	private int getLuckyNumber() {
		Random r = new Random();
		return r.nextInt(100);
	}

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
		if(intent.getAction().equals(UPDATE_ON_DEMAND)) {
			updateWidget(context);
		}
	}
}

Na końcu dodajemy jeszcze naszą metodę updateWidget(Context context), która jest bardzo podobna do implementowanej przez nas metody onUpdate(). Czytelnikom pozostawiam kwestię refaktoryzacji kodu. :)

	private void updateWidget(Context context) {
		AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
		ComponentName componentName = new ComponentName(context, MyAppWidget.class);
		int[] appWidgetIds = appWidgetManager.getAppWidgetIds(componentName);
		for (int i = 0; i < appWidgetIds.length; i++) {
			int appWidgetId = appWidgetIds[i];
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			String luckyNumber = "Lucky number on demand: " + getLuckyNumber();
			views.setTextViewText(R.id.tvWidgetText, luckyNumber);
			appWidgetManager.updateAppWidget(appWidgetId, views);
		}
	}

Teraz by zaktualizować ręcznie nasz widżet wystarczy rozesłać intencję w podobny do poniższego sposób:

Intent intent = new Intent();
intent.setAction(MyAppWidget.UPDATE_ON_DEMAND);
sendBroadcast(intent);

W naszej aplikacji powyższy kod podpiąłem pod przycisk wyświetlany w głównej Aktywności naszej aplikacji. Całość będzie można podejrzeć na githubie android4devs.

Tym też sposobem zakończyliśmy tworzenie naszego pierwszego widżetu. :) Oto kompletny kod źródłowy pliku MyAppWidget.java:

package pl.froger.hello.widgets;

import java.util.Random;

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 MyAppWidget extends AppWidgetProvider {
	public static final String UPDATE_ON_DEMAND = "pl.froger.hello.widget.UPDATE_ON_DEMAND";

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		super.onUpdate(context, appWidgetManager, appWidgetIds);
		for (int i = 0; i < appWidgetIds.length; i++) {
			int appWidgetId = appWidgetIds[i];
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			String luckyNumber = "Your lucky number is: " + getLuckyNumber();
			views.setTextViewText(R.id.tvWidgetText, luckyNumber);
			appWidgetManager.updateAppWidget(appWidgetId, views);
		}
	}

	private int getLuckyNumber() {
		Random r = new Random();
		return r.nextInt(100);
	}

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
		if(intent.getAction().equals(UPDATE_ON_DEMAND)) {
			updateWidget(context);
		}
	}

	private void updateWidget(Context context) {
		AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
		ComponentName componentName = new ComponentName(context, MyAppWidget.class);
		int[] appWidgetIds = appWidgetManager.getAppWidgetIds(componentName);
		for (int i = 0; i < appWidgetIds.length; i++) {
			int appWidgetId = appWidgetIds[i];
			RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
			String luckyNumber = "Lucky number on demand: " + getLuckyNumber();
			views.setTextViewText(R.id.tvWidgetText, luckyNumber);
			appWidgetManager.updateAppWidget(appWidgetId, views);
		}
	}
}

Zrzuty ekranu

Kompletny kod źródłowy

Cały projekt dostępny jest na naszym githubie – HelloWidget.

Komentarze (0) Subskrybuj

Prześlij komentarz

Zaloguj się lub skorzystaj z profilu:

[rpxlogin redirect="http://www.android4devs.pl" prompt="" style="large"]

Możesz również zostawić komentarz bez rejestracji, korzystając z poniższego formularza:

Musisz być zalogowany aby móc pisać komentarze.