Использование Android Search Dialog. Пример простого приложения

image

Данная статья предназначена для тех, кто уже написал свой HelloWorld для Android и знает, что такое Activity и Intent, а так же где находится манифест, и зачем нужны layout'ы. В противном случае, можно ознакомиться с этим материалом, например, на developer.android.com.

В статье описывается создание несложного приложения, которое использует механизм реализации поиска, основанный на возможностях встроенного фреймворка. После прочтения вы также сможете настроить свое приложение таким образом, чтобы оно осуществляло поиск по данным, используя стандартный Android Search Dialog.


Немного теории


Android Search Dialog (далее — «диалог поиска») управляется с помощью поискового фреймворка. Это означает, что разработчику не нужно задумываться над тем как его нарисовать или как отловить поисковый запрос. За вас эту работу сделает SearchManager.

Итак, когда пользователь запускает поиск, SearchManager создает Intent, и направляет его к Activity, которое отвечает за поиск данных (при этом сам запрос помещается в экстры). То есть по сути в приложении должно быть хотя бы одно Activity, которое получает поисковые Intent'ы, выполняет поиск, и предоставляет пользователю результаты. Для реализации потребуется следующее:
  • Конфигурационный xml файл (в нем содержится информация о диалоге)
  • Activity, которое будет получать поисковые запросы, выполнять поиск и выводить результаты на экран
  • Механизм вызова поискового диалога (так как не все устройства с Android на борту имеют на корпусе кнопку поиска)


Конфигурационный файл


Начнем с создания конфигурационного файла, потому что это самая простая часть. Конечно, вам нужно заранее приготовиться, создать новый проект, Activity (У меня используется Activity «Main» и имя проекта «SearchExample»). Всё, что нужно для конфигурации — это создать файл searchable.xml в поддиректории res/xml/ вашего проекта, со следующим содержанием.
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/app_name"
  android:hint="@string/search_hint"
  >
</searchable>


* This source code was highlighted with Source Code Highlighter.

Обязательным атрибутом является только android:label, причем он должен ссылаться на строку, которая является такой же, что и название приложения. Второй атрибут, android:hint используется для отображения строки в пустом диалоге. Например, это может быть «Поиск по Видео» или «Поиск контактов» и т.п. Этот атрибут указывает на то, по каким данным осуществляется поиск. Также важно знать, что элемент searchable поддерживает множество других атрибутов, подробнее можно прочесть Здесь.

Создаем Activity


Минимально, всё что нам нужно от пользовательского интерфейса Activity — это список для вывода результатов поиска и механизм вызова поискового диалога. Так и сделаем, добавив только поле для ввода текста и кнопку, чтобы мы сами могли заполнять базу. Забегая вперед, скажу, что данные будем хранить в БД SQLite.

