Добавляем поисковый фильтр в RecyclerView в Android

Добавляем поисковый фильтр в RecyclerView в Android

Сегодня мы разберём пример добавления поиска в RecyclerView. Это несложно и мы используем поисковый виджет из Toolbar для ввода поискового запроса. Для наглядности, я создам приложение, аналогичное адресной книге, в котором мы сможем искать нужный контакт по имени или номеру телефона.

1. Фильтр поиска RecyclerView – getFilter()

В Android есть класс Filterable для отбора данных по условию. Обычно для этого переопределяется метод getFilter() в классе адаптера, в котором по условию отфильтровываются только нужные данные из списка. Ниже приведён пример метода getFilter() для поиска контакта по имени или номеру телефона из списка контактов.

@Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence charSequence) {
                String charString = charSequence.toString();
                if (charString.isEmpty()) {
                    contactListFiltered = contactList;
                } else {
                    List<Contact> filteredList = new ArrayList<>();
                    for (Contact row : contactList) {
 
                        // здесь мы отбираем нужные данные 
                        if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {
                            filteredList.add(row);
                        }
                    }
 
                    contactListFiltered = filteredList;
                }
 
                FilterResults filterResults = new FilterResults();
                filterResults.values = contactListFiltered;
                return filterResults;
            }
 
            @Override
            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
                contactListFiltered = (ArrayList<Contact>) filterResults.values;
 
                // обновляем список отфильтрованных данных
                notifyDataSetChanged();
            }
        };
    }
2. JSON примера

Для этого примера я буду использовать json с адреса https://api.androidhive.info/json/contacts.json . Он содержит список контактов, каждый из которых содержит имя, номер телефона и изображение профиля.

[{
        "name": "Tom Hardy",
        "image": "https://api.androidhive.info/json/images/tom_hardy.jpg",
        "phone": "(541) 754-3010"
    },
    {
        "name": "Johnny Depp",
        "image": "https://api.androidhive.info/json/images/johnny.jpg",
        "phone": "(452) 839-1210"
    }
]
Создаём проект

1. Создайте новый проект в Android Studio (File ⇒ New Project) и выберите Basic Activity в списке шаблонов.

2. Откройте build.gradle из папки app и добавьте зависимости RecyclerView, Glide и Volley:

build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // ...
 
    // recycler view
    implementation 'com.android.support:recyclerview-v7:26.1.0'
 
    // библиотека для работы с изображениями glide
    implementation 'com.github.bumptech.glide:glide:4.3.1'
 
    // http библиотека volley 
    implementation 'com.android.volley:volley:1.0.0'
    implementation 'com.google.code.gson:gson:2.6.2'
 
}

3. Замените файлы ресурсов strings.xml, dimens.xml, colors.xml таким образом:

strings.xml

<resources>
    <string name="app_name">RecyclerView Search</string>
    <string name="action_settings">Settings</string>
    <string name="toolbar_title">Contacts</string>
    <string name="action_search">Search</string>
    <string name="search_hint">Type name…</string>
</resources>



dimens.xml

<resources>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="activity_margin">16dp</dimen>
    <dimen name="thumbnail">40dp</dimen>
    <dimen name="row_padding">10dp</dimen>
    <dimen name="contact_name">15dp</dimen>
    <dimen name="contact_number">12dp</dimen>
</resources>



colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#111</color>
    <color name="colorPrimaryDark">#FFF</color>
    <color name="colorAccent">#ea3732</color>
    <color name="contact_name">#333333</color>
    <color name="contact_number">#8c8c8c</color>
</resources>

4. Скачайте файл res.zip и добавьте изображения из него в папку res. В этих папках также присутствует иконка поиска в тулбар.

5. Создайте класс MyApplication.java, расширяющий класс Application:


package info.androidhive.recyclerviewsearch;
 
import android.app.Application;
import android.text.TextUtils;
 
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
 
public class MyApplication extends Application {
 
    public static final String TAG = MyApplication.class
            .getSimpleName();
 
    private RequestQueue mRequestQueue;
 
