Material Design и поиск на примере приложения-справочника

    Введение


    Несколько лет назад я писал статью на Хабр о приложении-справочнике по математике для Android, которое стало моим первым опытом в разработке для GooglePlay. Сегодня, оглядываясь назад на свой прошлый хабрапост и прошлую версию приложения, мне становится страшно (чтобы содрогнуться достаточно взглянуть на первый скриншот ниже). За прошедшие несколько лет многое поменялось: AndroidMarket стал называться GooglePlay с новыми правилами и прочим, выходили новые версии ОС, появилась некая общая google-концепция к дизайну приложений material-design, появились новые среды разработки, да и Хабр изменился.

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


    Вообще, приложение претерпело несколько серьёзных изменений дизайна за время своего существования. Краткая история о том как оно менялось приведена на скриншотах:



    Material Design


    Разумеется material design. Куда же без него сейчас в разработке под android? Пришлось избавиться от многих графических ресурсов, которые в своё время так тщательно рисовались, но ничего не поделать, в концепцию материального дизайна они не вписывались — слишком неминималистичны. К примеру, иконки бокового меню:



    В работе с ресурсами иконок для разных экранов хорошо помогает asset studio, в котором, помимо прочего, ещё и имеются неплохие эффекты long shadow и dog-ear. В общем, asset studio — замечательный конструктор, который сэкономит много времени при работе с ресурсами. Также при помощи asset studio были сделаны новые material-иконки для покупки пива и социального взаимодействия:




    Если пиво приобретено, то в правом нижнем углу будет появляться sold out:



    Иконка приложения также претерпела некоторые изменения, здесь уже пришлось открыть Photoshop и порисовать:



    Самое трудное позади, о графических ресурсах больше говорить не будем.

    Теперь сделаем несколько тем оформления для нашего приложения и добавим FloatingActionButton. В папке values/ проекта в файле themes.xml опишем две темы оформления для нашего приложения Light и Dark:

    themes.xml
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <style name="LightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
            <item name="colorPrimary">@color/greenPrimary1</item>
            <item name="colorPrimaryDark">@color/greenPrmrDark1</item>
            <item name="android:windowBackground">@color/mn_bck1</item>
            <item name="colorAccent">@color/fabBckgrnd1</item>
        </style>
    
        <style name="DarkTheme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
            <item name="colorPrimary">@color/greyPrimary1</item>
            <item name="colorPrimaryDark">@color/greyPrmrDark1</item>
            <item name="android:windowBackground">@color/mn_bck2</item>
            <item name="colorAccent">@color/fabBckgrnd2</item>
        </style>
    
    </resources>
    


    О том, что такое colorPrimary, colorPrimaryDark, colorAccent хорошо написано тут и тут. А вот как выглядят эти темы в приложении:



    Расскажу теперь, как сделать так, чтобы применять тему сразу ко всем Activity вашего приложения. Для этого необходимо сделать BaseActivity унаследованную от ActionBarActivity (её не нужно объявлять в манифесте и создавать для неё xml-файл разметки). В методе onCreate() данной деятельности вызываем setTheme() в зависимости от выбора пользователя в настройках приложения:

    BaseActivity.java
    public class BaseActivity extends ActionBarActivity {
    
        public static final String NAME_PREFERENCES = "mysetting";
        public static final String THEME_SWITCHER = "thmswtch";
        public static final int THM_SWTCHR_DFLT = 0;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            SharedPreferences mSet = getSharedPreferences(NAME_PREFERENCES, Context.MODE_PRIVATE);
            /** применяем темную тему, если в настройках был осуществлён её выбор (по умолчанию в приложении LightTheme) */
            if(mSet.getInt(THEME_SWITCHER, THM_SWTCHR_DFLT) == 1){
                /** если устройство c LOLLIPOP и выше - раскрашиваем статус-бар */
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
                    getWindow().setStatusBarColor(getResources().getColor(R.color.greyPrmrDark1));
                }
                setTheme(R.style.DarkTheme);
            }
        }
    }
    


    Ну а все остальные Activity нашего приложения, будем наследовать от BaseActivity:



    При подборе сочетаний цветов для темы в стиле material может здорово помочь ресурс materialpalette.com, на котором предлагается полная цветовая палитра для темы по двум выбранным вами основным оттенкам.

    Для добавления слева круглых иконок с текстом в каждом элементе списка отлично подходит библиотека TextDrawable, которая легка в использовании и позволяет создавать не только круглые однотипные иконки (как на скриншотах), но и иконки разных форм, цветов, шрифтов и даже добавлять анимацию для них.

    Пример использования TextDrawable в адаптере основного списка приложения
                TextDrawable drawable = null;
                if(position==0)  drawable = TextDrawable.builder().beginConfig().bold().endConfig().buildRound("dx", context.getResources().getColor((curr_theme==1) ? R.color.mn_dvdr_dark : R.color.mn_dvdr_lght));
                if(position==1)  drawable = TextDrawable.builder().beginConfig().bold().endConfig().buildRound("lim",context.getResources().getColor((curr_theme==1) ? R.color.mn_dvdr_dark : R.color.mn_dvdr_lght));
                
    


    Floating Action Button (далее будем нызывать её fab) должна нести в себе основную функцию приложения. В приложении-справочнике это разумеется поиск. Т.о. при клике по кнопке будет выпадать SearchView. Для того, чтобы fab при скроллинге списка вниз/вверх красиво исчезала/появлялась рекомендую использовать библиотеку FloatingActionButton.

    Пример использования FloatingActionButton
    FloatingActionButton fab;
    ListView MainListView;
    LinearLayout searchLayout;
    SearchView searchView;
    ...
            searchLayout  = (LinearLayout) findViewById(R.id.search_view);
            searchView = (SearchView) findViewById(R.id.search);
            MainListView  = (ListView) findViewById(android.R.id.list);
            fab = (FloatingActionButton) findViewById(R.id.fab);
            // Прикрепляем fab к MainListView. 
            // Теперь при скроллинге списка вниз fab будет исчезать, а при скроллинге вверх - появляться
            fab.attachToListView(MainListView); 
            fab.setShadow(true);
            fab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Animation openSearch = AnimationUtils.loadAnimation(context, R.anim.search_down);
                    searchLayout.startAnimation(openSearch);
                    searchLayout.setVisibility(View.VISIBLE);
                    Animation hideFab = AnimationUtils.loadAnimation(context, R.anim.s_down);
                    fab.startAnimation(hideFab);
                    fab.setVisibility(View.GONE);
                    // открываем клавиатуру и активируем searchView
                    searchView.requestFocus();
                    openKeyboard();
                }
            });
    ...
    


    На этом работа по materialизации интерфейсов приложения заканчивается.

    Поиск


    Так как содержимое справочника хранится в разных html-файлах, то для того, чтобы сделать быстрый поиск по ним необходимо:
    • Поработать с самими html-файлами — добавить в каждый якоря в те места, в которые будет переходить пользователь при вводе того или иного запроса.
    • Использовать виртуальную FTS-таблицу (что это такое можно почитать тут (англ.) и тут (на русском). Если говорить кратко, то FTS позволяют пользователям выполнять полнотекстовый поиск на множестве документов).


    Таблица содержит два столбца. Первый столбец (KEY_INPUT) представляет собой список всех названий разделов и терминов, содержащихся в справочнике, иначе говоря — это список возможных запросов пользователей. Второй столбец (KEY_ANKER) — список html-файлов с якорями (т.е. файлов и позиций в этих файлах), соответствующий этим запросам. Как и для всех других таблиц SQLite, как виртуальных, так и обычных, данные из таблиц FTS получаются с помощью запросов SELECT:

    String query = "SELECT docid as _id," + KEY_INPUT + "," + KEY_ANKER + " FROM " + FTS_VIRTUAL_TABLE + " WHERE " +  KEY_INPUT + " MATCH '" + inputText + "';";
    


    При вводе текстового запроса осуществляется поиск по FTS-таблице и пользователю в выпадающем списке предоставляются варианты. При выборе осуществляется переход к нужному разделу по соответствующему якорю. Принцип показан на рисунке ниже:

    image

    SearchDbAdapter.java
    public class SearchDbAdapter {
        private static final String DATABASE_NAME = "mhdb";
        private static final String FTS_VIRTUAL_TABLE = "srcht";
        private static final int DATABASE_VERSION = 1;
        public static final String KEY_INPUT = "rqst";
        public static final String KEY_ANKER = "ankr";
    
        private static final String DATABASE_CREATE = "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE + " USING fts3(" + KEY_INPUT + "," + KEY_ANKER + ");";
    
        private final Context mCtx;
    
        // Массив с поисковыми запросами (темами и разделами, содержащимися в файлах)
        public static final String search_arr[] = {"data1 request 1","data1 request 2","data2 request 3","data2 request 4"};
        // Массив с соответствующими им html-файлами с якорями (файлы хранятся в папке assets проекта)
        public static final String ankers_arr[] = {"file1.html#an1","file2.html#an2","file1.html#an3","file1.html#an4"};
    
        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);
                int LNGTH = search_arr.length;
                ContentValues initValues = new ContentValues();
                for(int i=0; i<LNGTH; i++){
                    initValues.put(KEY_INPUT, search_arr[i]);
                    initValues.put(KEY_ANKER, ankers_arr[i]);
                    db.insert(FTS_VIRTUAL_TABLE, null, initValues);
                    initValues.clear();
                }
    
            }
            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
                onCreate(db);
            }
        }
    
        public SearchDbAdapter(Context ctx) {
            this.mCtx = ctx;
        }
    
        public SearchDbAdapter open() throws SQLException {
            mDbHelper = new DatabaseHelper(mCtx);
            mDb = mDbHelper.getWritableDatabase();
            return this;
        }
    
        public void close() {
            if (mDbHelper != null) {
                mDbHelper.close();
            }
        }
    
        public Cursor searchAnker(String inputText) throws SQLException {
            inputText = inputText.toLowerCase();
            String query = "SELECT docid as _id," + KEY_INPUT + "," + KEY_ANKER + " FROM " + FTS_VIRTUAL_TABLE + " WHERE " +  KEY_INPUT + " MATCH '" + inputText + "';";
            Cursor mCursor = mDb.rawQuery(query,null);
            if (mCursor != null) {
                mCursor.moveToFirst();
            }
            return mCursor;
        }
    }
    


    1. Пользователь вводит в SearchView поисковый запрос «data2». Слушатель SearchView вызывает метод searchAnker() класса SearchDbAdapter, который возвращает курсор (mCursor), содержащий запросы похожие на введенный текст и соответствующие этим запросам html-файлы с якорями:
    data2 request 3 — file1.html#an3
    data2 request 4 — file2.html#an4
    2. Содержащиеся в mCursor похожие запросы отображаются в выпадающем списке: data2 request 3, data2 request 4.
    3. При клике по элементам выпадающего списка осуществляется запуск ViewActivity, в которую с интентом передаётся соответствующее имя html-файла с якорем из mCursor: file1.html#an3

    Реклама и скрытые возможности приложения


    Да нужна ли она, реклама? Она портит интерфейс, а столько времени и сил потрачено, чтобы он стал красивым. Сейчас что-то заработать на рекламе можно либо, имея миллионы активных пользователей, либо на агрессивной баннерной рекламе, которая работает так:
    • пользователь скачивает обновление, в которое интегрирована рекламная библиотека;
    • стадия выжидания, чтобы пользователь в момент начала самого интересного не сразу понял из-за чего это происходит;
    • самое интересное: у пользователя поверх всех интерфейсов в других приложениях выскакивают огромные рекламные баннеры на весь экран, не кликнуть по которым — трудная задача.

    Само собой, такая реклама, мягко говоря, мало кому понравится. Я уже достаточно давно отказался от какой-либо рекламы в справочнике, и больше, наверное, из интереса добавил обычный донат — покупку пива в приложении.



    Покупка пива легко реализуется при помощи In-app Billing. Для упрощения внедрения биллинга существуют библиотеки про которые не раз писалось на хабре здесь и здесь.

    Для того, чтобы как-то оживить нашу Activity с донатом, добавлена небольшая «пасхалка». При клике по любой области экрана в правом нижнем углу будет появляться Android, размышляющий о пиве.



    Вот такое вот творчество. Возможно, если бы в правом нижнем углу появлялся Джимми Уэйлс пиво полилось бы рекой.
    • +17
    • 26,6k
    • 4
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Спасибо за статью.
      Небольшое замечание ActionBarActivity deprecated
        +1
        И Floating Action Button уже есть официальная
          0
          Верно, поэтому достаточно ActionBarActivity заменить на AppCompatActivity
          0
          Спасибо! познавательно…

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

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