Разработка приложений под платформу Android становится все более и более массовой. Так, мне недавно довелось заниматься разработкой клиентской части такого приложения, фактически с нуля разбираясь в премудростях этого дела, опираясь лишь на базовые знания Java.
Здесь на примере простого приложения, которое позволяет выбрать станцию метро из списка и отобразить её на карте, показаны некоторые полезные фичи работы с адаптером для ListView, реализованы простейшая работа гуглокарты и взаимодействие с встроенной БД.
Весь проект можно скачать на github по ссылке в конце статьи.
Собственно, так выглядит список:
Он включает в себя сам список станций и строку поиска по станциям. В поиске реализована подсказка — при наборе последовательности букв в списке остаются только те станции, название которых начинается с этой последовательности.
Тапаем по станции Белорусская, получаем:
Теперь посмотрим, что же там происходит.
Нетрудно заметить, что здесь используется гуглокарта (MapView). Соответственно, важно помнить, что в качестве target при создании проекта нужно указать Google APIs.
Теперь по пунктам:
Это базовый файл, в котором описываются настройки приложения и указывается, к чему оно имеет доступ. Соответственно, в этом файле необходимо прописать доступ к интернету, указать, что используется библиотека com.google.android.maps, и указать наши активити: в данном случае SearchDialog, которое появляется при запуске, и Map — активити с картой.
Для удобной работы с базой данных необходимо создать database helper, который реализует методы, необходимые нам для работы с БД – такие, как фильтрация по первым буквам и поиск по id.
Сам файл RecordsDbHelper.java, как уже говорилось, можно посмотреть по ссылке ниже. Здесь стоит отметить лишь, что мы реализовали методы поиска по id и по текстовому фильтру, которые будем в дальнейшем использовать.
В общем, тут всё как обычно, никаких премудростей.
Это класс, реализующий связь списка с Базой данных. В результате запроса к БД мы получаем курсор таблицы. Адаптер берет курсор, список, названия полей таблицы и id элементов из item.xml (см.ниже), которые нужно соотнести друг с другом, и заполняет ListView. Наследуемся от стандартного SimpleCursorAdapter, в котором всё это реализовано, и переопределяем в нём метод getView.
Adapter.java
В методе getView готовому View элемента списка(т.е. строке списка с названием станции и цветным кружочком) в тэг(который есть у каждого объекта типа View) помещается id строки таблицы БД, которой этот элемент соответствует. Этот id впоследствии будет передаваться в активити с картой при тапе по элементу списка.
Сначала нужно задать его Layout – а именно каркас визуального представления необходимых нам элементов:
search.xml
Layout отдельного элемента списка (картинка – кружочек с цветом линии метро и название станции):
item.xml
Теперь, наконец, класс Search, в котором описывается активити с поиском. Он реализован с помощью наследования от ListActivity – специального активити для списков. Данные о станциях (название, координаты и номер ветки метро) берутся из вышерассмотренной базы данных.
Самое интересное здесь, как мне кажется, Binder. Он позволяет определить, каким образом привязывать значение из таблицы БД к элементу строки списка, то есть, например, как привязать номер ветки, который хранится в таблице, к соответствующему рисунку с кружочком нужного цвета. Метод setViewValue вызывается для каждого view, указанного в конструкторе курсора. Если возвращаемый результат true, то данные привязаны, всё хорошо. Если false — система понимает, что вызван не подходящий метод и ищет нужный среди стандартных Binder’ов. В этом примере проверяется, является ли view, переданный в качестве параметра, тем самым ImageView, в котором должен быть кружочек. Если это так, то передаем туда соответствующую картинку, иначе возвращаем false, дав понять, что нужно использовать стандартный Binder (т.к. это не картинка, а тут мы обрабатываем только картинки).
При вызове активити первым делом вызывается метод onCreate(), в котором инициализируются все отображаемые элементы. Здесь устанавливается обработчик кликов по элементам списка, который вытягивает тэг с id записи и передаёт этот тэг в активити с картой (ну и собственно вызывает это активити). Также устанавливается обработчик изменения текста в EditText (строка фильтрации). При изменении текста вызывается функция updateList(), в которой с помощью описанного выше хелпера кидается запрос в базу, вытаскивающий отфильтрованные данные, и создается адаптер для списка.
Наконец переходим к самим картам. Начнем с Layout:
map.xml
Здесь требуется указать ключ к GoogleMaps API. Свой я светить не стал :) О том, как его получить, можно почитать тут.
Ну и собственно активити с картой:
Map.java
Здесь:
1. На карту устанавливаются стандартные кнопки ZoomIn и ZoomOut (с помощью setBuiltInZoomControls())
2. Открывается наша БД.
3. Из intent, который вызвал это активити, вытягивается id записи, который передавался в обработчике клика по элементу списка (я писал об этом выше), и по этому id из базы достается нужная строчка, из которой нам нужны координаты станции.
4. Координаты вытаскиваются с помощью метода getDouble, полученного в результате запроса.
5. Далее создаётся GeoPoint и выполняются преобразования координат (в базе хранятся дробные, а гуглу нужны целочисленные. для этого нужно взятые из базы умножить на 10^6 и округлить).
6. animateTo() и setZoom() сдвигают карту в нужное место и устанавливают уровень зума.
Фух, в общем всё. Как видно, за достаточно простым функционалом стоят не совсем тривиальные вещи, разобраться с которыми новичку не так просто. Поэтому я очень надеюсь, что опыт, которым я поделился, поможет кому-то не потерять энтузиазм из-за проблем на старте, и качественных приложений, оптимизирующих нашу жизнь, будет больше!
Весь проект можно найти по ссылке.
Здесь на примере простого приложения, которое позволяет выбрать станцию метро из списка и отобразить её на карте, показаны некоторые полезные фичи работы с адаптером для ListView, реализованы простейшая работа гуглокарты и взаимодействие с встроенной БД.
Весь проект можно скачать на github по ссылке в конце статьи.
Собственно, так выглядит список:
Он включает в себя сам список станций и строку поиска по станциям. В поиске реализована подсказка — при наборе последовательности букв в списке остаются только те станции, название которых начинается с этой последовательности.
Тапаем по станции Белорусская, получаем:
Теперь посмотрим, что же там происходит.
Нетрудно заметить, что здесь используется гуглокарта (MapView). Соответственно, важно помнить, что в качестве target при создании проекта нужно указать Google APIs.
Теперь по пунктам:
1. AndroidManifest
Это базовый файл, в котором описываются настройки приложения и указывается, к чему оно имеет доступ. Соответственно, в этом файле необходимо прописать доступ к интернету, указать, что используется библиотека com.google.android.maps, и указать наши активити: в данном случае SearchDialog, которое появляется при запуске, и Map — активити с картой.
2. База данных
Для удобной работы с базой данных необходимо создать database helper, который реализует методы, необходимые нам для работы с БД – такие, как фильтрация по первым буквам и поиск по id.
Сам файл RecordsDbHelper.java, как уже говорилось, можно посмотреть по ссылке ниже. Здесь стоит отметить лишь, что мы реализовали методы поиска по id и по текстовому фильтру, которые будем в дальнейшем использовать.
public Cursor getById(long id) {
Cursor ccc = mDb.query(true, DATABASE_TABLE, new String[] { KEY_ROWID, KEY_NAME, KEY_X, KEY_Y, KEY_LINES }, KEY_ROWID + "=" + Long.toString(id), null, null, null, null, null);
return ccc;
}
synchronized public Cursor fetchRecordsByQuery(String query) {
Cursor ccc = mDb.query(true, DATABASE_TABLE, new String[] { KEY_ROWID, KEY_NAME, KEY_LINES }, KEY_NAME + " LIKE" + "'" + query + "%'", null, null, null, KEY_NAME, null);
return ccc;
}
В общем, тут всё как обычно, никаких премудростей.
3. Адаптер
Это класс, реализующий связь списка с Базой данных. В результате запроса к БД мы получаем курсор таблицы. Адаптер берет курсор, список, названия полей таблицы и id элементов из item.xml (см.ниже), которые нужно соотнести друг с другом, и заполняет ListView. Наследуемся от стандартного SimpleCursorAdapter, в котором всё это реализовано, и переопределяем в нём метод getView.
Adapter.java
public class Adapter extends SimpleCursorAdapter{
public Adapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
long id=getItemId(position);
view.setTag(id);
return view;
}
}
В методе getView готовому View элемента списка(т.е. строке списка с названием станции и цветным кружочком) в тэг(который есть у каждого объекта типа View) помещается id строки таблицы БД, которой этот элемент соответствует. Этот id впоследствии будет передаваться в активити с картой при тапе по элементу списка.
4. Список
Сначала нужно задать его Layout – а именно каркас визуального представления необходимых нам элементов:
search.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<EditText
android:id="@+id/search_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/search_hint"
/>
<ListView
android:id="@android:id/android:list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
Layout отдельного элемента списка (картинка – кружочек с цветом линии метро и название станции):
item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/line_image"
>
</ImageView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:id="@+id/text1"
>
</TextView>
</LinearLayout>
Теперь, наконец, класс Search, в котором описывается активити с поиском. Он реализован с помощью наследования от ListActivity – специального активити для списков. Данные о станциях (название, координаты и номер ветки метро) берутся из вышерассмотренной базы данных.
public class Search extends ListActivity {
//объявляем переменные
private EditText searchText;
private RecordsDbHelper recordsDBHelper;
private ListView list;
private Adapter notes;
//вложенный класс Binder описан ниже
private Binder binder;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
binder = new Binder();
list = getListView();
//устанавливаем кликабельность на нашем списке и указываем обработчик кликов,
//который при тапе с помощью intent передаёт id записи, который мы поместили в
//тэг(см. выше), в активити с картой и осуществляет переход на него.
list.setClickable(true);
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> av, View v, int pos, long id) {
Intent intent = new Intent();
intent.putExtra("ID", (Long) v.getTag());
intent.setClass(Search.this, Map.class);
startActivity(intent);
}
});
searchText = (EditText) findViewById(R.id.search_text);
//указываем обработчик изменения введенного к поле фильтра текста, который при
//срабатывании вызывает метод updateList(), описанный в этом классе ниже
searchText.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
updateList(searchText.getText().toString());
}
});
//здесь создаётся объект нашего Database Helper’а, с помощью которого мы
//работаем с базой. Операция открытия БД довольно ресурсоёмкая, поэтому
//выполняем её в новом отдельном потоке.
list.post(new Runnable() {
@Override
public void run() {
recordsDBHelper = new RecordsDbHelper(Search.this);
recordsDBHelper.open();
updateList(searchText.getText().toString());
}
});
}
//Вложенный класс Binder, реализующий интерфейс SimpleCursorAdapter.ViewBinder.
//Подробнее о нём написано ниже. Вкратце, здесь в соответствие номеру ветки станции
//ставится в соответствие кружочек с нужным цветом.
private static class Binder implements Adapter.ViewBinder {
public boolean setViewValue(View view, Cursor c, int i) {
if (view.getId() == R.id.line_image) {
Integer line = c.getInt(i);
switch (line) {
case 0:
((ImageView) view).setImageResource(R.drawable.line0);
break;
case 1:
((ImageView) view).setImageResource(R.drawable.line1);
break;
…
…
}
return true;
}
return false;
}
}
//В этом методе происходит инициализация адаптера в соответствии с введенным в
//фильтре текстом.
private void updateList(String s) {
if (recordsDBHelper.isReady()) {
//вытягиваем курсор на таблицу с отфильтрованными данными
Cursor c = recordsDBHelper.fetchRecordsByQuery(s);
startManagingCursor(c);
//определяем множество полей таблицы, которые нам нужны для заполнения списка
String[] from = new String[] { RecordsDbHelper.KEY_NAME, RecordsDbHelper.KEY_LINES };
//и указываем id элементов ячейки списка(т.е. ImageView, в котором будет
//цветной кружочек, и TextView, в котором будет название станции)
int[] to = new int[] { R.id.text1, R.id.line_image };
//передаём всё это в конструктор адаптера
notes = new Adapter(Search.this, R.layout.item, c, from, to);
notes.setViewBinder(binder);
setListAdapter(notes);
}
}
}
Самое интересное здесь, как мне кажется, Binder. Он позволяет определить, каким образом привязывать значение из таблицы БД к элементу строки списка, то есть, например, как привязать номер ветки, который хранится в таблице, к соответствующему рисунку с кружочком нужного цвета. Метод setViewValue вызывается для каждого view, указанного в конструкторе курсора. Если возвращаемый результат true, то данные привязаны, всё хорошо. Если false — система понимает, что вызван не подходящий метод и ищет нужный среди стандартных Binder’ов. В этом примере проверяется, является ли view, переданный в качестве параметра, тем самым ImageView, в котором должен быть кружочек. Если это так, то передаем туда соответствующую картинку, иначе возвращаем false, дав понять, что нужно использовать стандартный Binder (т.к. это не картинка, а тут мы обрабатываем только картинки).
При вызове активити первым делом вызывается метод onCreate(), в котором инициализируются все отображаемые элементы. Здесь устанавливается обработчик кликов по элементам списка, который вытягивает тэг с id записи и передаёт этот тэг в активити с картой (ну и собственно вызывает это активити). Также устанавливается обработчик изменения текста в EditText (строка фильтрации). При изменении текста вызывается функция updateList(), в которой с помощью описанного выше хелпера кидается запрос в базу, вытаскивающий отфильтрованные данные, и создается адаптер для списка.
5. Карта
Наконец переходим к самим картам. Начнем с Layout:
map.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.maps.MapView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey=<b><u>ключ к GoogleMaps API</u></b>
/>
Здесь требуется указать ключ к GoogleMaps API. Свой я светить не стал :) О том, как его получить, можно почитать тут.
Ну и собственно активити с картой:
Map.java
public class Map extends MapActivity {
public MapView mapView;
public MapController mapController;
private RecordsDbHelper mDbHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.map);
//инициализируем карту
mapView = (MapView) findViewById(R.id.mapview);
//добавляем стандартные кнопочки зума
mapView.setBuiltInZoomControls(true);
mapController = mapView.getController();
//здесь открытие БД также сделано в отдельном потоке
mapView.post(new Runnable() {
@Override
public void run() {
//открывается база
mDbHelper = new RecordsDbHelper(Map.this);
mDbHelper.open();
//извлекается доп. инфа, то есть id станции в БД, переданный при тапе по списку
Bundle extras = Map.this.getIntent().getExtras();
if (extras != null) {
long str = extras.getLong("ID");
//по этому id вытягивается соответствующая запись из БД
Cursor cursor = mDbHelper.getById(str);
startManagingCursor(cursor);
cursor.moveToFirst();
double x = cursor.getDouble(cursor.getColumnIndex(RecordsDbHelper.KEY_X));
double y = cursor.getDouble(cursor.getColumnIndex(RecordsDbHelper.KEY_Y));
//создаём GeoPoint с координатами извлеченными из БД с //помощью курсора
GeoPoint point = new GeoPoint((int) (x * 1E6), (int) (y * 1E6));
//и передвигаем карту в эту точку
mapController.animateTo(point);
mapController.setZoom(16);
}
}
});
}
}
Здесь:
1. На карту устанавливаются стандартные кнопки ZoomIn и ZoomOut (с помощью setBuiltInZoomControls())
2. Открывается наша БД.
3. Из intent, который вызвал это активити, вытягивается id записи, который передавался в обработчике клика по элементу списка (я писал об этом выше), и по этому id из базы достается нужная строчка, из которой нам нужны координаты станции.
4. Координаты вытаскиваются с помощью метода getDouble, полученного в результате запроса.
5. Далее создаётся GeoPoint и выполняются преобразования координат (в базе хранятся дробные, а гуглу нужны целочисленные. для этого нужно взятые из базы умножить на 10^6 и округлить).
6. animateTo() и setZoom() сдвигают карту в нужное место и устанавливают уровень зума.
Фух, в общем всё. Как видно, за достаточно простым функционалом стоят не совсем тривиальные вещи, разобраться с которыми новичку не так просто. Поэтому я очень надеюсь, что опыт, которым я поделился, поможет кому-то не потерять энтузиазм из-за проблем на старте, и качественных приложений, оптимизирующих нашу жизнь, будет больше!
Весь проект можно найти по ссылке.