    private static MyApplication mInstance;
 
    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
    }
 
    public static synchronized MyApplication getInstance() {
        return mInstance;
    }
 
    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        }
 
        return mRequestQueue;
    }
 
    public <T> void addToRequestQueue(Request<T> req, String tag) {
        // зададим tag по-умолчанию, если он пуст
        req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
        getRequestQueue().add(req);
    }
 
    public <T> void addToRequestQueue(Request<T> req) {
        req.setTag(TAG);
        getRequestQueue().add(req);
    }
 
    public void cancelPendingRequests(Object tag) {
        if (mRequestQueue != null) {
            mRequestQueue.cancelAll(tag);
        }
    }
}

6. Откройте файл AndroidManifest.xml и добавьте MyApplication в тег . Также добавьте разрешение INTERNET, т.к. нам необходимо будет делать запросы по http.



<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.recyclerviewsearch">
 
    <uses-permission android:name="android.permission.INTERNET" />
 
    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
 
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

7. Для разбора json нам понадобится класс POJO для сериализации. Создайте класс Contact.java, добавьте в него поля «имя» (name), «картинка» (image) и «номер телефона» (phone).

package info.androidhive.recyclerviewsearch;
 
public class Contact {
    String name;
    String image;
    String phone;
 
    public Contact() {
    }
 
    public String getName() {
        return name;
    }
 
    public String getImage() {
        return image;
    }
 
    public String getPhone() {
        return phone;
    }
}

8. Создайте класс MyDividerItemDecoration.java. В нём мы добавим отступ слева к линии разделения элементов списка.

package info.androidhive.recyclerviewsearch;
 
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;
 
/**
 * Created by ravi on 17/11/17.
 */
 
public class MyDividerItemDecoration  extends RecyclerView.ItemDecoration {
 
    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };
 
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
 
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
 
    private Drawable mDivider;
    private int mOrientation;
    private Context context;
    private int margin;
 
    public MyDividerItemDecoration(Context context, int orientation, int margin) {
        this.context = context;
        this.margin = margin;
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }
 
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }
 
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
 
    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
 
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left + dpToPx(margin), top, right, bottom);
            mDivider.draw©;
        }
    }
 
    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
 
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top + dpToPx(margin), right, bottom - dpToPx(margin));
            mDivider.draw©;
        }
    }
 
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
 
    private int dpToPx(int dp) {
        Resources r = context.getResources();
        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));
    }
}
Создаём Adapter с фильтром

Итак, ресурсы готовы, приступим к написанию класса адаптера. Это главный компонент нашей статьи, будьте внимательны.

9. Создайте представление (layout) user_row_item.xml с описанной ниже разметкой. Это представление отображает один контакт списка. В нём два TextView для отображения имени и номера телефона и ImageView для вывода картинки контакта.

user_row_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/selectableItemBackground"
    android:clickable="true"
    android:paddingBottom="@dimen/row_padding"
    android:paddingLeft="@dimen/activity_margin"
    android:paddingRight="@dimen/activity_margin"
    android:paddingTop="@dimen/row_padding">
 
    <ImageView
        android:id="@+id/thumbnail"
        android:layout_width="@dimen/thumbnail"
        android:layout_height="@dimen/thumbnail"
        android:layout_centerVertical="true"
        android:layout_marginRight="@dimen/row_padding" />
 
    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/thumbnail"
        android:fontFamily="sans-serif-medium"
        android:textColor="@color/contact_name"
        android:textSize="@dimen/contact_name" />
 
    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_toRightOf="@id/thumbnail"
        android:textColor="@color/contact_number"
        android:textSize="@dimen/contact_number" />
 
</RelativeLayout>

10. Создайте класс ContactsAdapter.java, реализующий Filterable, в котором вас попросят переопределить метод getFilter().

При этом в методе getFilter() строка поиска будет передана методу performFiltering(). Интерфейс ContactsAdapterListener содержит вызов метода onContactSelected() при выборе контакта из списка.

package info.androidhive.recyclerviewsearch;
 
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
 
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
 
import java.util.ArrayList;
import java.util.List;
 
