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 + " З " + 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 + " З " + 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, розмірковує про пиво.



Ось таке ось творчість. Можливо, краще щоб анімації був Джиммі Вейлз.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.