Pull to refresh

Пишем простое приложение с использованием GoogleMap и прокачанным SimpleCursorAdapter

Development for Android *
Sandbox
Разработка приложений под платформу Android становится все более и более массовой. Так, мне недавно довелось заниматься разработкой клиентской части такого приложения, фактически с нуля разбираясь в премудростях этого дела, опираясь лишь на базовые знания Java.

Здесь на примере простого приложения, которое позволяет выбрать станцию метро из списка и отобразить её на карте, показаны некоторые полезные фичи работы с адаптером для ListView, реализованы простейшая работа гуглокарты и взаимодействие с встроенной БД.
Весь проект можно скачать на github по ссылке в конце статьи.

Собственно, так выглядит список:
image

Он включает в себя сам список станций и строку поиска по станциям. В поиске реализована подсказка — при наборе последовательности букв в списке остаются только те станции, название которых начинается с этой последовательности.
Тапаем по станции Белорусская, получаем:

image
Теперь посмотрим, что же там происходит.
Нетрудно заметить, что здесь используется гуглокарта (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() сдвигают карту в нужное место и устанавливают уровень зума.

Фух, в общем всё. Как видно, за достаточно простым функционалом стоят не совсем тривиальные вещи, разобраться с которыми новичку не так просто. Поэтому я очень надеюсь, что опыт, которым я поделился, поможет кому-то не потерять энтузиазм из-за проблем на старте, и качественных приложений, оптимизирующих нашу жизнь, будет больше!

Весь проект можно найти по ссылке.
Tags:
Hubs:
Total votes 19: ↑18 and ↓1 +17
Views 10K
Comments 15
Comments Comments 15