public class ContactsAdapter extends RecyclerView.Adapter<ContactsAdapter.MyViewHolder>
        implements Filterable {
    private Context context;
    private List<Contact> contactList;
    private List<Contact> contactListFiltered;
    private ContactsAdapterListener listener;
 
    public class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView name, phone;
        public ImageView thumbnail;
 
        public MyViewHolder(View view) {
            super(view);
            name = view.findViewById(R.id.name);
            phone = view.findViewById(R.id.phone);
            thumbnail = view.findViewById(R.id.thumbnail);
 
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    // отправим выбранный контакт в callback
                    listener.onContactSelected(contactListFiltered.get(getAdapterPosition()));
                }
            });
        }
    }
 
 
    public ContactsAdapter(Context context, List<Contact> contactList, ContactsAdapterListener listener) {
        this.context = context;
        this.listener = listener;
        this.contactList = contactList;
        this.contactListFiltered = contactList;
    }
 
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.user_row_item, parent, false);
 
        return new MyViewHolder(itemView);
    }
 
    @Override
    public void onBindViewHolder(MyViewHolder holder, final int position) {
        final Contact contact = contactListFiltered.get(position);
        holder.name.setText(contact.getName());
        holder.phone.setText(contact.getPhone());
 
        Glide.with(context)
                .load(contact.getImage())
                .apply(RequestOptions.circleCropTransform())
                .into(holder.thumbnail);
    }
 
    @Override
    public int getItemCount() {
        return contactListFiltered.size();
    }
 
    @Override
    public Filter getFilter() {
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence charSequence) {
                String charString = charSequence.toString();
                if (charString.isEmpty()) {
                    contactListFiltered = contactList;
                } else {
                    List<Contact> filteredList = new ArrayList<>();
                    for (Contact row : contactList) {
 
                        if (row.getName().toLowerCase().contains(charString.toLowerCase()) || row.getPhone().contains(charSequence)) {
                            filteredList.add(row);
                        }
                    }
 
                    contactListFiltered = filteredList;
                }
 
                FilterResults filterResults = new FilterResults();
                filterResults.values = contactListFiltered;
                return filterResults;
            }
 
            @Override
            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
                contactListFiltered = (ArrayList<Contact>) filterResults.values;
                notifyDataSetChanged();
            }
        };
    }
 
    public interface ContactsAdapterListener {
        void onContactSelected(Contact contact);
    }
}
5. Добавляем виджет поиска

Нам осталось добавить SearchView в Toolbar, отрисовать RecyclerView, прочитав json и передать поисковый запрос в adapter.

11. Откройте / создайте файл menu_main.xml в папке res ⇒ menus и добавьте в него виджет SearchView, также сделаем его видимым всегда.

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="info.androidhive.recyclerviewsearch.MainActivity">
    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search_black_24dp"
        android:orderInCategory="100"
        android:title="@string/action_search"
        app:showAsAction="always"
        app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>

12. В папке res ⇒ xml создайте xml файл searchable.xml (если папки xml нет, создайте её)

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/search_hint"
    android:label="@string/app_name" />

13. Откройте файл AndroidManifest.xml и настройте поиск:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.recyclerviewsearch">
 
    <uses-permission android:name="android.permission.INTERNET" />
 
    <application ...>
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
 
            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable" />
 
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
 
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

14. Откройте файлы главной activity activity_main.xml и content_main.xml и добавьте элемент RecyclerView.


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.androidhive.recyclerviewsearch.MainActivity">
 
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">
 
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/white"
            app:popupTheme="@style/AppTheme.PopupOverlay" />
 
    </android.support.design.widget.AppBarLayout>
 
    <include layout="@layout/content_main" />
 
</android.support.design.widget.CoordinatorLayout>




content_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="info.androidhive.recyclerviewsearch.MainActivity"
    tools:showIn="@layout/activity_main">
 
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="vertical" />
 
</RelativeLayout>

15. Откройте файл MainActivity.java и добавьте нижеописанный код.

> В методе fetchContacts() запрос Volley получает json. Полученный json сериализуется с помощью Gson и все контакты из него добавляются в список. Вызов mAdapter.notifyDataSetChanged() перерисовывает RecyclerView.

> В onCreateOptionsMenu() создаётся меню и выводится SearchView.

> searchView.setOnQueryTextListener() ожидает ввод текста в поле поиска от пользователя. Введённый запрос затем передаётся адаптеру через mAdapter.getFilter().filter(query), затем RecyclerView обновляется, показывая только отфильтрованные данные.

> onContactSelected() вызывается при выборе контакта в списке.

MainActivity.java

package info.androidhive.recyclerviewsearch;
 
import android.app.SearchManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
 
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
 
