среда, 25 апреля 2012 г.

9. Меню

Итак, в соответствии с планом, мы наконец-то, с чистой совестью, можем заняться кодированием.

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, 'Рыжий осел'");
           ...
       }
       ...
}

После запуска пересоздания БД, главное меню содержит пункт "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;
    }
    ...
}

При выборе пункта меню, мы проверяем, используется ли этот пункт для выбора языка приложения, получаем 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;
   }
   ...
}

После запуска приложения, можно убедиться, что все работает именно так, как мы и запланировали (правда код локали не загружается при старте приложения из профиля, но эту задачу мы перед собой сегодня и не ставили).

В следующей статье мы продолжим двигаться по плану и реализуем отображение окна подсказки при рестарте головоломки. Код проекта можно получить здесь.

Комментариев нет:

Отправить комментарий