Как сделать Gmail похожим на окно поиска на панели действий?

В настоящее время я использую виджет SearchView внутри ActionBarcompat для фильтрации списка при поиске.

Когда пользователь начинает вводить текст, ListView в основном макете обновляется адаптером для фильтрации результатов. Я делаю это, реализуя OnQueryTextListener и фильтруя результаты при каждом нажатии клавиши.

Вместо этого я хочу создать окно поиска, похожее на Gmail, с автоматически созданным списком предложений и без изменений в базовом представлении.

введите здесь описание изображения

Я прошел через это руководство, в котором используется компонент SearchView, но для него требуется активность для поиска. Я хочу, чтобы раскрывающийся список находился над MainActivity, где у меня есть ListView (как в приложении Gmail), а не выделенное действие.
Кроме того, реализация его так же, как в руководстве, кажется излишним для того, что я хочу (просто раскрывающийся список)


person Michael    schedule 18.09.2013    source источник


Ответы (7)


Если вам просто нужен компонент, который делает то, что описано в вопросе, я предлагаю эту библиотеку. Вы также можете реализовать необычный интерфейс поиска, однако имейте в виду, что у него есть ограничения пользовательского интерфейса:

Чтобы реализовать интерфейс, похожий на приложение Gmail, вам необходимо понимать концепции:

  • Поставщики контента;
  • Сохранение данных в SQLite
  • Listview или RecyclerView и их адаптеры;
  • Передача данных между действиями;

Конечный результат должен выглядеть примерно так:

окончательный результат

Есть много (много) способов получить тот же результат (или лучше), я опишу один из них.

Часть 01: Макет

Я решил управлять всем интерфейсом в новом Activity, для этого создал три XML-макета:

  • custom_searchable.xml: собирает все элементы пользовательского интерфейса в один RelativeLayout, который будет служить контентом для SearchActivity;

    <include
        android:id="@+id/cs_header"
        layout="@layout/custom_searchable_header_layout" />
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/cs_result_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stackFromBottom="true"
        android:transcriptMode="normal"/>
    

  • custom_searchable_header_layout.xml: holds the search bar where the user will type his query. It will also contain the mic, erase and return btn;

    <RelativeLayout
        android:id="@+id/custombar_return_wrapper"
        android:layout_width="55dp"
        android:layout_height="fill_parent"
        android:gravity="center_vertical"
        android:background="@drawable/right_oval_ripple"
        android:focusable="true"
        android:clickable="true" >
    
        <ImageView
            android:id="@+id/custombar_return"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#00000000"
            android:src="@drawable/arrow_left_icon"/>
    </RelativeLayout>
    
    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_toRightOf="@+id/custombar_return_wrapper"
        android:layout_marginRight="60dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp">
    
        <EditText
            android:id="@+id/custombar_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:hint="Search..."
            android:textColor="@color/textPrimaryColor"
            android:singleLine="true"
            android:imeOptions="actionSearch"
            android:background="#00000000">
            <requestFocus/>
        </EditText>
    
    </android.support.design.widget.TextInputLayout>
    
    <RelativeLayout
        android:id="@+id/custombar_mic_wrapper"
        android:layout_width="55dp"
        android:layout_height="fill_parent"
        android:layout_alignParentRight="true"
        android:gravity="center_vertical"
        android:background="@drawable/left_oval_ripple"
        android:focusable="true"
        android:clickable="true" >
    
        <ImageView
            android:id="@+id/custombar_mic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#00000000"
            android:src="@drawable/mic_icon"/>
    </RelativeLayout>
    

  • custom_searchable_row_details.xml: holds the UI elements to be displayed in the result list to be displayed in response to the user query;

    <ImageView
        android:id="@+id/rd_left_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:layout_centerVertical="true"
        android:layout_marginLeft="5dp"
        android:src="@drawable/clock_icon" />
    
    <LinearLayout
        android:id="@+id/rd_wrapper"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:layout_toRightOf="@+id/rd_left_icon"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="50dp">
    
        <TextView
            android:id="@+id/rd_header_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/textPrimaryColor"
            android:text="Header"
            android:textSize="16dp"
            android:textStyle="bold"
            android:maxLines="1"/>
    
        <TextView
            android:id="@+id/rd_sub_header_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/textPrimaryColor"
            android:text="Sub Header"
            android:textSize="14dp"
            android:maxLines="1" />
    </LinearLayout>
    
    <ImageView
        android:id="@+id/rd_right_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:src="@drawable/arrow_left_up_icon"/>
    

Часть 02: Реализация SearchActivity

