Archiwa miesięczne: Maj 2016

Drawable resources

Jednym z mankamentów widżetu Losowy Fakt z Wikipedii, był fakt, że do momentu zmiany tekstu, nie widzieliśmy, czy w widżet kliknęliśmy.  Na szczęśćie Android udostępnia gotowe rozwiązanie tego problemu. W tym celu możemy wykorzystać drawable resources, czyli grafiki, do których inne pliki xml mogą się odwoływać poprzez atrybut android:drawable. Przykładem drawable resource jest StateListDrawable

StateListDrawable wyświetla różne grafiki dla odpowiednio zdefiniowanych stanów aplikacji. Dla  Losowego Faktu z Wikipedii możemy wskazać dwa stany: stan, w którym widżet jest wciśnięty i stan, w którym ciekawostka jest poprostu wyświetlana. Teraz wystarczy, że przy wwciśnięciu zmienimy tło widżetu na błękitne:

StateListDrawable można definiować jako xml, którego korzeniem jest element <selector>. Stany oraz odpowiadające im grafiki są zdefiniowane w elementach <item>. Aby zdefiniować tryb widżetu na wciśnięty należy ustawić wartość atrybutu  android:state_pressed.

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/appwidget_bg_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/appwidget_bg"/>
</selector>

Powyższy plik app_widget_bg_clickable.xml  znajduje się w katalogu res/drawable. W tym samym katalogu powinny znajdować sie grafiki appwidget_bg.png i appwidget_bg_pressed.png. Oba pliki wzięłam ze strony:
https://developer.android.com/guide/practices/ui_guidelines/widget_design.htmlPoniżej znajduje się fragment main_widget.xml z definicją interfejsu użytkownika. Wartość atrybutu android:background zawiera odniesienie do xml-a appwidget_bg_clickable. Warto zapamiętać atrybut android:clickable. Ja na początku nie miałam go zdefiniowanego i zmiana tła po wciśnięcu nie działała.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable = "true"
    android:padding="@dimen/widget_margin"
    android:id = "@+id/layout"
    android:layout_margin="@dimen/widget_margin"
    android:background="@drawable/appwidget_bg_clickable">
Reklamy

Style

Dni coraz piękniejsze, coraz ciężej spędzać je z komputerem. Niemniej jednak, jak mówią starzy Indianie:

Lepiej 2% dziś niż 200% jutro.

Dlatego postanowiłam popracować trochę nad wyglądem widżeta Losowy fakt z WikipediiSC20160522-144021 (z dzisiejszej daty) – uważne oko czytelnika na pewno dostrzeże subtelne róźnice w nowym wyglądzie widżeta. Na początek kilka słów o stylach w Androdzie.

Przy pisaniu aplikacji na Androida, style pełnią podobną funkcję do CSS-a przy tworzeniu stron internetowych. Umożliwiają zebranie atrybutów dotyczących wyglądu widoku w oddzielnym pliku xml – czyli oddzielenie wyglądu widoku od jego zawartości.

Tak wyglądała definicja widoku TextView z nagłówkiem widżetu Today in Wikipedia w pliku main_widget.xml, który definiuje UI widżetu:

 

<TextView
    android:id="@+id/header"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Today in Wikipedia:"
    android:textColor="#ff000000"
    android:textSize="12sp"
    android:textStyle="bold"
    />

Atrybuty takie jak wymiary układu, kolor, rozmiar i format czcionki można zdefiniować jako Styl (u mnie to styl o nazwie Header). Wtedy ta sama definicja będzie wyglądać tak:

<TextView
    android:id="@+id/header"
    style="@style/Header"
    android:text="Today in Wikipedia:"
/>