import org.json.JSONArray;
 
import java.util.ArrayList;
import java.util.List;
 
public class MainActivity extends AppCompatActivity implements ContactsAdapter.ContactsAdapterListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    private RecyclerView recyclerView;
    private List<Contact> contactList;
    private ContactsAdapter mAdapter;
    private SearchView searchView;
 
    // url для получения контактов в json
    private static final String URL = "https://api.androidhive.info/json/contacts.json";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
 
        // делаем красивым toolbar 
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setTitle(R.string.toolbar_title);
 
        recyclerView = findViewById(R.id.recycler_view);
        contactList = new ArrayList<>();
        mAdapter = new ContactsAdapter(this, contactList, this);
 
        // белый фон строки состояния
        whiteNotificationBar(recyclerView);
 
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.addItemDecoration(new MyDividerItemDecoration(this, DividerItemDecoration.VERTICAL, 36));
        recyclerView.setAdapter(mAdapter);
 
        fetchContacts();
    }
 
    /**
     * получает json посредством http-запроса
     */
    private void fetchContacts() {
        JsonArrayRequest request = new JsonArrayRequest(URL,
                new Response.Listener<JSONArray>() {
                    @Override
                    public void onResponse(JSONArray response) {
                        if (response == null) {
                            Toast.makeText(getApplicationContext(), "Couldn't fetch the contacts! Pleas try again.", Toast.LENGTH_LONG).show();
                            return;
                        }
 
                        List<Contact> items = new Gson().fromJson(response.toString(), new TypeToken<List<Contact>>() {
                        }.getType());
 
                        // добавляем контакты в список
                        contactList.clear();
                        contactList.addAll(items);
 
                        // обновляем recycler view
                        mAdapter.notifyDataSetChanged();
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // ошибка в получении json
                Log.e(TAG, "Error: " + error.getMessage());
                Toast.makeText(getApplicationContext(), "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
 
        MyApplication.getInstance().addToRequestQueue(request);
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
 
        // Ассоциируем настройку поиска с SearchView
        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        searchView = (SearchView) menu.findItem(R.id.action_search)
                .getActionView();
        searchView.setSearchableInfo(searchManager
                .getSearchableInfo(getComponentName()));
        searchView.setMaxWidth(Integer.MAX_VALUE);
 
        // отслеживаем изменения текста в поисковом поле
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                // фильтруем recycler view при окончании ввода
                mAdapter.getFilter().filter(query);
                return false;
            }
 
            @Override
            public boolean onQueryTextChange(String query) {
                // фильтруем recycler view при изменении текста
                mAdapter.getFilter().filter(query);
                return false;
            }
        });
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Обрабатываем меню здесь. Нажатия на Home/Up
        // будут автоматически обработаны так, как указано в
        // родительской activity в AndroidManifest.xml.
        int id = item.getItemId();
 
        //noinspection SimplifiableIfStatement
        if (id == R.id.action_search) {
            return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
 
    @Override
    public void onBackPressed() {
        // при нажатии кнопки "назад" закрываем поиск
        if (!searchView.isIconified()) {
            searchView.setIconified(true);
            return;
        }
        super.onBackPressed();
    }
 
    private void whiteNotificationBar(View view) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            int flags = view.getSystemUiVisibility();
            flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            view.setSystemUiVisibility(flags);
            getWindow().setStatusBarColor(Color.WHITE);
        }
    }
 
    @Override
    public void onContactSelected(Contact contact) {
        Toast.makeText(getApplicationContext(), "Selected: " + contact.getName() + ", " + contact.getPhone(), Toast.LENGTH_LONG).show();
    }
}
Добавляем поисковый фильтр в RecyclerView в Android

Надеюсь, я понятно описал как фильтровать данные RecyclerView. Если у вас, всё ещё есть вопросы, пишите их в комментариях.

Happy Coding!

Источник: «Android RecyclerView adding Search Filter»

Добавляем поисковый фильтр в RecyclerView в Android

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка: Разработка кросс-платформенного мобильного приложения на голом JSON

Предыдущие несколько месяцев я посвятил работе над совершенно новым способом разработки нативных приложений для iOS и Android и назвал его Jasonette.

