Smarthome App #14: Verlauf der Tageswerte anzeigen


16.06.2017  |  Tutorial, Smarthome App

Im letzten Beitrag wurde das Anzeigen des Sensorwerteverlaufs implementiert. Im heutigen Beitrag wird eine weitere Activity erstellt, die den Verlauf der Sensorwerte eines bestimmten Tages anzeigt.

Gradle-Dateien anpassen

Wenn zu viele Bibliotheken in das Projekt eingebunden werden, kann es passieren, dass das Standardlimit von 64.000 Methoden überschritten wird. Um dieses Problem zu beheben, öffnest du die Datei "build.gradle (Module: mobile)". Dort fügst du dem "dependencies"-Block die folgende Zeile hinzu:
compile 'com.android.support:multidex:1.0.1'
Als nächstes erstellst du im "android"-Block einen "dexOptions"-Block:
dexOptions {
    javaMaxHeapSize "4g"
}
Außerdem wird dem Block "defaultConfig"die folgende Zeile hinzugefügt:
multiDexEnabled true
Anschließend muss das Projekt mit den Änderungen synchronisiert werden. Klicke dazu pben rechts auf "Sync now".
Nun muss das Projekt mit den Gradle-Dateien synchronisiert werden.

Activity für den Tageswerteverlauf erstellen

Als nächstes erstellst die neue Activity. Dazu klickst du mit rechts auf eine Java-Datei im Hauptpackage und wählst "New -> Activity -> Empty Activity".
Als erstes muss eine neue Activity erstellt werden.
Gib der Activity den Namen "ValueCourseActivity" und klicke auf "Finish".
Die neue Activity definierst du, wie auf dem Bild zu sehen.
Wechsle nun zur Datei "activity_value_course.xml", um das Layout der Activity zu bearbeiten. Dort änderst du zuerst den Tag des äußersten Layouts von "android.support.constraint.ConstraintLayout" in "RelativeLayout" und fügst ihm drei neue Attribute hinzu:
android:id="@+id/frame"
android:padding="5dp"
android:background="@color/layoutBackground"
Danach erstellst du innerhalb des RelativeLayouts ein LineChart-Element, inkludierst die Lade-Animation und das Layout für den Fall, dass keine Daten geladen werden können:
<com.github.mikephil.charting.charts.LineChart
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/chart"
    android:visibility="gone"></com.github.mikephil.charting.charts.LineChart>

<include
    layout="@layout/loading_animation"
    android:visibility="gone"
    android:id="@+id/loading_animation">
</include>

<include
    layout="@layout/empty_item"
    android:visibility="gone"
    android:id="@+id/empty_item"/>

Activity implementieren

Jetzt wechselst du zur Datei "ValueCourseActivity.java". Dort werden als erstes ein paar globale Attribute angelegt:
private String location, devicetype, post_id, datum, einheit;
private LineChart chart;
Anschließend erweiterst du die Methode onCreate() mit dem folgenden Code:
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

//Intent abfangen
Intent intent = getIntent();
if(intent != null){
    location = intent.getStringExtra(MainActivity.EXTRA_LOCATION);
    devicetype = intent.getStringExtra(MainActivity.EXTRA_DEVICETYPE);
    post_id = intent.getStringExtra(MainActivity.EXTRA_ID);
    datum = intent.getStringExtra(MainActivity.EXTRA_START_DATE);
    einheit = intent.getStringExtra(MainActivity.EXTRA_UNIT);
}

chart = (LineChart) findViewById(R.id.chart);

setTitle(datum);

loadDayData(datum);
Die erste Zeile des Codes ermöglicht es, die Activity mit einem Pfeil in der ActionBar wieder zu verlassen. In den Zeilen 4-11 wird der Intent abgefangen und die übergebenen Werte den Klassenattributen zugewiesen. Anschließend wird die Variable "chart" mit dem LineChart aus dem Layout initialisiert, der Titel der Activity auf das anzuzeigende Datum gesetzt und die Methode loadDayData() mit dem anzuzeigenden Datum aufgerufen. Bevor die Methode loadDayData() implementiert wird, wird zuerst die geerbte Methode onSupportNavigateUp() überschrieben, damit die Activity mit dem Pfeil in der ActionBar verlassen werden kann:
@Override
public boolean onSupportNavigateUp(){
    super.onSupportNavigateUp();
    onBackPressed();
    getSupportFragmentManager().popBackStack();
    return true;
}
Nun wird die Methode loadDayData() implementiert. Sie gibt nichts zurück und erhält als Parameter einen String in der Form "DD.MM.JJJJ", der das anzuzeigende Datum erhält:
/**
 * Zeigt den Verlauf des Sensors am angegebenen Datum an
 * @param datum das Datum
 */
