Итак, в соответствии с планом, мы наконец-то, с чистой совестью, можем заняться кодированием.
9. Меню
В сегодняшней статье мы займемся задачей (2.1) - созданием меню приложения, попутно составив несколько запросов к Content Provider-у и проконтролировав корректность создания БД.
Само по себе, меню добавляется в приложение достаточно просто. Создадим главное меню, содержащее в себе один пункт вложенного меню, для выбора текущего языка интерфейса, переопределив метод onCreateOptionsMenu:
package com.whiterabbit.tags;
...
public class PuzzleActivity extends Activity {
private static final int BASE_MENU = 100;
private static final int BASE_LOCALE_MENU = 200;
private static final int MENU_PROFILE_ITEM = 5;
private View view = null;
private Menu mainMenu = null;
private Map<Long, String> stringCache = new HashMap<Long, String>();
private int currentLocale = PuzzleDb.Locales.LOCALE_EN;
private static final String[] STRINGS_PROJECTION =
new String[] {
PuzzleDb.StringValues._ID,
PuzzleDb.StringValues.COLUMN_NAME_VALUE
};
private static final String[] LOCALE_PROJECTION =
new String[] {
PuzzleDb.Locales._ID,
PuzzleDb.StringValues.COLUMN_NAME_VALUE
};
public String getLocalizedString(long id) {
String r = stringCache.get(id);
if (r != null) {
return r;
}
Uri uri = ContentUris.withAppendedId(PuzzleDb.Strings.CONTENT_URI, id);
Cursor cursor = managedQuery(
uri,
STRINGS_PROJECTION,
PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " = ?",
new String [] {Integer.toString(currentLocale)},
null
);
if (cursor.moveToFirst()) {
int stringsValueColumn = cursor.getColumnIndex(PuzzleDb.StringValues.COLUMN_NAME_VALUE);
r = cursor.getString(stringsValueColumn);
}
stringCache.put(id, r);
return r;
}
public void getLocales(Menu menu) {
Uri uri = PuzzleDb.Locales.CONTENT_URI;
Cursor cursor = managedQuery(
uri,
LOCALE_PROJECTION,
"b." + PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " = ?",
new String [] {Integer.toString(currentLocale)},
null
);
for (cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()) {
int idColumn = cursor.getColumnIndex(PuzzleDb.Puzzles._ID);
int nameColumn = cursor.getColumnIndex(PuzzleDb.StringValues.COLUMN_NAME_VALUE);
int id = cursor.getInt(idColumn);
String name = cursor.getString(nameColumn);
menu.add(0, id + BASE_LOCALE_MENU, id, name);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mainMenu = menu;
super.onCreateOptionsMenu(menu);
SubMenu newMenu = menu.addSubMenu(BASE_MENU, BASE_MENU + MENU_PROFILE_ITEM, Menu.NONE, getLocalizedString(MENU_PROFILE_ITEM));
getLocales(newMenu);
return true;
}
...
}
В методе onCreateOptionsMenu мы создаем один пункт меню, наименование которого получаем при помощи функции getLocalizedString (в зависимости от текущего значения локали) и добавляем в него пункты подменю, методом getLocales.
Методы getLocalizedString и getLocales служат прекрасной иллюстрацией выборки данных, предоставляемых Content Provider-ом. В getLocalizedString мы дополнительно кэшируем полученные строки, с целью минимизации количества обращений к БД.
Доработаем Content Provider таким образом, чтобы он отрабатывал используемые нами запросы:
package com.whiterabbit.tags;
...
public class PuzzleProvider extends ContentProvider {
...
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int STRING_URI_INDICATOR = 1;
private static final int LOCALE_URI_INDICATOR = 2;
private static final int PROFILE_URI_INDICATOR = 3;
static {
uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Strings.PATH_STRING + "/#", STRING_URI_INDICATOR);
uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Locales.PATH_LOCALE, LOCALE_URI_INDICATOR);
}
...
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case STRING_URI_INDICATOR:
return PuzzleDb.Strings.CONTENT_ITEM_TYPE;
case LOCALE_URI_INDICATOR:
return PuzzleDb.Locales.CONTENT_DIR_TYPE;
}
return null;
}
...
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
switch (uriMatcher.match(uri)) {
case STRING_URI_INDICATOR:
return db.rawQuery("select " + PuzzleDb.StringValues._ID +
", " + PuzzleDb.StringValues.COLUMN_NAME_VALUE +
" from " + PuzzleDb.StringValues.TABLE_NAME +
" where " + PuzzleDb.StringValues.COLUMN_NAME_STRING_ID +
" = " + uri.getPathSegments().get(1) +
" and " + selection, selectionArgs);
case LOCALE_URI_INDICATOR:
return db.rawQuery("select a." + PuzzleDb.Locales._ID + " as " + PuzzleDb.Locales._ID + ", " +
"b." + PuzzleDb.StringValues.COLUMN_NAME_VALUE + " as " + PuzzleDb.StringValues.COLUMN_NAME_VALUE + " " +
"from " + PuzzleDb.Locales.TABLE_NAME + " a, " +
PuzzleDb.StringValues.TABLE_NAME + " b " +
"where b." + PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " = " +
"a." + PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " and " + selection, selectionArgs);
}
return null;
}
...
}
Здесь мы создаем UriMatcher, который будет определять тип запроса по URI, и дописываем обработку запросов в getType и query. После запуска приложения и нажатия на кнопку <меню> наблюдаем ... пункт меню с пустым названием :(
Выясняется, что в прошлой статье мы напутали с идентификаторами строковых ресурсов. В отладке такого рода ошибок хорошо помогает утилита просмотра содержимого БД SQLite (например sqlitebrowser). Саму БД можно забрать с эмулятора (после того как она создана, в результате первого к ней обращения) из каталога /data/data/com.whiterabbit.tags/databases/puzzle.db (в перспективе "DDMS" Eclipse).
Удалим БД на эмуляторе и внесем исправления в код создания БД:
package com.whiterabbit.tags;
...
public class PuzzleProvider extends ContentProvider {
...
static class DatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
...
db.execSQL("CREATE TABLE " + PuzzleDb.Strings.TABLE_NAME + " (" +
PuzzleDb.Strings._ID + " INTEGER PRIMARY KEY);");
addString(db, "1"); // English
addString(db, "2"); // Russian
addString(db, "3"); // Size X
addString(db, "4"); // Size Y
addString(db, "5"); // Language
addString(db, "11");
db.execSQL("CREATE TABLE " + PuzzleDb.Locales.TABLE_NAME + " (" +
PuzzleDb.Locales._ID + " INTEGER PRIMARY KEY," +
PuzzleDb.Locales.COLUMN_NAME_NAME + " TEXT," +
PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " INTEGER," +
PuzzleDb.Locales.COLUMN_NAME_IS_DEFAULT + " INTEGER," +
PuzzleDb.Locales.COLUMN_NAME_DESCRIPTION + " TEXT);");
addLocale(db, PuzzleDb.Locales.LOCALE_EN + ", 'en_US', 1, 1");
addLocale(db, PuzzleDb.Locales.LOCALE_RU + ", 'ru_RU', 2, 0");
db.execSQL("CREATE TABLE " + PuzzleDb.StringValues.TABLE_NAME + " (" +
PuzzleDb.StringValues._ID + " INTEGER PRIMARY KEY," +
PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " INTEGER," +
PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " INTEGER," +
PuzzleDb.StringValues.COLUMN_NAME_VALUE + " TEXT);");
addValue(db, "1, 1, 1, 'English'");
addValue(db, "2, 2, 1, 'Английский'");
addValue(db, "3, 1, 2, 'Russian'");
addValue(db, "4, 2, 2, 'Русский'");
addValue(db, "5, 1, 5, 'Language'");
addValue(db, "6, 2, 5, 'Язык'");
addValue(db, "7, 1, 11, 'Donkey'");
addValue(db, "8, 2, 11, 'Рыжий осел'");
...
}
...
}
...
public class PuzzleProvider extends ContentProvider {
...
static class DatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
...
db.execSQL("CREATE TABLE " + PuzzleDb.Strings.TABLE_NAME + " (" +
PuzzleDb.Strings._ID + " INTEGER PRIMARY KEY);");
addString(db, "1"); // English
addString(db, "2"); // Russian
addString(db, "3"); // Size X
addString(db, "4"); // Size Y
addString(db, "5"); // Language
addString(db, "11");
db.execSQL("CREATE TABLE " + PuzzleDb.Locales.TABLE_NAME + " (" +
PuzzleDb.Locales._ID + " INTEGER PRIMARY KEY," +
PuzzleDb.Locales.COLUMN_NAME_NAME + " TEXT," +
PuzzleDb.Locales.COLUMN_NAME_STRING_ID + " INTEGER," +
PuzzleDb.Locales.COLUMN_NAME_IS_DEFAULT + " INTEGER," +
PuzzleDb.Locales.COLUMN_NAME_DESCRIPTION + " TEXT);");
addLocale(db, PuzzleDb.Locales.LOCALE_EN + ", 'en_US', 1, 1");
addLocale(db, PuzzleDb.Locales.LOCALE_RU + ", 'ru_RU', 2, 0");
db.execSQL("CREATE TABLE " + PuzzleDb.StringValues.TABLE_NAME + " (" +
PuzzleDb.StringValues._ID + " INTEGER PRIMARY KEY," +
PuzzleDb.StringValues.COLUMN_NAME_LOCALE_ID + " INTEGER," +
PuzzleDb.StringValues.COLUMN_NAME_STRING_ID + " INTEGER," +
PuzzleDb.StringValues.COLUMN_NAME_VALUE + " TEXT);");
addValue(db, "1, 1, 1, 'English'");
addValue(db, "2, 2, 1, 'Английский'");
addValue(db, "3, 1, 2, 'Russian'");
addValue(db, "4, 2, 2, 'Русский'");
addValue(db, "5, 1, 5, 'Language'");
addValue(db, "6, 2, 5, 'Язык'");
addValue(db, "7, 1, 11, 'Donkey'");
addValue(db, "8, 2, 11, 'Рыжий осел'");
...
}
...
}
После запуска пересоздания БД, главное меню содержит пункт "Language", при выборе которого открывается подменю, содержащее пункты "English" и "Russian", при нажатии на которые ничего не происходит.
Добавим реакцию на выбор языка интерфейса, переопределив метод onOptionsItemSelected:
package com.whiterabbit.tags;
...
public class PuzzleActivity extends Activity {
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId >= BASE_LOCALE_MENU) {
currentLocale = itemId - BASE_LOCALE_MENU;
stringCache.clear();
view.invalidate();
if (mainMenu != null) {
mainMenu.clear();
onCreateOptionsMenu(mainMenu);
}
saveProfile();
}
return true;
}
...
}
...
public class PuzzleActivity extends Activity {
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId >= BASE_LOCALE_MENU) {
currentLocale = itemId - BASE_LOCALE_MENU;
stringCache.clear();
view.invalidate();
if (mainMenu != null) {
mainMenu.clear();
onCreateOptionsMenu(mainMenu);
}
saveProfile();
}
return true;
}
...
}
При выборе пункта меню, мы проверяем, используется ли этот пункт для выбора языка приложения, получаем currentLocale из идентификатора пункта меню и сохраняем изменения в профиле. Для перестроения меню мы принудительно его очищаем и самостоятельно вызываем onCreateOptionsMenu (я не знаю насколько такие манипуляции законны, но это работает).
Добавим в PuzzleProvider поддержку сохранения изменений профиля:
package com.whiterabbit.tags;
...
public class PuzzleProvider extends ContentProvider {
...
private static final int PROFILE_URI_INDICATOR = 3;
static {
...
uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Profiles.PATH_PROFILE, PROFILE_URI_INDICATOR);
}
...
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
...
case PROFILE_URI_INDICATOR:
return PuzzleDb.Profiles.CONTENT_DIR_TYPE;
}
return null;
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case PROFILE_URI_INDICATOR:
return db.update(PuzzleDb.Profiles.TABLE_NAME,
values,
PuzzleDb.Profiles.COLUMN_NAME_IS_DEFAULT + " = 1",
null);
}
return 0;
}
...
}
...
public class PuzzleProvider extends ContentProvider {
...
private static final int PROFILE_URI_INDICATOR = 3;
static {
...
uriMatcher.addURI(PuzzleDb.AUTHORITY, PuzzleDb.Profiles.PATH_PROFILE, PROFILE_URI_INDICATOR);
}
...
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
...
case PROFILE_URI_INDICATOR:
return PuzzleDb.Profiles.CONTENT_DIR_TYPE;
}
return null;
}
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case PROFILE_URI_INDICATOR:
return db.update(PuzzleDb.Profiles.TABLE_NAME,
values,
PuzzleDb.Profiles.COLUMN_NAME_IS_DEFAULT + " = 1",
null);
}
return 0;
}
...
}
После запуска приложения, можно убедиться, что все работает именно так, как мы и запланировали (правда код локали не загружается при старте приложения из профиля, но эту задачу мы перед собой сегодня и не ставили).
Комментариев нет:
Отправить комментарий