С Jasonette можно описать всю логику приложения всего в одном файле JSON-разметки. А если всё ваше приложение состоит из JSON, его можно загружать так же, как любые другие данные.

Нет нужды хранить логику приложения на устройстве, поэтому вы можете дорабатывать её и обновлять так часто, как требуется, простым обновлением JSON на сервере. Ваше приложение будет обновлено при следующем открытии.

В видео ниже кратко описан весь процесс (на англ.):

Jasonette состоит из многих компонентов. Там есть функции, шаблоны, стили и многое другое, и всё это в JSON разметке. Вы можете писать супер-изощрённые нативные приложения в стиле [simple_tooltip content=’Model-View-Controller (Модель-Представление-Контроллер) — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер — таким образом, что модификация каждого компонента может осуществляться независимо.’]MVC[/simple_tooltip].

Сегодня мы разберём только «Представления»:

  1. Как Jasonette выражает различные кросс-платформенные шаблоны UI в JSON.
  2. Как реализованы внутренние преобразования JSON-в-Native.

Базовая структура

Издалека можно подумать, что Jasonette работает подобно браузеру. Но вместо того, чтобы работать с HTML и отрисовывать web-view, Jasonette загружает JSON и «на лету» собирает нативное представление.

Разметка JSON здесь самая обычная, но при этом она строится на нескольких стандартах. Во-первых, структура начинается с ключа $jason, который имеет двух потомков: head и body. Выглядит она так:

{
  "$jason": {
    "head": {
      .. метаданные документа ...
    },
    "body": {
      .. содержимое, выводимое на экран ..
    }
  }
}

Философия проектирования

Если посмотреть на то, как построены большинство мобильных приложений, то можно увидеть, что они используют всего несколько видов представлений:

  1. Список с вертикальной прокруткой
  2. Список с горизонтальной прокруткой
  3. Абсолютное позиционирование
  4. Сетка

Взглянем на первые три, ибо они самые распространённые.

1. Секции — описание списка с прокруткой

Списки с прокруткой это самый популярный способ построения интерфейса приложений. В Jasonette мы называем их sections.

Они бывают двух видов: вертикальные и горизонтальные.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON


Реализация — Вертикальные секции

Под iOS Jasonette реализует их с помощью UITableView. Под Android — с RecyclerView.


{
  "body": {
    "sections": [{
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}

Под iOS эта JSON разметка создаст UITableView с тремя UITableViewCells, каждая из которых содержит UILabel, с соответствующими атрибутами.
Под Android будет создано RecyclerView с тремя элементами, каждый из которых это TextView, выводящий соответствующий элемент.
Всё это будет сконструировано программно, без какого-либо использования Storyboards (в iOS) или файлов макета XML (в Android).

Реализация — Горизонтальные секции

Синтаксически нет разницы с вертикальными секциями. Единственное, что мы изменили, это type в “horizontal”.


{
  "body": {
    "sections": [{
      "type": "horizontal",
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}
2. Элементы — Описываем макет каждого элементы прокрутки

Теперь мы понимаем, как работают представления верхнего уровня, перейдём к items. Каждая секция может состоять из множества прокручиваемых элементов, items. Помните, что каждый элемент имеет фиксированный размер, и внутри самого элемента нет других прокручиваемых элементов.

Элемент может быть:

  • Единичным компонентом типа label (метка), image (картинка), button (кнопка), textarea (текстовое поле) и т.д.
  • Комбинацией любых этих элементов

К счастью, iOS и Android имеют похожие системы построения представлений, UIStackView и LinearLayout, соответственно. И эти системы в свою очередь похожи на CSS Flexbox, что облегчает нам работу. И вдобавок к этому, нативные системы представлений бесконечно компонуемы — как показано ниже, вы можете создать вертикальный макет, горизонтальный макет, а также скомпоновать и горизонтальный и вертикальный в одном, и так до бесконечности.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка кросс-платформенного мобильного приложения на голом JSON

Для создания вертикального макета, выставим type как vertical, затем настроим остальные компоненты:


{
  "items": [{
    "type": "vertical",
    "components": [
      {
        "type": "label",
        "text": "First"
      }, 
      {
        "type": "label",
        "text": "Second"
      }, 
      {
        "type": "label",
        "text": "Third"
      }
    ]
  }]
}

Здесь то же самое. Просто установим type в horizontal:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "label",
        "text": "Rick"
      }
    ]
  }]
}

Встроить один макет в другой так же просто:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "vertical",
        "components": [{
          "type": "label",
          "text": "User"
        }, {
          "type": "label",
          "text": "Rick"
        }]
      }
    ]
  }]
}