private void loadDayData(String datum){
    findViewById(R.id.loading_animation).setVisibility(View.VISIBLE);
    chart.setVisibility(GONE);

    final Map<String, String> requestData = new HashMap<>();
    requestData.put("action", "getgraphdata");

    requestData.put("room", location);
    requestData.put("type", devicetype);
    requestData.put("id", post_id);
    requestData.put("von", datum);
    requestData.put("bis", datum);
    requestData.put("username", SaveData.getUsername(getApplicationContext()));
    requestData.put("password", SaveData.getPassword(getApplicationContext()));

    HTTPRequest.sendRequest(getApplicationContext(), requestData, SaveData.getServerIp(getApplicationContext()),
            new HTTPRequest.HTTPRequestCallback() {
                @Override
                public void onRequestResult(String result) {
                    findViewById(R.id.loading_animation).setVisibility(GONE);
                    findViewById(R.id.empty_item).setVisibility(GONE);

                    switch(result){
                        case "wrongdata":
                            Dialogs.fehlermeldung("Anmeldung nicht m&ouml;glich!\nBitte logge dich erneut ein.",
                                    findViewById(R.id.frame));
                            break;
                        case "unknownuser":
                            Dialogs.fehlermeldung("Dieser Benutzer existiert nicht!\nBitte logge dich erneut ein.",
                                    findViewById(R.id.frame));
                            break;
                        case "nopermission":
                            Dialogs.fehlermeldung("Du hast dazu keine Berechtigung.",
                                    findViewById(R.id.frame));
                            break;
                        default:
                            try{
                                JSONObject object = new JSONObject(result);

                                JSONArray values = object.getJSONArray("values");

                                einheit = object.getString("einheit");

                                double[] valueList = new double[values.length()];
                                String[] timeList = new String[values.length()];

                                for(int i = 0; i < values.length(); i++){
                                    JSONObject c = values.getJSONObject(i);

                                    valueList[i] = c.getDouble("value");
                                    timeList[i] = c.getString("time");
                                }

                                showValueCourse(valueList, timeList);

                            }
                            catch(Exception e){
                                e.printStackTrace();

                                onError("");
                            }
                            break;
                    }
                }

                @Override
                public void onError(String msg) {
                    findViewById(R.id.loading_animation).setVisibility(GONE);

                    chart.setVisibility(GONE);

                    findViewById(R.id.empty_item).setVisibility(View.VISIBLE);
                    ((ImageView)findViewById(R.id.empty_icon)).setImageResource(Icons.getDrawerIcon("overview"));
                    ((TextView)findViewById(R.id.empty_title)).setText("Fehler beim Laden");
                    ((TextView)findViewById(R.id.empty_info)).setText("Die Verlaufsdaten konnten nicht geladen werden");
                }
            });
}
In der Methode wird als erstes die Lade-Animation sichtbar gemacht und das Diagramm versteckt. Dann wird eine Map namens "requestData" mit den Parametern für die HTTP-Request gefüllt und die Request ausgeführt. Ist die Abfrage erfolgreich, werden Lade-Animation und EmptyItem unsichtbar gemacht und anschließend in einem switch-Block zwischen den möglichen Rückgabefällen unterschieden. Falls die Rückgabe "wrongdata", "unknownuser" oder "nopermission" ist, wird eine entsprechende Fehlermeldung ausgegeben. Andernfalls wird in einem try-Block versucht, ein JSON-Objekt aus der Rückgabe des Servers zu erstellen. Aus diesem Objekt werden dann die Verlaufsdaten des angegebenen Tages mit den Uhrzeiten herausgelesen und anschließend die Methode showValueCourse() mit den Sensorwerten und den Uhrzeiten aufgerufen. Wird in diesem try-Block eine Exception geworfen, so wird der StackTrace ausgegeben und die Methode onError() mit einem leeren String aufgerufen. In der Methode onError() werden zuerst Lade-Animation und Diagramm unsichtbar gemacht. Anschließend wird das Empty-Item sichtbar gemacht und Icon, Titel und Info-Text mit Werten gefüllt. Als letztes wird in der ValueCourseActivity noch die Methode showValueCourse() angelegt, die die Erhaltenen Sensorwerte im Graphen anzeigt:
private void showValueCourse(double[] values, String[] time){
    ArrayList<Entry> vals = new ArrayList<>();

    final ArrayList<String> times = new ArrayList<>();

    for(int i = 0; i < values.length; i++){
        vals.add(new Entry(i, (float) values[i]));

        times.add(time[i]);
    }

    //Einstellungen des Graphen
    String title = "Werteverlauf";

    if(einheit != null){
        title = "Werteverlauf in "+einheit;
    }

    LineDataSet valueLine = new LineDataSet(vals, title);

    valueLine.setMode(LineDataSet.Mode.CUBIC_BEZIER);
    valueLine.setCubicIntensity(0.2f);

    valueLine.setAxisDependency(YAxis.AxisDependency.LEFT);
    valueLine.setColor(getResources().getColor(R.color.colorPrimary));

    valueLine.setDrawCircles(false);
    valueLine.setLineWidth(1.5f);
    valueLine.setDrawValues(false);

    Legend legend = chart.getLegend();
    legend.setTextSize(20);

    LineData chartLineData = new LineData(valueLine);

    //Graph anzeigen
    chart.animateY(500);
    chart.setData(chartLineData);

    chart.getDescription().setEnabled(false);

    chart.setScaleYEnabled(false);

    XAxis xAxis = chart.getXAxis();
    xAxis.setTextSize(12f);
    xAxis.setGranularity(1f);
    xAxis.setValueFormatter(new IAxisValueFormatter() {
        @Override
        public String getFormattedValue(float value, AxisBase axis) {
            try{
                return times.get((int) value);
            }catch(Exception e){
                e.printStackTrace();
                return String.valueOf(value);
            }
        }
    });

    chart.invalidate();
    chart.setVisibility(View.VISIBLE);
}
Die Methode hat den Rückgabewert "void" und erhält als Parameter ein double-Array mit Sensorwerten und ein String-Array mit den dazugehörigen Uhrzeiten. Innerhalb der Methode wird eine Entry-ArrayList angelegt, in der die Diagramm-Einträge gespeichert werden. Anschließend wird eine String-ArrayList angelegt, in die die Uhrzeiten kopiert werden. In einer for-Schleife wird nun durch das double-Array iteriert und die beiden ArrayLists werden mit Werten gefüllt. Danach werden die Einstellungen des Graphen festgelegt, der Titel gesetzt, dem Graphen die Daten zugewiesen, die er anzeigen soll, eine Animation definiert und gestartet und der Graph schließlich sichtbar gemacht.