Опишем интерфейс Activity следующим образом (файл находится в res/layout/main.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="fill_parent">
  <LinearLayout
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="top">
  <EditText
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="@string/text"
    android:layout_weight="100.0" />
  <Button
    android:id="@+id/add"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/add" />
  </LinearLayout>
  <ListView
    android:id="@android:id/list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
  <TextView
    android:layout_gravity="left"
    android:id="@android:id/empty"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:text="@string/no_records"/>
</LinearLayout>


* This source code was highlighted with Source Code Highlighter.

Выглядит следующим образом:

image

Также нам понадобится layout для вида элемента списка, опишем его простейшим образом (файл находится в res/layout/record.xml)
<?xml version="1.0" encoding="utf-8" ?>
<TextView
  android:id="@+id/text1"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
/>


* This source code was highlighted with Source Code Highlighter.


Также, не забываем про файл ресурсов, где хранятся наши строки (файл в res/values/strings.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">SearchExample</string>
  <string name="add">Add</string>
  <string name="text">Enter text</string>
  <string name="no_records">There are no records in the table</string>
  <string name="search_hint">Search the records</string>
  <string name="search">Search</string>
</resources>


* This source code was highlighted with Source Code Highlighter.

Напоминаю, что когда пользователь выполняет поиск, то нужно, чтобы SearchManager отправлял Intent вместе с поисковым запросом к нашему Activity. Чтобы SearchManager точно знал, куда ему отсылать Intent, нужно объявить об этом в манифесте. Итак, идем в AndroidManifest.xml, и изменяем его так, чтобы Activity могло принимать Intent'ы типа Search. Кроме этого, применим наш конфигурационный файл searchable.xml. Итак, манифест будет выглядеть примерно так:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.search"
   android:versionCode="1"
   android:versionName="1.0">
  <application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name=".Main"
         android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
      </intent-filter>
      <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable"
      />
    </activity>

  </application>
  <uses-sdk android:minSdkVersion="5" />

</manifest>


* This source code was highlighted with Source Code Highlighter.

Сейчас, вы уже можете проверить, все ли вы сделали правильно. Вызвать диалог на эмуляторе можно, например, нажав кнопку поиска. Ну или если вы проверяете на девайсе, то зажав «Меню». Выглядеть должно примерно так:

image

Выполнение поиска


После того, как мы объявили наше Activity и внесли необходимые изменения в AndroidManifest.xml, можно рассмотреть само выполнение поиска. Оно включает в себя три этапа:
  • Получение запроса
  • Поиск данных
  • вывод результатов

Получение запроса

Так как SearchManager посылает Intent типа Search нашему Activity, то всё что нужно сделать это проверить на Intent этого типа при старте Activity. Тогда, если мы получаем нужный Intent, то можно извлекать из него экстру и выполнять поиск.

Поиск данных

Так как тип структуры хранения данных для разных приложений может различаться, то и методы для них свои. В нашем случае, проще всего выполнить запрос по таблице БД SQLite запросом LIKE. Конечно, лучше использовать FTS3, он значительно быстрее, подробнее о FTS3 можно прочесть на сайте SQLite.org. В идеале, также нужно всегда рассчитывать, что поиск может занять продолжительное время, поэтому можно создать какой-нибудь ProgressDialog, чтобы у нас не завис интерфейс, и чтобы пользователь знал, что приложение работает.

Вывод результатов

Вообще вывод результатов — это проблема UI, но так как мы используем ListView, то для нас проблема решается простым обновлением адаптера.

Исходный код


Наконец, привожу полный исходный код двух классов с комментариями. Первый — Main, наследник ListActivity, он используется для наполнения БД и вывода результатов. Второй класс — RecordsDbHelper, он реализует интерфейс для взаимодействия с БД. Самые важные методы — добавление записей и поиск совпадений, с помощью запроса LIKE.

Файл Main.java
package com.example.search;

import android.app.ListActivity;
import android.app.SearchManager;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.SimpleCursorAdapter;

public class Main extends ListActivity {
  private EditText text;
  private Button add;
  private RecordsDbHelper mDbHelper;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    //Создаем экземпляр БД
    mDbHelper = new RecordsDbHelper(this);
    //Открываем БД для записи
    mDbHelper.open();
    //Получаем Intent
    Intent intent = getIntent();
    //Проверяем тип Intent
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
      //Берем строку запроса из экстры
      String query = intent.getStringExtra(SearchManager.QUERY);
      //Выполняем поиск
      showResults(query);
    }

    add = (Button) findViewById(R.id.add);
    text = (EditText) findViewById(R.id.text);
    add.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        String data = text.getText().toString();
        if (!data.equals("")) {
          saveTask(data);
          text.setText("");
        }
      }
    });
  }

  private void saveTask(String data) {
    mDbHelper.createRecord(data);
  }

  private void showResults(String query) {
    //Ищем совпадения
    Cursor cursor = mDbHelper.fetchRecordsByQuery(query);
    startManagingCursor(cursor);
    String[] from = new String[] { RecordsDbHelper.KEY_DATA };
    int[] to = new int[] { R.id.text1 };

    SimpleCursorAdapter records = new SimpleCursorAdapter(this,
        R.layout.record, cursor, from, to);
    //Обновляем адаптер
    setListAdapter(records);
  }
  //Создаем меню для вызова поиска (интерфейс в res/menu/main_menu.xml)
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main_menu, menu);
    return true;  
  }

  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case R.id.search_record:
      onSearchRequested();
      return true;
    default:
      return super.onOptionsItemSelected(item);
    }
  }
}


* This source code was highlighted with Source Code Highlighter.


