Create a search experience

Now you have simple app with a showing map. In this page, you'll create a simple search and display the search results in a list. You'll also learn how to filter the data displayed on the map based on the search results.

Start by creating a new activity or fragment to facilitate searches on your application. Here we will be using a fragment to search and show search results on, and using a bottom sheet to display the results. We also create a search input field on our main map activity for the user to input the text they want to search for.

Both UI component implementations can be found in the getting started app sample.Getting Started App sample for java or Getting started App sample for kotlin

To perform a search you will need to have initiated MapsIndoors. This was shown in the previous section of the getting started tutorial how you do this. guides

For advanced usage of the search functionality read the Search guide and tutorials connected to it. guides

Show a list of search results

Create a search method that takes a search string as a parameter. In this example we only use the setTake on the MPFilter to limit our result to 30 locations.


void search(String searchQuery) {
//Query with a string to search on
MPQuery mpQuery = new MPQuery.Builder().setQuery(searchQuery).build();
//Filter for the search query, only taking 30 locations
MPFilter mpFilter = new MPFilter.Builder().setTake(30).build();
//Query for the locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter, (list, miError) -> {
//Implement UI handling of the search result here
}
}

private fun search(searchQuery: String) {
//Query with a string to search on
val mpQuery = MPQuery.Builder().setQuery(searchQuery).build()
//Filter for the search query, only taking 30 locations
val mpFilter = MPFilter.Builder().setTake(30).build()

//Gets locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { list: List<MPLocation?>?, miError: MIError? ->
//Implement UI handling of the search result here
}
}

To be able to search we need a text input field where a user can write what they want to search for.

We'll start by adding a search bar on the top of our MapsActivity. So we add it to the root layout.

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="1dp"
android:background="@drawable/card_shape_top">

<ImageButton
android:id="@+id/search_btn"
android:layout_margin="10dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/ic_baseline_search_24"/>

<com.google.android.material.textfield.TextInputLayout
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/search_btn">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_edit_txt"
android:hint="search"
android:imeOptions="actionSearch"
android:inputType="text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>

We then add an EditorActionListener and a OnClickListener to our text input field and our search button in our onCreate. That calls our search method with the text in the search input field. Find the full onCreate example here: MapsActivity.java or MapsActivity.kt

...
//ClickListener to start a search, when the user clicks the search button
mSearchBtn.setOnClickListener(view -> {
if (mSearchTxtField.getText().length() != 0) {
//There is text inside the search field. So lets do the search.
search(mSearchTxtField.getText().toString());
}
});

//Listener for when the user searches through the keyboard
mSearchTxtField.setOnEditorActionListener((textView, i, keyEvent) -> {
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_SEARCH) {
if (textView.getText().length() != 0) {
//There is text inside the search field. So lets do the search.
search(textView.getText().toString());
}
return true;
}
return false;
});
...
...
//Listener for when the user searches through the keyboard
mSearchTxtField.setOnEditorActionListener { textView, i, _ ->
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_SEARCH) {
if (textView.text.isNotEmpty()) {
search(textView.text.toString())
}
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}

//ClickListener to start a search, when the user clicks the search button
searchBtn.setOnClickListener {
if (mSearchTxtField.text?.length != 0) {
//There is text inside the search field. So lets do the search.
search(mSearchTxtField.text.toString())
}
}
...

To accompany this we'll create a fragment and a BottomSheet to handle the searchFragment.

Start by creating a fragment with a view-only consisting of a RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="@dimen/list_item_spacing_half"
android:paddingBottom="@dimen/list_item_spacing_half"
tools:context=".SearchFragment"
tools:listitem="@layout/fragment_search__list_item" />
public class SearchFragment extends Fragment {

private List<MPLocation> mLocations = null;
private MapsActivity mMapActivity = null;

public static SearchFragment newInstance(List<MPLocation> locations, MapsActivity mapsActivity) {
final SearchFragment fragment = new SearchFragment();
fragment.mLocations = locations;
fragment.mMapActivity = mapsActivity;
return fragment;
}

...

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
final RecyclerView recyclerView = (RecyclerView) view;
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(new SearchItemAdapter(mLocations, mMapActivity));
}

...
}
class SearchFragment : Fragment() {
private var mLocations: List<MPLocation?>? = null
private var mMapActivity: MapsActivity? = null

override fun onViewCreated(view: View, @Nullable savedInstanceState: Bundle?) {
val recyclerView = view as RecyclerView
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = mLocations?.let { locations -> SearchItemAdapter(locations, mMapActivity) }
}

...

companion object {
fun newInstance(locations: List<MPLocation?>?, mapsActivity: MapsActivity?): SearchFragment {
val fragment = SearchFragment()
fragment.mLocations = locations
fragment.mMapActivity = mapsActivity
return fragment
}
}
}

See the full example of SearchFragment here: SearchFragment.java or SearchFragment.kt

Create a RecyclerView adapter and the accompanying Viewholder:

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

<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:id="@+id/location_image"/>

<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/location_image"
android:background="?attr/selectableItemBackground"
android:paddingLeft="@dimen/list_item_spacing"
android:paddingTop="@dimen/list_item_spacing_half"
android:paddingRight="@dimen/list_item_spacing"
android:paddingBottom="@dimen/list_item_spacing_half"
android:textAppearance="@style/TextAppearance.AppCompat.Large"/>