Идея состоит в том, что, когда пользователь нажимает кнопку поиска (которую вы можете разместить в любом месте), этот SearchActivity. У него есть несколько основных обязанностей:

  • Привязать к элементам пользовательского интерфейса в custom_searchable_header_layout.xml: при этом возможно:

  • чтобы предоставить прослушиватели для EditText (где пользователь будет вводить свой запрос):

    TextView.OnEditorActionListener searchListener = new TextView.OnEditorActionListener() {
    public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent event) {
        // do processing
       }
    }
    
    searchInput.setOnEditorActionListener(searchListener);
    
    searchInput.addTextChangedListener(new TextWatcher() {        
    public void onTextChanged(final CharSequence s, int start, int before, int count) {
         // Do processing
       }
    }
    
  • добавить прослушиватель для кнопки возврата (которая, в свою очередь, просто вызовет finish() и вернется к действию вызывающей стороны):

    this.dismissDialog.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        finish();
    }    
    
  • вызывает намерение для Google API преобразования речи в текст:

        private void implementVoiceInputListener () {
            this.voiceInput.setOnClickListener(new View.OnClickListener() {
    
                public void onClick(View v) {
                    if (micIcon.isSelected()) {
                        searchInput.setText("");
                        query = "";
                        micIcon.setSelected(Boolean.FALSE);
                        micIcon.setImageResource(R.drawable.mic_icon);
                    } else {
                        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    
                        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now");
    
                        SearchActivity.this.startActivityForResult(intent, VOICE_RECOGNITION_CODE);
                    }
                }
            });
        }
    

Поставщик услуг

При создании интерфейса поиска у разработчика обычно есть два варианта:

  1. Предлагать пользователю недавние запросы: это означает, что каждый раз, когда пользователь выполняет поиск, введенный запрос будет сохраняться в базе данных для последующего извлечения в качестве предложения для будущих поисков;
  2. Предлагать пользователю пользовательские варианты: разработчик попытается предсказать, чего хочет пользователь, обработав уже набранные буквы;

В обоих случаях ответы должны быть возвращены как объект Cursor, содержимое которого будет отображаться как itens в списке результатов. Весь этот процесс можно реализовать с помощью Content Provider API. Подробнее об использовании поставщиков контента можно узнать по этой ссылке. .

В случае, когда разработчик хочет реализовать поведение, описанное в 1., может быть полезно использовать стратегию расширения класса SearchRecentSuggestionsProvider. Подробнее о том, как это сделать, можно узнать по этой ссылке.

Реализация интерфейса поиска