A jak zdefiniować styl Header? Style należy definiować w pliku z rozszerzeniem .xml w katalogu  res/values/.Nazwa pliku jest dowolna, ale przy rozpoczynaniu projektu, Android Studio tworzy dla nas plik style.xml. Korzeniem XML-a ze stylami  musi być <resources>.Dodając nowy styl do dokumentu, dodajemy element <style>. Wybrane atrybuty umieszczamy w elementach <item>. Poniżej znajduje się plik styles.xml Losowego Faktu z Wikipedii. Styl Header odpowiada TextView z nagłówkiem widżetu, a styl FunFact TextView z losową ciekawostką z dzisiejszej daty.

<resources>
    <style name="Header" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">#ff000000</item>
        <item name="android:textSize">12sp</item>
        <item name="android:textStyle">bold</item>
    </style>
    <style name="FunFact" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">#ff000000</item>
        <item name="android:textSize">10sp</item>
    </style>
</resources>

Style mogą po sobie dziedzić. Atrybut parent elementu <style> umożliwia wskazanie stylu, po którym dany element dziedziczy.

Zamiast definiować style dla poszczególnych widoków, można też definiować je jako motywy (themes) dla całych aktywności lub aplikacji. We’ll get there.

Losowy fakt z Wikipedii wersja alfa

Bardzo serdecznie zapraszam do obejrzenia dema aplikacji Random Wikipedia Fact

 

Co zrobiłam?

  1. Przy pomocy API Wikimedia pobieram plik JSON z zawartością strony: https://en.wikipedia.org/wiki/MMMM_dd
  2. Napisałam klasy JsonParse i RandomFact przy pomocy których otrzymują listę wydarzeń  z danego dnia i losuję z nich jedno
  3. Napisałam klasę MainWidget do zarządzania widżetami oraz klasę OnUpdateService, definiującą usługę, przy pomocy której widżety są aktualizowane.

Stuff to be done (czyli co jeszcze powinnam zrobić):

  1. Nie szata zdobi człowieka, ale już prosty widżet powinien wyglądać lepiej niż mój (because it’s 2016)
  2. Zdarzają się szczególne przypadki, w których losową ciekawostką jest seria znaków „===”
  3.  Porządek na githubie 🙂

 

Usługi

Dziś o komponencie aplikacji, który umożliwia wykonowynanie operacji w tle, czyli o usługach. Dlaczego? Dlatego, że za aktualizację widżetu może odpowiadać usługa.  Wtedy nie musimy się martwić, że AppWidgetProvider zostanie zamknięty z powodu błędu  Application Not Responding (ANR) .

Usługi nie udostępniają interfejsu użytkownika. Są one uruchomiane przez inny komponenty aplikacji (np AppWidgetProvider) i działają dopóki nie zostaną explicite zakończone, nawet po zatrzymaniu aplikacji.

Usługi nie są osobnymi procesami ani wątkami – działają w głównym wątku naszych aplikacji. Dzięki usługom:

  1. możemy powiedzieć systemowi: teraz zacznij robić coś tle,   wywołując metodę: Context.startService()
  2. możemy udostępniać funkcjonalności danej aplikacji innym aplikacjom wywołując metodę: Context.bindService()

W widżecie Losowy Fakt z Wikipedii, wykorzystamy pierwszą możliwość. Dlatego w metodzie onUpdate()  wywołamy metodę  startService(), przekazując jej odpowiednią intencję.

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

    ComponentName thisWidget = new ComponentName(context,
                MainWidget.class);
    int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
    Intent intent = new Intent(context.getApplicationContext(),
                OnUpdateService.class);
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, allWidgetIds);
    context.startService(intent);

}

Po wywołaniu tej metody automatycznie wywoływana jest metoda onStart(Intent, int,int),która jako argument przyjmuje:

  • intencję przekazaną w startService()
  • liczbę całkowitą odpowiedającą identyfikatorowi żądania.

W kodzie metody onStart() w klasie onUpdateService (która rozszerza klasę Service) zaimplementowana jest logika uaktualniania widzętu, która poprzednio zawarta była w metodzie onUpdate()