</RelativeLayout>

Create a getter for your MapControl object on the MapsActivity so that it can be used in the adapter.

class SearchItemAdapter extends RecyclerView.Adapter<ViewHolder> {
private final List<MPLocation> mLocations;
private final MapsActivity mMapActivity;

...

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.text.setText(mLocations.get(position).getName());

if (mMapActivity != null) {
LocationDisplayRule locationDisplayRule = mMapActivity.getMapControl().getDisplayRule(mLocations.get(position));

if (locationDisplayRule != null && locationDisplayRule.getIcon() != null) {
mMapActivity.runOnUiThread(()-> {
holder.imageView.setImageBitmap(locationDisplayRule.getIcon());
});
}else {
//Location does not have a special displayRule using type Display rule
LocationDisplayRule typeDisplayRule = mMapActivity.getMapControl().getDisplayRule(mLocations.get(position).getType());

if (typeDisplayRule != null) {
mMapActivity.runOnUiThread(()-> {
holder.imageView.setImageBitmap(typeDisplayRule.getIcon());
});
}
}
}
}

...
}
class ViewHolder extends RecyclerView.ViewHolder {
...
}
internal class SearchItemAdapter(private val mLocations: List<MPLocation?>, private val mMapActivity: MapsActivity?) : RecyclerView.Adapter<ViewHolder>() {

...

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.text.text = mLocations[position]?.name

...

if (mMapActivity != null) {
val locationDisplayRule: LocationDisplayRule? = mMapActivity.getMapControl().getDisplayRule(mLocations[position])

if (locationDisplayRule != null && locationDisplayRule.icon != null) {
mMapActivity.runOnUiThread(Runnable {
holder.imageView.setImageBitmap(
locationDisplayRule.icon
)
})
} else {
//Location does not have a special displayRule using type Display rule
val typeDisplayRule: LocationDisplayRule? = mMapActivity.getMapControl().getDisplayRule(mLocations[position]?.type)

if (typeDisplayRule != null) {
mMapActivity.runOnUiThread(Runnable {
holder.imageView.setImageBitmap(
typeDisplayRule.icon
)
})
}
}
}
}

...

}

internal class ViewHolder(inflater: LayoutInflater, parent: ViewGroup?) :
...
}

See the full example of SearchItemAdapter and accompanying ViewHolder here: SearchItemAdapter.java or SearchItemAdapter.kt

Implement a BottomSheet to the bottom of your MapsActivity Layout. The root of the view should be a CoordinatorLayout. You can find the full xml layout on MapsActivity Layout

    <FrameLayout
android:id="@+id/standardBottomSheet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
style="?attr/bottomSheetStyle"
android:background="@drawable/card_shape_bottom"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:behavior_hideable="true"
android:focusable="true">

</FrameLayout>

Now add the search fragment to the BottomSheet in the search query method on your MapsActivity.

void search(String searchQuery) {
//Query with a string to search on
MPQuery mpQuery = new MPQuery.Builder().setQuery(searchQuery).build();
//Filter for the search query, only taking 30 locations
MPFilter mpFilter = new MPFilter.Builder().setTake(30).build();

//Query for the locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter, (list, miError) -> {
//Check if there is no error and the list is not empty
if (miError == null && !list.isEmpty()) {
//Create a new instance of the search fragment
mSearchFragment = SearchFragment.newInstance(list, this);
//Make a transaction to the bottomsheet
getSupportFragmentManager().beginTransaction().replace(R.id.standardBottomSheet, mSearchFragment).commit();

...
}
}
}
private fun search(searchQuery: String) {
//Query with a string to search on
val mpQuery = MPQuery.Builder().setQuery(searchQuery).build()
//Filter for the search query, only taking 30 locations
val mpFilter = MPFilter.Builder().setTake(30).build()

//Query for the locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { list: List<MPLocation?>?, miError: MIError? ->
//Check if there is no error and the list is not empty
if (miError == null && !list.isNullOrEmpty()) {
//Create a new instance of the search fragment
mSearchFragment = SearchFragment.newInstance(list, this)
//Make a transaction to the bottom sheet
supportFragmentManager.beginTransaction().replace(R.id.standardBottomSheet, mSearchFragment).commit()

...
}
}
}

See the full example of the search method here: MapsActivity.java or MapsActivity.kt

Filter Locations on map based on search results

When getting a search result, you might want to only show those search results on the map. You can do this through calling displaySearchResults(List<MPLocation> locations) on MapControl. This method has different parameters to make it easier for you as a developer to fit your exact need in terms of animation and more. This can be read in the JavaDoc of MapControl.

The standard implementation animates the camera to fit all Locations on the map and show the info window of a Location, if it's a list of only one Location.

When you are done showing the search results you can call clearMap() on MapControl.

mMapControl.displaySearchResults(locationList);
mMapControl.displaySearchResults(locationList)

The accompanying UI and implementation of this search experience can be found in the getting started app sample. Getting Started App sample or Getting Started App sample kotlin.

Next up: Directions