Этот интерфейс должен обеспечивать следующее поведение:

  • Когда пользователь набирает письмо, вызов метода запроса извлеченного класса контент-провайдера должен возвращать заполненный курсор с предложением для отображения в списке - вы должны принять, чтобы не замораживать поток пользовательского интерфейса, поэтому я рекомендую выполнить это поиск в AsyncTask:

        public void onTextChanged(final CharSequence s, int start, int before, int count) {
            if (!"".equals(searchInput.getText().toString())) {
                query = searchInput.getText().toString();
    
                setClearTextIcon();
    
                if (isRecentSuggestionsProvider) {
                    // Provider is descendant of SearchRecentSuggestionsProvider
                    mapResultsFromRecentProviderToList(); // query is performed in this method
                } else {
                    // Provider is custom and shall follow the contract
                    mapResultsFromCustomProviderToList(); // query is performed in this method
                }
            } else {
                setMicIcon();
            }
        }
    
  • Внутри метода onPostExecute() вашей AsyncTask вы должны получить список (который должен исходить от метода doInBackground()), содержащий результаты, которые будут отображаться в ResultList (вы можете сопоставить его в классе POJO и передать его в свой собственный адаптер или вы можете использовать CursorAdapter, который лучше всего подходит для этой задачи):

    protected void onPostExecute(List resultList) {
         SearchAdapter adapter = new SearchAdapter(resultList);
         searchResultList.setAdapter(adapter);
    }
    
    protected List doInBackground(Void[] params) {
        Cursor results = results = queryCustomSuggestionProvider();
        List<ResultItem> resultList = new ArrayList<>();
    
        Integer headerIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
        Integer subHeaderIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
        Integer leftIconIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
        Integer rightIconIdx = results.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
    
        while (results.moveToNext()) {
            String header = results.getString(headerIdx);
            String subHeader = (subHeaderIdx == -1) ? null : results.getString(subHeaderIdx);
            Integer leftIcon = (leftIconIdx == -1) ? 0 : results.getInt(leftIconIdx);
            Integer rightIcon = (rightIconIdx == -1) ? 0 : results.getInt(rightIconIdx);
    
            ResultItem aux = new ResultItem(header, subHeader, leftIcon, rightIcon);
            resultList.add(aux);
        }
    
        results.close();
        return resultList;
    
  • Определите, когда пользователь касается кнопки поиска на программной клавиатуре. Когда он это сделает, отправьте намерение доступной для поиска активности (той, которая отвечает за обработку результатов поиска) и добавьте запрос в качестве дополнительной информации в намерение.

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case VOICE_RECOGNITION_CODE: {
                if (resultCode == RESULT_OK && null != data) {
                    ArrayList<String> text = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
                    searchInput.setText(text.get(0));
                }
                break;
            }
        }
    }
    
  • Определите, когда пользователь щелкает одно из отображаемых предложений и отправок и намерений, содержащих информацию об элементе (это намерение должно отличаться от намерения на предыдущем шаге).

    private void sendSuggestionIntent(ResultItem item) {
        try {
            Intent sendIntent = new Intent(this, Class.forName(searchableActivity));
            sendIntent.setAction(Intent.ACTION_VIEW);
            sendIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    
            Bundle b = new Bundle();
            b.putParcelable(CustomSearchableConstants.CLICKED_RESULT_ITEM, item);
    
            sendIntent.putExtras(b);
            startActivity(sendIntent);
            finish();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    

Описанных шагов должно хватить для самостоятельной реализации интерфейса. Все примеры кода взяты здесь. Я сделал эту библиотеку, которая делает многое из того, что описано выше. Он еще недостаточно протестирован, и некоторые настройки пользовательского интерфейса могут быть недоступны.

Я надеюсь, что этот ответ может помочь кому-то в этом нуждается.

person E. Fernandes    schedule 03.07.2015
comment
Новый вопрос здесь: в custom_searchable_header_layout.xml, почему вы завернули вещи в обертки. Почему у вас не было ImageView, EditText, а затем ImageView в LinearLayout или RelativeLayout? - person Solace; 30.08.2015
comment
Если я не ошибаюсь, я сделал это для волнового эффекта, который применяется к RelativeLayout. - person E. Fernandes; 02.09.2015

Я подготовил для этого небольшое руководство

http://drzon.net/how-to-create-a-clearable-autocomplete-dropdown-with-autocompletetextview/

Обзор

Мне пришлось заменить SearchView на AutoCompleteTextView, как было предложено.

Сначала создайте адаптер. В моем случае это был JSONObject ArrayAdapter. Данные, которые я хотел отобразить в раскрывающемся списке, были названием и адресом места проведения. Обратите внимание, что адаптер должен быть Filtarable и переопределять getFilter().

// adapter for the search dropdown auto suggest
ArrayAdapter<JSONObject> searchAdapter = new ArrayAdapter<JSONObject>(this, android.R.id.text1) {
private Filter filter;

public View getView(final int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = this.getLayoutInflater().inflate(R.layout.search_item, parent, false);
    }

    TextView venueName = (TextView) convertView.findViewById(R.id.search_item_venue_name);
    TextView venueAddress = (TextView) convertView.findViewById(R.id.search_item_venue_address);

    final JSONObject venue = this.getItem(position);
    convertView.setTag(venue);
    try {

        CharSequence name = highlightText(venue.getString("name"));
        CharSequence address = highlightText(venue.getString("address"));

        venueName.setText(name);
        venueAddress.setText(address);
    }
    catch (JSONException e) {
        Log.i(Consts.TAG, e.getMessage());
    }

    return convertView;

}

@Override
public Filter getFilter() {
    if (filter == null) {
        filter = new VenueFilter();
    }
    return filter;
}
};

Вот пользовательский VenueFilter :

private class VenueFilter extends Filter {

    @Override
    protected FilterResults performFiltering(CharSequence constraint) {
        List<JSONObject> list = new ArrayList<JSONObject>(venues);
        FilterResults result = new FilterResults();
        String substr = constraint.toString().toLowerCase();

        if (substr == null || substr.length() == 0) {
            result.values = list;
            result.count = list.size();
        } else {

            final ArrayList<JSONObject> retList = new ArrayList<JSONObject>();
            for (JSONObject venue : list) {
                try {
                    if (venue.getString("name").toLowerCase().contains(constraint) ||  venue.getString("address").toLowerCase().contains(constraint) || 
                         {
                        retList.add(venue);
                    }
                } catch (JSONException e) {
                    Log.i(Consts.TAG, e.getMessage());
                }
            }
            result.values = retList;
            result.count = retList.size();
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        searchAdapter.clear();
        if (results.count > 0) {
            for (JSONObject o : (ArrayList<JSONObject>) results.values) {
                searchAdapter.add(o);
            }
        }
    }

}

Теперь настройте макет для окна поиска (actionbar_search.xml):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="match_parent"
    android:layout_gravity="fill_horizontal"
    android:focusable="true" >

    <AutoCompleteTextView
        android:id="@+id/search_box"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:dropDownVerticalOffset="5dp"
        android:dropDownWidth="wrap_content"
        android:inputType="textAutoComplete|textAutoCorrect"
        android:popupBackground="@color/white"
        android:textColor="#FFFFFF" >
    </AutoCompleteTextView>

</RelativeLayout>

И макет для отдельного выпадающего элемента (название и адрес места проведения). Этот выглядит плохо, вам придется его настроить:

<?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="match_parent"
    android:textAlignment="gravity" >

    <TextView
        android:id="@+id/search_item_venue_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/cyan"
        android:layout_gravity="right" />

    <TextView
        android:id="@+id/search_item_venue_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toStartOf="@+id/search_item_venue_name"
        android:gravity="right"
        android:textColor="@color/white" />


</RelativeLayout>

Затем мы хотим поместить его в панель действий.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ActionBar actionBar = getSupportActionBar();
    actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_USE_LOGO | ActionBar.DISPLAY_SHOW_HOME
            | ActionBar.DISPLAY_HOME_AS_UP);
    LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflater.inflate(R.layout.actionbar_search, null);
    AutoCompleteTextView textView =  (AutoCompleteTextView) v.findViewById(R.id.search_box);

    textView.setAdapter(searchAdapter);

    textView.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            // do something when the user clicks
        }
    });
    actionBar.setCustomView(v);
}

