16 June 2015

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

Development for Android

Введение


Несколько лет назад я писал статью на Хабр о приложении-справочнике по математике для 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, размышляющий о пиве.



Вот такое вот творчество. Возможно, если бы в правом нижнем углу появлялся Джимми Уэйлс пиво полилось бы рекой.
Tags:androidandroid appsandroid developmentразработка под androidмобильные приложениямобильная разработка и дизайнandroid studio
Hubs: Development for Android
+17
27.2k 190
Comments 4
Popular right now
Android-разработчик
from 80,000 ₽FlowwowRemote job
Android-разработчик
from 170,000 to 230,000 ₽ENJOY PROСанкт-ПетербургRemote job
Android-разработчик
to 80,000 ₽AmigowebМагнитогорскRemote job
Android Developer
from 150,000 to 200,000 ₽/difway.studioRemote job
Android-разработчик
from 170,000 to 230,000 ₽ENJOY PROСанкт-ПетербургRemote job