Файл RecordsDbHelper.java
package com.example.search;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class RecordsDbHelper {

  public static final String KEY_DATA = "data";
  public static final String KEY_ROWID = "_id";

  private static final String TAG = "RecordsDbHelper";
  private DatabaseHelper mDbHelper;
  private SQLiteDatabase mDb;

  private static final String DATABASE_CREATE = "CREATE TABLE records(_id INTEGER PRIMARY KEY AUTOINCREMENT, "
      + "data TEXT NOT NULL);";

  private static final String DATABASE_NAME = "data";
  private static final String DATABASE_TABLE = "records";
  private static final int DATABASE_VERSION = 1;

  private final Context mCtx;

  private static class DatabaseHelper extends SQLiteOpenHelper {

    DatabaseHelper(Context context) {
      super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

      db.execSQL(DATABASE_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
      Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
          + newVersion + ", which will destroy all old data");
      db.execSQL("DROP TABLE IF EXISTS tasks");
      onCreate(db);
    }
  }

  public RecordsDbHelper(Context ctx) {
    this.mCtx = ctx;
  }

  public RecordsDbHelper open() throws SQLException {
    mDbHelper = new DatabaseHelper(mCtx);
    mDb = mDbHelper.getWritableDatabase();
    return this;
  }

  public void close() {
    mDbHelper.close();
  }
  
  //Добавляем запись в таблицу
  public long createRecord(String data) {
    ContentValues initialValues = new ContentValues();
    initialValues.put(KEY_DATA, data);
    return mDb.insert(DATABASE_TABLE, null, initialValues);
  }
  
  //Поиск запросом LIKE
  public Cursor fetchRecordsByQuery(String query) {
    return mDb.query(true, DATABASE_TABLE, new String[] { KEY_ROWID,
        KEY_DATA }, KEY_DATA + " LIKE" + "'%" + query + "%'", null,
        null, null, null, null);
  }
}


* This source code was highlighted with Source Code Highlighter.

Заключение

Для написания статьи использовалась документация с developer.android.com
Весь проект можно взять на code.google.com.
Поделиться публикацией

Комментарии 12

    +3
    хорошая статья, спасибо!
      +2
      Пожалуйста. Если тема будет интересна, то могу написать статью о добавлении к диалогу подсказок недавних запросов и динамических подсказок.
        +3
        Тема интересна) В любом случае трюки и готовые работающие примеры различных аспектов системы лишними не будут.
          +1
          Не нужно спрашивать, если видите что пост плюсуют, значит нужно писать дальше, это же элементарно=)
            +2
            Так же элементарно и то, что в конце вопросов ставят вопросительный знак.
              0
              Эх ты, это же тонкий намек) Подбросить уголька в топку, угостить конфетами каракумы, подкинуть копейку на проезд, одолжить точилку для карандаша малышу) Я вот понял, держите конфету и пишите-пишите)
          0
          >так как не все устройства с Android на борту имеют на корпусе кнопку поиска

          Фраза ввела в ступор и заставила погуглить.

          Со stackoverflow (со ссылкой на официальный источник):

          The Home, Menu and Back functions are essential to the Android navigation paradigm. Device implementations MUST make these functions available to the user at all times, regardless of application state. These functions SHOULD be implemented via dedicated buttons. They MAY be implemented using software, gestures, touch panel, etc., but if so they MUST be always accessible and not obscure or interfere with the available application display area. Device implementers SHOULD also provide a dedicated search key. Device implementers MAY also provide send and end keys for phone calls.

          Т.о. производителю следует добавлять эту кнопку на корпус, но она все же не essential. На практике не видел девайсов без кнопки поиска, думаю довольно редкое явление.
            +1
            mdata.yandex.net/i?path=b1106195815_img_id6251552565997729478.jpg
            Если картинка не будет казать, то market.yandex.ru/model.xml?hid=&modelid=5059755
            Sony Ericsson XPERIA X10
            3 кнопки, по всей видимости меню, home и back
              0
              Спасибо, на xperia невнимательно смотрел, напрасно.
              0
              А вот я вообще не видел девайсов с этой кнопкой (ну кроме старых). Например, у меня есть SGS, на нем тоже только Menu, Home и Back.
                0
                Гм, посмотрите у htc. На samsung, lg, motorola так же search на большинстве устройств.
                  0
                  Согласен, я просто о том, что, я этих девайсов вживую не видел. Интерфейс для вызова все равно надо предусматривать — каким бы редким это явление не было, пользователи не должны от этого страдать.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое