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

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

d02476d9e2a942669867ca6a9b6218e6.png

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

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

cf6bcd696dff4c76bdc423373e3f9998.png790b64620e534a90983ac1c12a83def7.png

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

0d5ffbf84d7b44078519af0e4866819e.png

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

02904d030a7d492a80f6ab27f9fc3556.png

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

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

themes.xml

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

Расскажу теперь, как сделать так, чтобы применять тему сразу ко всем 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: 433506d561944889a3c82d3c71859365.png

При подборе сочетаний цветов для темы в стиле 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

} @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#an3data2 request 4 — file2.html#an42. Содержащиеся в mCursor похожие запросы отображаются в выпадающем списке: data2 request 3, data2 request 4.3. При клике по элементам выпадающего списка осуществляется запуск ViewActivity, в которую с интентом передаётся соответствующее имя html-файла с якорем из mCursor: file1.html#an3Реклама и скрытые возможности приложения Да нужна ли она, реклама? Она портит интерфейс, а столько времени и сил потрачено, чтобы он стал красивым. Сейчас что-то заработать на рекламе можно либо, имея миллионы активных пользователей, либо на агрессивной баннерной рекламе, которая работает так: пользователь скачивает обновление, в которое интегрирована рекламная библиотека; стадия выжидания, чтобы пользователь в момент начала самого интересного не сразу понял из-за чего это происходит; самое интересное: у пользователя поверх всех интерфейсов в других приложениях выскакивают огромные рекламные баннеры на весь экран, не кликнуть по которым — трудная задача. Само собой, такая реклама, мягко говоря, не понравится. Я уже достаточно давно отказался от какой-либо рекламы в справочнике, и больше, наверное, из интереса добавил обычный донат — покупку пива в приложении.ac23addf0446412a9cfd3bfd5bc15bb0.jpg

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

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

456e126b600048609aa221a53465dd68.png

Вот такое вот творчество. Возможно, лучше чтобы в анимации был Джимми Уэйлс.

© Habrahabr.ru