Вот и все, мне еще предстоит кое-что выяснить:

  • Это помещает «всегда» поиск на панель действий, я хочу, чтобы он был похож на виджет SearchView — увеличительное стекло, которое открывается в поле поиска, когда вы щелкаете по нему (и имеет маленькую кнопку X, чтобы закрыть его и вернуться назад). в норму)
  • Еще не понял, как настроить раскрывающийся список, например, у Gmail, похоже, есть тень, у меня просто плоская, изменить цвет разделителей строк и т. Д.

В целом это экономит все накладные расходы на создание активности для поиска. Пожалуйста, добавьте, если вы знаете, как настроить его и т. д.

person Michael    schedule 19.09.2013
comment
Можете ли вы предоставить ссылку на github для проекта? - person Rohan Kandwal; 08.06.2015

Если вы хотите реализовать только эффект раскрывающегося списка, выберите AutoCompleteTextView.

И вы можете найти хороший учебник здесь

Затем, если вы хотите реализовать точный дизайн, вам нужно реализовать ActionBar, и если вы хотите реализовать более низкую версию, вам нужно реализовать ActionBarCombat вместо ActionBar

person NARESH REDDY    schedule 18.09.2013
comment
Спасибо, забыл упомянуть, что я уже использовал ActionBarCompat - person Michael; 18.09.2013

Вы должны использовать ListPopupWindow и привязать его к виджету представления поиска.

person Evgeni Roitburg    schedule 18.09.2013

Я успешно использовал ответ Майкла (https://stackoverflow.com/a/18894726/2408033), но не Мне не нравится, как это было вручную, раздувать представления и добавлять их на панель действий, переключать их состояние и т. д.

Я изменил его, чтобы использовать ActionBar ActionView вместо добавления представления вручную на панель действий/панель инструментов.

Я считаю, что это работает намного лучше, так как мне не нужно управлять состоянием открытия/закрытия и сокрытием представлений, как это было в его примере в методе toggleSearch в добавленной ссылке. Он также отлично работает с кнопкой «Назад».

В моем меню.xml

 <item
        android:id="@+id/global_search"
        android:icon="@android:drawable/ic_menu_search"
        android:title="Search"
        app:actionLayout="@layout/actionbar_search"
        app:showAsAction="ifRoom|collapseActionView" />

В моем onCreateOptionsMenu

 View actionView = menu.findItem(R.id.global_search).getActionView();
 searchTextView = (ClearableAutoCompleteTextView) actionView.findViewById(R.id.search_box);
 searchTextView.setAdapter(searchAdapter);

Полностью рабочий вариант реализации вы можете найти в моем проекте. Обратите внимание, что есть два представления поиска, поскольку я использовал фактический SearchView для фильтрации listView.

https://github.com/babramovitch/ShambaTimes/blob/master/app/src/main/java/com/shambatimes/schedule/MainActivity.java

person Ben987654    schedule 23.08.2015

Пожалуйста, обратитесь к этому примеру, который реализует именно то, что вы просили: http://wptrafficanalyzer.in/blog/android-searchview-widget-with-actionbarcompat-library/

person Evgeni Roitburg    schedule 18.09.2013

Вам нужно использовать атрибут collapseActionView.

Атрибут CollarActionView позволяет вашему SearchView расширяться, занимая всю панель действий, и сворачиваться обратно в обычный элемент панели действий, когда он не используется.

Например:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/search"
          android:title="@string/search_title"
          android:icon="@drawable/ic_search"
          android:showAsAction="collapseActionView|ifRoom"
          android:actionViewClass="android.widget.SearchView" />
</menu>
person Paresh Mayani    schedule 18.09.2013