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, размышляющий о пиве.

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

ссылка на оригинал статьи http://habrahabr.ru/post/260045/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *