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

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

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

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

enter image description here

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

Ответ 1

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

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

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

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

final result

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

Часть 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: удерживает строку поиска, где пользователь будет вводить свой запрос. Он также будет содержать микрофон, стирание и возврат 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: содержит элементы пользовательского интерфейса, которые будут отображаться в списке результатов, который будет отображаться в ответ на запрос пользователя;

    <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
       }
    }
    
  • добавить слушателя для кнопки возврата (которая по своему ходу будет просто называть финиш() и вернуться к активности вызывающего абонента):

    this.dismissDialog.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        finish();
    }    
    
  • вызывает намерение API-интерфейса google-речи:

        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);
                    }
                }
            });
        }
    

Поставщик контента

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

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

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

В случае, когда разработчик хочет реализовать поведение, описанное в 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();
        }
    }
    

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

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

Ответ 2

Я создал небольшой учебник, чтобы сделать это

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, похоже, есть тень, моя просто плоская, меняет цвет разделителей строк и т.д.

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

Ответ 3

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

Ответ 4

Если вы хотите реализовать только эффект понижения, перейдите для AutoCompleteTextView

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

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

Ответ 5

Я успешно использовал ответ Майклса (fooobar.com/questions/48853/...), но мне не понравилось, как это было вручную, раздувая представления и добавляя их в панель действий, переключая его состояние и т.д.

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

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

В моем menu.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 для фильтрации спискаView.

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

Ответ 7

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

Атрибут collapseActionView позволяет вашему 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>