Чтобы не усложнять понимание, я пока не упоминал про стилизование элементов приложения, но это делается очень просто. Всё, что вам нужно для этого, это добавить объект style с описанием атрибутов font (шрифт), size (размер), width (ширина), height (высота), color (цвет), background (фон), corner_radius (угловой радиус), opacity (прозрачность) и т.п.

3. Слои — абсолютное позиционирование

Иногда вам может понадобиться разместить элементы в определённой части экрана без прокрутки и перемещений. Jasonette поддерживает такое размещение в layers.

На текущий момент в слое можно разместить только два типа дочерних объектов: image и label. Вы можете разместить их в любой части экрана. Ниже пример этого:

Разработка кросс-платформенного мобильного приложения на голом JSON

В этом примере у нас созданы две метки (для температуры и состояния погоды) и картинка (иконка камеры). Они размещены в своих координатах и не двигаются:


{
  "$jason": {
    "body": {
      "style": {
        "background": "camera"
      },
      "layers": [
        {
          "type": "label",
          "text": "22°C",
          "style": {
            "font": "HelveticaNeue-Light",
            "size": "20",
            "top": "50",
            "left": "50%-100",
            "width": "200",
            "align": "center"
          }
        },
        {
          "type": "label",
          "text": "few clouds",
          "style": {
            "font": "HelveticaNeue",
            "size": "15"
          }
        },
        {
          "type": "image",
          "url": "https://s3.amazonaws.com/.../camera%402x.png",
          "style": {
            "bottom": "100",
            "width": "30",
            "color": "#ffffff",
            "right": "30"
          }
        }
      ]
    }
  }
}

И этого достаточно для того, чтобы сделать практически любое приложение, какое вы только можете представить.

Вот ещё примеры того, что построено в Jasonette:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON
За пределами представлений

Прочитав всё это, вы можете подумать:

  • «Ух ты, круто! Я должен это попробовать!» или
  • «Да, я конечно могу сделать приложение на поиграться, но реальное — никогда!»

Ещё раз повторюсь, здесь мы говорили про самую лёгкую часть работы с Jasonette — представления. Но вы действительно можете построить приложение практически любой сложности в JSON.

Вы можете соединить действия с элементами UI и они сработают, как только пользователь кликнет на них. Также вы можете вызывать действия одно за другим, основываясь на возвращаемых ими успехах/ошибках. А можно даже автоматически вызывать действия на основе происходящих событий с устройством или приложением.

Что ещё возможно?

Вам обязательно нужен сервер, хранящий JSON, а в остальном Jasonette совершенно автономна. И этот JSON может прилетать отовсюду: с локального устройства, с удаленных серверов, да хоть с raspberry pi!

  1. У вас есть веб-приложение? Тогда вы с лёгкостью построите мобильное приложение, вызывая серверный API
  2. Вам можно вообще не думать о сервере. Храните JSON файл на Pastebin или Github!
  3. Сконвертируйте любой веб-сайт в приложение. В Jasonette есть мощный парсер HTML-в-JSON на базе библиотеки cheerio, которая позволяет преобразовать любой HTML в объект JSON. Ну и, конечно, вы можете сами сформировать нужный JSON.

Ещё немного примеров:

Разработка кросс-платформенного мобильного приложения на голом JSON

Фото-приложение, делающее снимок камерой устройства и выкладывающая его в S3, затем она создаёт запись в ленте новостей на своём сервере:

Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение Eliza Chatbot для iOS и Android на базе Node.js:

Приложение для микро-блоггинга:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение, конвертирующее сайт HTML в JSON структуру, а затем в мобильное приложение:

Разработка кросс-платформенного мобильного приложения на голом JSON
Заключение

Jasonette это пока молодой проект. Версия для iOS вышла в конце 2016 г, а для Android ещё немного позже.

Но уже сейчас у неё есть огромное сообщество разработчиков и она активно развивается.

Звучит круто? Тогда Jasonette будет очень рад вам!

Источник

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).