public void onStart(Intent intent, int startId) {
    final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this
            .getApplicationContext());
    int[] allWidgetIds = intent
            .getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
    final String url = getRandomUrlForDate();
    Log.w("stefan", url);
    for (final int widgetId : allWidgetIds) {
        RequestQueue queue = Volley.newRequestQueue(this
                .getApplicationContext());
        final RemoteViews remoteViews = new RemoteViews(this
                .getApplicationContext().getPackageName(),
                R.layout.main_widget);
        JsonObjectRequest jsObjRequest = new JsonObjectRequest
                (Request.Method.GET, url, null, new Response.Listener<JSONObject>() {

                    @Override
                    public void onResponse(JSONObject response) {
                        JsonParse jp = new JsonParse(response);
                        String allEvents = jp.parseJson();
                        RandomFact rf = new RandomFact(allEvents);
                        CharSequence result = rf.randomEvent();
                        remoteViews.setTextViewText(R.id.update, result);
                        appWidgetManager.updateAppWidget(widgetId, remoteViews);
                    }
                }, new Response.ErrorListener() {

                    @Override
                    public void onErrorResponse(VolleyError error) {
                        CharSequence errorToPrint = error.toString();
                        remoteViews.setTextViewText(R.id.update, errorToPrint);

                        appWidgetManager.updateAppWidget(widgetId, remoteViews);
                    }
                });

        queue.add(jsObjRequest);
        Intent clickIntent = new Intent(this.getApplicationContext(), MainWidget.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.actionButton, pendingIntent);
        appWidgetManager.updateAppWidget(widgetId, remoteViews);
    }
    stopSelf();
    super.onStart(intent, startId);
}

 

 

 

Pending intent

Dziś, zgodnie z zapowiedzią, parę słów o metodzie onUpdate() widżetu, który generuje losową liczbę w ustalonych odstępach czasu lub po wciśnięciu przycisku Refresh

public class MainWidget extends AppWidgetProvider {
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

        for (int widgetId : allWidgetIds) {
            int number = (new Random().nextInt(100));
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                    R.layout.main_widget);
            remoteViews.setTextViewText(R.id.update, String.valueOf(number));
            Log.w("WidgetExample", String.valueOf(number));
            Intent intent = new Intent(context, MainWidget.class);
            intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.actionButton, pendingIntent);
            appWidgetManager.updateAppWidget(widgetId, remoteViews);
        }
    }
}

W metodzie onUpdate(), w pętli for, przechodzimy przez id wszystkich widżetów, które są zdefiniowane przez klasę MainWidget . Dzięki temu, jeśli użytkownik stworzy więcej instancji widżetu MainWidget, wszystkie one zostaną uaktualnione.W tej metodzie są zdefiniowane dwie rzeczy:

  1. Uaktualnienie zawartości pola tekstowego, za który odpowiadają: remoteViews.setTextViewText(), updateAppWidget()
  2.  Przekazanie intencji, która ma wywołać metodę onUpdate(),, po wciśnięciu przycisku Refresh

Pierwsza część jest raczej straightforward. Do wykonania drugiej, wykorzystujemy klasę PendingIntent. Obiekty z tej klasy to odroczone w czasie intencje, które można przekazać do innych aplikacji, dając tym aplikacjom prawo do wykonania wskazanych przez te intencje akcji. Aby utworzyć obiekt należący do tej klasy, trzeba wywołac jedną z  następującyc metod:

getActivity(Context, int, Intent, int), getActivities(Context, int, Intent[], int), getBroadcast(Context, int, Intent, int)getService(Context, int, Intent, int)

Ponieważ intencję chcemy wysłać do wszystkich widżetów MainWidget, tworzymy ją przez metodę getBroadcast(Context context, int requestCode, Intent intent, int flags).

Argumenty tej metody to kolejno:

1. kontekst, w którym komunikaty mają zostać rozesłane

2. kod żądania

3. intencja, która ma zostać rozesłana – w naszym przypadku to wysłanie komunikatu pora zaktualizować widżet do widżetów, których id znajduje się w liście appWidgetIds:

  Intent intent = new Intent(context, MainWidget.class);
  intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
  intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);

 

