Привет, хаброчитатели!
Предыстория: с недавних пор отслеживаю рост курса криптовалюты bitcoin(BTC). Раньше для просмотра котировок заглядывал на сайт какой-нибудь биржи, но гораздо удобнее иметь на рабочем экране смартфона небольшой виджет, который отобразит актуальную информацию.
На маркетах представлена куча разнообразных виджетов, показывающих курсы криптовалют. Но гораздо интереснее создать что-то свое:)
В этой статье я кратко опишу свой опыт создания виджета для гаджета под управлением ОС Android.
Для понимания материала статьи желательно начальное знакомство с Java и Android-разработкой в IDE Android Studio. Если нет — Вам сюда:) Android developers.
Мой небольшой виджет будет отображать цену биткоина в долларах и время, на которое актуальна данная цена. Информация будет загружаться с русскоязычной биржи BTC-e, поскольку эта площадка отдает курс в удобном JSON-формате в ответ на get-запрос по url btc-e.nz/api/3/ticker/btc_usd.
Пример ответа биржи:
{"btc_usd":{"high":880,"low":836,"avg":858,"vol":3774491.17766,"vol_cur":4368.01172,"last":871.999,"buy":872,"sell":870.701,"updated":1482754417}}
Итак, для начала разработки создаем в IDE новый проект с помощью подходящего шаблона. Выбираем «Start a new Android Studio project», затем вводим имя и расположение проекта, выбираем целевое устройство и версию API, далее нужен пункт «Add no activity».
После того, как откроется IDE workspace, создадим пустой виджет с помощью встроенных шаблонов. Для этого нужно в дереве файлов проекта вызвать контекстное меню на папке app и выбрать пункт New->Widget->App Widget.
Будут созданы несколько файлов, из которых особенно интересны три.
Первый — xml-файл (res->xml->btcwidget_info.xml) с основными параметрами виджета:
<source lang="xml"><?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialKeyguardLayout="@layout/btcwidget" android:initialLayout="@layout/btcwidget" android:minHeight="40dp" android:minWidth="110dp" android:previewImage="@drawable/example_appwidget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="180000" android:widgetCategory="home_screen"></appwidget-provider>
Параметр initialLayout задает имя xml-файла с визуальной разметкой виждета. minHeight и min-Width — минимальные размеры добавляемого виджета, updatePeriodMillis — время обновления информации в мс, но не чаще раза в полчаса (параметр 10 мс все равно воспринимается как минимальные 30 мин).
Второй xml-файл (res->layout->btcwidget.xml) содержит параметры визуального отображения виджета (разметка визуальных элементов).
В нем находится описание одного визуального элемента TextView внутри разметки RelativeLayout (Layouts):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#09C" android:padding="@dimen/widget_margin"> <TextView android:id="@+id/appwidget_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_margin="8dp" android:background="#09C" android:contentDescription="@string/appwidget_text" android:text="@string/appwidget_text" android:textColor="#ffffff" android:textSize="20sp" android:textStyle="bold|italic" /> </RelativeLayout>
Самое интересное — третий файл с шаблоном исходного кода виджета на языке java. В таком виде шаблон не несет функциональной нагрузки, однако по комментариям кода можно понять, какой метод когда выполняется и за что отвечает:
/** * Implementation of App Widget functionality. */ public class BTCWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { CharSequence widgetText = context.getString(R.string.appwidget_text); // Construct the RemoteViews object RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.btcwidget); views.setTextViewText(R.id.appwidget_text, widgetText); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // There may be multiple widgets active, so update all of them for (int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } @Override public void onEnabled(Context context) { // Enter relevant functionality for when the first widget is created } @Override public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } }
Стоит сразу сказать, что ОС Android позволяет создавать большое количество экземпляров нашего виджета и их поведение будет целиком описываться приведенным выше кодом. Для более глубокого понимания поведения и жизненного цикла виджетов рекомендую ознакомиться со статьей Android Widget. Ниже в комментариях кода я объясню лишь некоторые моменты.
Столкнулся со следущей сложностью: система позволяет виджету обновляться (методом updateAppWidget) не чаще чем раз в 30 минут по соображениям экономии батареи. Но мне хотелось иметь возможность видеть данные в реальном времени и я нашел способ обойти это ограничение. Для этого виджет был запрограммирован к принудительному обновлению по клику на него. Реализовал такое действие следующим образом: по нажатию на виджет в систему отправляется интент (Intent), который ловится самим же виджетом и обрабатывается запуском обновления данных. Если кто-то знает способ проще — буду рад советам в комментариях:)
/** * Implementation of App Widget functionality. */ public class BTCwidget extends AppWidgetProvider { private static final String SYNC_CLICKED = "btcwidget_update_action"; private static final String WAITING_MESSAGE = "Wait for BTC price"; public static final int httpsDelayMs = 300; //этот метод выполняется, когда пора обновлять виджет static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { //Объект RemoteViews дает нам доступ к отображаемым в виджете элементам: RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.btcwidget); //в данном случае - к TextView views.setTextViewText(R.id.appwidget_text, WAITING_MESSAGE); appWidgetManager.updateAppWidget(appWidgetId, views); String output; //запускаем отдельный поток для получения данных с сайта биржи //в основном потоке делать запрос нельзя - выбросит исключение HTTPRequestThread thread = new HTTPRequestThread(); thread.start(); try { while (true) { Thread.sleep(300); if(!thread.isAlive()) { output = thread.getInfoString(); break; } } } catch (Exception e) { output = e.toString(); } //выводим в виджет результат views.setTextViewText(R.id.appwidget_text, output); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { RemoteViews remoteViews; ComponentName watchWidget; remoteViews = new RemoteViews(context.getPackageName(), R.layout.btcwidget); watchWidget = new ComponentName(context, BTCwidget.class); //при клике на виджет в систему отсылается вот такой интент, описание метода ниже remoteViews.setOnClickPendingIntent(R.id.appwidget_text, getPendingSelfIntent(context, SYNC_CLICKED)); appWidgetManager.updateAppWidget(watchWidget, remoteViews); //обновление всех экземпляров виджета for (int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } //этот метод ловит интенты, срабатывает когда интент создан нажатием на виджет и //запускает обновление виджета @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); if (SYNC_CLICKED.equals(intent.getAction())) { AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); RemoteViews remoteViews; ComponentName watchWidget; remoteViews = new RemoteViews(context.getPackageName(), R.layout.btcwidget); watchWidget = new ComponentName(context, BTCwidget.class); remoteViews.setTextViewText(R.id.appwidget_text, WAITING_MESSAGE); //updating widget appWidgetManager.updateAppWidget(watchWidget, remoteViews); String output; HTTPRequestThread thread = new HTTPRequestThread(); thread.start(); try { while (true) { Thread.sleep(httpsDelayMs); if(!thread.isAlive()) { output = thread.getInfoString(); break; } } } catch (Exception e) { output = e.toString(); } remoteViews.setTextViewText(R.id.appwidget_text, output); //widget manager to update the widget appWidgetManager.updateAppWidget(watchWidget, remoteViews); } } //создание интента protected PendingIntent getPendingSelfIntent(Context context, String action) { Intent intent = new Intent(context, getClass()); intent.setAction(action); return PendingIntent.getBroadcast(context, 0, intent, 0); } }
Содержимое класса HTTPRequestThread.java:
package com.hakey.btcwidget; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Calendar; class HTTPRequestThread extends Thread{ private static final String urlString = "https://btc-e.nz/api/3/ticker/btc_usd"; String getInfoString() { return output; } private String output = ""; private void requestPrice() { try { URL url = new URL(urlString); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("GET"); BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); output = "Price: " + JSONParser.getPrice(response.toString()) + "\n" + getTimeStamp(); } catch (Exception e) { output = e.toString(); } } @Override public void run() { requestPrice(); } private String getTimeStamp() { Calendar calendar = Calendar.getInstance(); if(calendar.get(Calendar.MINUTE)>9) { return "Time: " + calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE); } else { return "Time: " + calendar.get(Calendar.HOUR_OF_DAY) + ":0" + calendar.get(Calendar.MINUTE); } } }
Парсер ответа с сервера — JSONParser.java:
package com.hakey.btcwidget; import org.json.JSONException; import org.json.JSONObject; class JSONParser { static String getPrice(String s) throws JSONException { String price; JSONObject obj = new JSONObject(s); JSONObject pairObj = obj.getJSONObject("btc_usd"); price = pairObj.getString("last"); return price; } }
Вот так выглядит описанный выше виджет:
Полный исходный код доступен здесь: github.com/hakeydotom/BTCPriceWidget
ссылка на оригинал статьи https://habrahabr.ru/post/318482/
Добавить комментарий