plusone

28. Mai 2012

Widget - Multi-Touch-Action

Hallo zusammen,

und wilkommem am Pfingstmontag zum weiterem Teil meiner Widget-Reihe.

Heute zeige ich euch, wie ihr mehrere Buttons in euer Widget einbauen könnt und jeweils mit einem Event belegt.

Dieses Tutorial setzt Kenntnisse in Widget-Entwicklung voraus und basiert auf meinem Beitrag Zeitliche- und Interaktive-Updates des Widgets.

Entwickelt und getestet mit:
  • Eclipse Indigo Service Release 2
  • Android SDK Tools rev. 17
  • Android version 2.3.6
  • MB860 (Atrix)
  • ROM: Marko!s Algor!thm V0.5

Beginnen wir mit dem Layout, das ich diesmal benutzt habe.
Das wichtigste sind die Buttons!

layout/widget_view_sites.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/background"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="4dp"
    android:layout_marginLeft="8dp"
    android:layout_marginRight="8dp"
    android:layout_marginTop="4dp"
    android:background="@drawable/widget_background" >

    <Button
        android:id="@+id/buttonPrevious"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="4dp"
        android:padding="8dp"
        android:text="previous" />

    <Button
        android:id="@+id/buttonNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_marginRight="4dp"
        android:padding="8dp"
        android:text="next" />

    <TextView
        android:id="@+id/textHeader"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginTop="4dp"
        android:background="@drawable/widget_item_background_top"
        android:padding="8dp"
        android:text="Platzhalter header"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/textBody"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/buttonPrevious"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/textHeader"
        android:layout_margin="4dp"
        android:background="@drawable/widget_item_background"
        android:padding="8dp"
        android:text="Platzhalter body"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/textFooter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/buttonPrevious"
        android:layout_alignTop="@+id/buttonPrevious"
        android:layout_marginBottom="4dp"
        android:layout_toLeftOf="@+id/buttonNext"
        android:layout_toRightOf="@+id/buttonPrevious"
        android:background="@drawable/widget_item_background"
        android:padding="8dp"
        android:text="Platzhalter footer"
        android:textAppearance="?android:attr/textAppearanceSmall" />

</RelativeLayout>

drawable/widget_background.xml:
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <stroke
        android:width="4dp"
        android:color="#FF666666" />

    <gradient
        android:angle="135"
        android:endColor="#FF000000"
        android:startColor="#FF000000" />

    <corners android:radius="5dp" />

</shape>
drawable/widget_item_background.xml:
 <?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <gradient
        android:angle="45"
        android:endColor="#EE0099CC"
        android:startColor="#EE0099CC" />

</shape>
drawable/widget_item_background_top.xml:
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <gradient
        android:angle="45"
        android:endColor="#EE0099CC"
        android:startColor="#EE0099CC" />

    <corners
        android:topLeftRadius="4dp"
        android:topRightRadius="4dp" />

</shape>

Wenn ich mir das anschaue, denk ich immer, das ist zuviel code für sowenig Auswirkung. Aber das sei wohl jedem Layoutdesigner selbst überlassen.

In der Manifest müssen die Action-Events als Intent-Filter eingetragen werden!
Bei mir sind es zwei Variablen aus der Klasse BasisAppWidgetProvider.

AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="de.alexroid.widgetsample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="9" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <receiver android:name="de.alexroid.widgetsample.BasisAppWidgetProvider" >
            <intent-filter>
                <action android:name="android.appwidget.action.
APPWIDGET_UPDATE" />
                <action android:name="de.alexroid.widgetsample.BasisAppWidgetProvider.
ACTION_WIDGET_NEXT" />
                <action android:name="de.alexroid.widgetsample.BasisAppWidgetProvider.
ACTION_WIDGET_PREVIOUS" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_view_sites" />
        </receiver>

        <service android:name="de.alexroid.widgetsample.UpdateWidgetService" />
    </application>

</manifest>

In der Konfigurationsdatei steht wie gewohnt das Layout, das geladen wird.

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