4. jedna z dostępnych flag, w naszym przypadku jest nią FLAG_UPDATE_CURRENT, która mówi: jeśli dany obiekt PendingIntent już istnieje, zostanie on utrzymany, ale dane przekazywane w intencji, zostaną uaktualnione (w naszym przypadku, jest to lista id wszystkich widżetów MainWidget)

Metoda setOnClickPendingIntent()  umożliwia przypisanie obiektu PendingIntent do elementu interfejsu, po którego wciśnięciu rozesłana zostanie odpowiednia intencja.

 

 

już byłam w ogródku, już witałam się z gąską

Zanim rozpoczęłam ten projekt, zrobiłam mały wywiad u przyjaciół, jakie proste aplikacje chcieliby mieć na swoim telefonie. Mój Przyjaciel Jacek rzucił pomysł na stroboskop z latarki w telefonie z suwakiem, ile minut ma blyskac i z jaka czestotliwoascia. Chyba nie wyglądałam na przekonaną, bo dodał  losowy fakt z wikipedii, z dzisiejszą datą x lat temu, oczywiście w formie widżeta. Tę drugą aplikację wzięłam od razu na klatę. Wszystkie te poprzednie, drobne aplikacje zbliżały mnie do aplikacji dla Jacka. W tym tygodniu czułam się naprawdę blisko, bo przecież.

  1. Umiem już pobrać losowy fakt z danego dnia z Wikipedii.
  2. Znam już jakieś podstawy Androida: intencje, aktywności, elementy interfejsu i takie takie
  3.  w szczególności wiem, co to jest widżet

Ponadto w różnych (przynajmniej dwóch) tutorialach jest nawet przykład kodu widżeta, który wyświetla losową liczbę i uaktualnia się co zadany odstęp czasu lub po wciśnięciu przycisku. Naprawdę byłam przekonana, że w tym tygodniu będę miała analogiczny widżet z ciekawostkami z Wikipedii. Tylko u mnie kod z losową liczbą nie działał (btw czy są też takie koszulki?) przez dwa długie wieczory. Nie działał, jak zaczynałam pisać tego posta, a nawet nie działał, jak pisałam przedostatnie zdanie. Przycisk Refresh nie robił nic. No może nie do końca nic – im częściej go wciskałam, tym mocniej zaciskałam zęby i tym mocniej chciałam rzucić telefonem o ścianę. Pisanie tego bloga wymaga od mnie trochę szczerości w przyznawaniu się do rzeczy, do których nie lubię się przyznawać. Tak jest i teraz kiedy piszę: nie mam pojęcia, co takiego sprawiło, że zaczął działać. Naprawdę jestem przekonona, że kod metody onUpdate(), która odświeża widżeta, wyglądał dokładnie tak jak poniżej, przez większość czasu, kiedy go z negatywnym skutkiem testowałam. Wygląda tak również teraz.  A ponieważ:

  1. teraz działa (patrz zdjęcia i zaufanie do mnie)
  2. emocje ze mnie nie zeszły na tyle, żeby odkomentować kod z wersją z Wikipedią i sprawdzić, co się stanie
  3. ten weekend chce spędzić na Kurpiach z dala od komputera

w tym tygodniu poprzestanę na tej aplikacji. Za to następnym poście napiszę, co  dokładnie w poniższej metodzie się dzieje

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        ComponentName thisWidget = new ComponentName(context, MainWidget.class);
        int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
        for (int widgetId : allWidgetIds) {
            int number = (new Random().nextInt(100));
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                    R.layout.main_widget);
            remoteViews.setTextViewText(R.id.update, String.valueOf(number));
            Log.w("WidgetExample", String.valueOf(number));
            Intent intent = new Intent(context, MainWidget.class);
            intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.actionButton, pendingIntent);
            appWidgetManager.updateAppWidget(widgetId, remoteViews);
        }
    }