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