</appwidget-provider>

Kommen wir zum WidgetProvier. Dieser enthält zwei verschiedene PendingIntents, diese werden ausgelöst, sobald die Buttons gedrückt worden sind.

BasisAppWidgetProvider:
package de.alexroid.widgetsample;

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

public class BasisAppWidgetProvider extends AppWidgetProvider {

    //custom action-Bezeichnungen für zwei verschiedene Events
    public static final String ACTION_WIDGET_NEXT = "next";
    public static final String ACTION_WIDGET_PREVIOUS = "previous";

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
        //das geladene Layout
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.widget_view_sites);

        // Build the intent to call the service
        Intent intent = new Intent(context.getApplicationContext(),
                UpdateWidgetService.class);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);

        // Update the widgets via the service
        context.startService(intent);

        //Unterschiedliche PendigIntens für die verschiedene Buttons mit Events
        //Event "weiter"
        Intent activeNext = new Intent(context, BasisAppWidgetProvider.class);
        activeNext.setAction(ACTION_WIDGET_NEXT);
        activeNext.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);

        PendingIntent actionNextPendingIntent = PendingIntent.getBroadcast(
                context, 0, activeNext, PendingIntent.FLAG_UPDATE_CURRENT);
        //die dazugehörige Button-ID
        remoteViews.setOnClickPendingIntent(R.id.buttonNext,
                actionNextPendingIntent);

        //Event "zurück"
        Intent activePrevious = new Intent(context,
                BasisAppWidgetProvider.class);
        activePrevious.setAction(ACTION_WIDGET_PREVIOUS);
        activePrevious.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
                appWidgetIds);

        PendingIntent actionPreviousPendingIntent = PendingIntent.getBroadcast(
                context, 0, activePrevious,
                PendingIntent.FLAG_UPDATE_CURRENT);
        //die dazugehörige Button-ID
        remoteViews.setOnClickPendingIntent(R.id.buttonPrevious,
                actionPreviousPendingIntent);

        appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent != null && intent.getExtras() != null) {
            int[] appWidgetIds = intent.getExtras().getIntArray(
                    AppWidgetManager.EXTRA_APPWIDGET_IDS);

            // Build the intent to call the service
            Intent intentService = new Intent(context.getApplicationContext(),
                    UpdateWidgetService.class);
            intentService.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,
                    appWidgetIds);
            intentService.addFlags(PendingIntent.FLAG_UPDATE_CURRENT);

            if (intent.getAction() != null
                    && (intent.getAction().equals(ACTION_WIDGET_NEXT) || intent
                            .getAction().equals(ACTION_WIDGET_PREVIOUS))) {
                intentService.putExtra("button_action", intent.getAction());

                // Nur Update durchführen, wenn einer der knöpfe gedrück worden ist
                context.startService(intentService);

            } else {
                super.onReceive(context, intent);
            }
        }
    }
}

Kommen wir zum Service, der das Widget im Hintergrund aktualisiert.
Dazu muss ich vorweg sagen, dass ich eine Datenerhaltungs- bzw. Datenbereitstellungsschicht benutze. Die Klasse DataController symbolisier diese Schicht und beinhaltet hardcodiert alle Datenobjekte, die für die Anzeige der Texte notwendig sind. Ein Datenobject also die Klasse DataObject beinhaltet den Text für Header, Body und Footer.

UpdateWidgetService:
package de.alexroid.widgetsample;

import java.util.Arrays;

import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.IBinder;
import android.widget.RemoteViews;

public class UpdateWidgetService extends Service {