Die Activity aufrufen

Damit die ValueCourseActivity auch aufgerufen wird, wechselst du zur Datei "GraphActivity.java" und scrollst ans untere Ende der Methode fillLineChart(). Hier wird im OnChartValueSelectedListener des Graphen ein Intent erstellt und damit die ValueCOurseActivity gestartet (bei mir in den Zeilen 278 - 285). Dieser Codeblock wurde im letzten Tutorial erstellt aber auskommentiert. Entferne nun die Kommentare, damit der Code ausgeführt wird:
//ValueCourseActivity starten
Intent intent = new Intent(GraphActivity.this, ValueCourseActivity.class);
intent.putExtra(MainActivity.EXTRA_START_DATE, dayList.get(index).getDate());
intent.putExtra(MainActivity.EXTRA_DEVICETYPE, devicetype);
intent.putExtra(MainActivity.EXTRA_ID, post_id);
intent.putExtra(MainActivity.EXTRA_LOCATION, location);
intent.putExtra(MainActivity.EXTRA_UNIT, einheit);
startActivity(intent);

Bugfix in MainActivity

Wechsle zur Datei "MainActivity.java" und ändere am Ende der Methode onCreate() den catch-Block für den Aufruf von onNavigationItemSelected() (bei mir in Zeile 106) ab, damit er so aussieht:
catch(Exception e){
    e.printStackTrace();
}
Das behebt einen Fehler, bei dem die App abstürzt, wenn das DrawerMenu null oder leer ist. Das war's auch schon wieder mit diesem Beitrag. Bei Fragen oder Problemen kannst du mir gern einen Kommentar hinterlassen.

Über den Autor


Sascha Huber

Hallo, ich bin Sascha, der Gründer von Smarthome Blogger.

Mit einer Leidenschaft für Technologie und einem Hintergrund als Software Engineer habe ich 2016 Smarthome Blogger gegründet. Mein Ziel war es schon immer, innovative Lösungen zu entdecken, die unser Leben einfacher und intelligenter gestalten können. In meinem beruflichen Leben arbeite ich täglich mit Software und Technik, aber auch in meiner Freizeit bin ich stets auf der Suche nach neuen technischen Spielereien und Möglichkeiten, mein Zuhause zu automatisieren und zu verbessern.

Auf Smarthome Blogger teile ich mein Wissen, meine Erfahrungen und meine Begeisterung für alles rund um das Thema Smarthome.



Dieser Beitrag hat dir gefallen?

Dann abonniere doch unseren Newsletter!