Привет, хаброчитатели! Предыстория: с недавних пор отслеживаю рост курса криптовалюты bitcoin(BTC). Раньше для просмотра котировок заглядывал на сайт какой-нибудь биржи, но гораздо удобнее иметь на рабочем экране смартфона небольшой виджет, который отобразит актуальную информацию.
На маркетах представлена куча разнообразных виджетов, показывающих курсы криптовалют. Но гораздо интереснее создать что-то свое. В этой статье я кратко опишу свой опыт создания виджета для гаджета под управлением ОС Android.
Для понимания материала статьи желательно начальное знакомство с Java и Android-разработкой в IDE Android Studio. Если нет — Вам сюда: Android developers.
Мой небольшой виджет будет отображать цену биткоина в долларах и время, на которое актуальна данная цена. Информация будет загружаться с русскоязычной биржи BTC-e, поскольку эта площадка отдает курс в удобном JSON-формате в ответ на get-запрос по url btc-e.nz/api/3/ticker/btc_usd.
Пример ответа биржи:
Итак, для начала разработки создаем в IDE новый проект с помощью подходящего шаблона. Выбираем «Start a new Android Studio project», затем вводим имя и расположение проекта, выбираем целевое устройство и версию API, далее нужен пункт «Add no activity».
После того, как откроется IDE workspace, создадим пустой виджет с помощью встроенных шаблонов. Для этого нужно в дереве файлов проекта вызвать контекстное меню на папке app и выбрать пункт New → Widget → App Widget.
Будут созданы несколько файлов, из которых особенно интересны три.
Первый — xml-файл (res → xml → btcwidget_info.xml) с основными параметрами виджета:
Параметр initialLayout задает имя xml-файла с визуальной разметкой виждета. minHeight и min-Width — минимальные размеры добавляемого виджета, updatePeriodMillis — время обновления информации в мс, но не чаще раза в полчаса (параметр 10 мс все равно воспринимается как минимальные 30 мин).
Второй xml-файл (res → layout → btcwidget.xml) содержит параметры визуального отображения виджета (разметка визуальных элементов).
В нем находится описание одного визуального элемента TextView внутри разметки RelativeLayout (Layouts):
Самое интересное — третий файл с шаблоном исходного кода виджета на языке java. В таком виде шаблон не несет функциональной нагрузки, однако по комментариям кода можно понять, какой метод когда выполняется и за что отвечает:
Стоит сразу сказать, что ОС Android позволяет создавать большое количество экземпляров нашего виджета и их поведение будет целиком описываться приведенным выше кодом. Для более глубокого понимания поведения и жизненного цикла виджетов рекомендую ознакомиться со статьей Android Widget. Ниже в комментариях кода я объясню лишь некоторые моменты.
Столкнулся со следущей сложностью: система позволяет виджету обновляться (методом updateAppWidget) не чаще чем раз в 30 минут по соображениям экономии батареи. Но мне хотелось иметь возможность обновлять данные в нужный момент и я нашел способ обойти это ограничение. Для этого виджет был запрограммирован к принудительному обновлению по клику на него. Реализовал такое действие следующим образом: по нажатию на виджет в систему отправляется интент (Intent), который ловится самим же виджетом и обрабатывается запуском обновления данных. Если кто-то знает способ проще — буду рад советам в комментариях.
Содержимое класса HTTPRequestThread.java:
Парсер ответа с сервера — JSONParser.java:
Вот так выглядит описанный выше виджет:
Полный исходный код доступен здесь: github.com/hakeydotom/BTCPriceWidget
На маркетах представлена куча разнообразных виджетов, показывающих курсы криптовалют. Но гораздо интереснее создать что-то свое. В этой статье я кратко опишу свой опыт создания виджета для гаджета под управлением ОС 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