    private static final String PREF_PAGE = "page";

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public void onStart(Intent intent, int startId) {
        if (intent != null) {
            ComponentName componentName = new ComponentName(getBaseContext(),
                    BasisAppWidgetProvider.class);

            String button_action = intent.getStringExtra("button_action");

            AppWidgetManager appWidgetManager = AppWidgetManager
                    .getInstance(this.getApplicationContext());

            int[] allWidgetIds = appWidgetManager
                    .getAppWidgetIds(componentName);

            // NPE beim Löschen des Widgets vom Home-Screen
            if (allWidgetIds != null) {
                //unser Layout als RemoteView beziehen
                RemoteViews remoteViews = new RemoteViews(this.getBaseContext()
                        .getPackageName(), R.layout.widget_view_sites);

                //laden und abspeichern der Seite, die geladen ist.       
                SharedPreferences preferences = getSharedPreferences(getClass()
                        .getSimpleName(), 0);

                int pagePosition = preferences.getInt(PREF_PAGE, 0);
                DataObject dataObject;

                //Event "weiter"
                if (button_action != null
                        && button_action
                                .equalsIgnoreCase(BasisAppWidgetProvider.ACTION_WIDGET_NEXT)
                        && pagePosition < 2) {
                    ++pagePosition;
                    dataObject = DataController.getInstance()
                            .getDataAtPostition(pagePosition);

                //Event "zurück"
                } else if (button_action != null
                        && button_action
                                .equalsIgnoreCase(BasisAppWidgetProvider.ACTION_WIDGET_PREVIOUS)
                        && pagePosition > 0) {
                    --pagePosition;
                    dataObject = DataController.getInstance()
                            .getDataAtPostition(pagePosition);

                //kein Event, dennoch laden/aktualisieren
                } else {
                    dataObject = DataController.getInstance()
                            .getDataAtPostition(pagePosition);
                }

                //Wenn Daten vorhanden, den passenden Views zuordnen
                //und neue aktuelle Seite speichern
                if (dataObject != null) {
                    String header = dataObject.getHeader();
                    String body = dataObject.getBody();
                    String footer = dataObject.getFooter();

                    remoteViews.setTextViewText(R.id.textHeader, header);
                    remoteViews.setTextViewText(R.id.textBody, body);
                    remoteViews.setTextViewText(R.id.textFooter, footer);

                    Editor editor = preferences.edit();
                    editor.putInt(PREF_PAGE, pagePosition);
                    editor.commit();
                }

                //Pendant zu invalidate
                appWidgetManager.updateAppWidget(allWidgetIds, remoteViews);
            }
        }

        stopSelf(startId);
        super.onStart(intent, startId);
    }
}

DataController:
package de.alexroid.widgetsample;

public class DataController {

    private static DataController instance;
    private DataObject[] dataObjects;

    public DataController() {
        this.dataObjects = new DataObject[3];

        this.dataObjects[0] = new DataObject("Kopf", "Körper", "Fuß");
        this.dataObjects[1] = new DataObject("head", "body", "foot");
        this.dataObjects[2] = new DataObject("rdm1", "rdm2", "rdm3");
    }

    public DataObject getDataAtPostition(int position) {
        DataObject res;
        if (position <= this.dataObjects.length) {
            res = dataObjects[position];
        } else {
            res = null;
        }

        return res;
    }

    public static DataController getInstance() {
        if (instance == null) {
            instance = new DataController();
        }

        return instance;
    }
}
DataObject:
package de.alexroid.widgetsample;

public class DataObject {

    private String header;
    private String body;
    private String footer;

    public DataObject(String header, String body, String footer) {
        this.header = header;
        this.body = body;
        this.footer = footer;
    }

    public String getHeader() {
        return header;
    }

    public String getBody() {
        return body;
    }

    public String getFooter() {
        return footer;
    }

    @Override
    public String toString() {
        return String.format("%s#header:%s;body:%s;footer:%s", getClass()
                .getSimpleName(), header, body, footer);
    }
}

Das ist viel Code für ein Resultat... das ich gebraucht habe :)
Wie steht es mit euch, fehlt da was?
Hier Screenshot, wie es bei mir aussieht.



Damit verabschiede ich mich für den Tag und wünsche euch noch frohe restliche Pfingsten.

regards
Alexander Fink

Keine Kommentare:

Kommentar veröffentlichen