Search Function with Custom Search Suggestions

In this tutorial I will show you how you can create a dedicated search view with custom search suggestions („autocomplete“) in combination with a list of recent search queries. The data source for the autocomplete comes from a REST call that is executed every time the user types in a letter in the search field.

SearchView with custom search suggestions (autocomplete) loaded from ArrayList.

Search Functionality in Action

Have a look at this video to see it in action:

Prerequisites / General Info

In this project I am using the ButterKnife library for the binding of the layout and views (i.e. @BindView(R.id.relative_layout)). If you don’t use ButterKnife you have to alter the parts of the code snippets accordingly.

app // build.gradle

minSdkVersion 21
targetSdkVersion 26

implementation "com.jakewharton:butterknife:8.8.1"
implementation "com.zsoltsafrany:needle:1.0.0"

AndroidManifest.xml

Add this part to your AndroidManifest.xml file. Replace AppTheme.RedToolbar with whatever style you have for your toolbar in your project.

<activity
android:name=".SearchActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.RedToolbar">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchActivity" />

</activity>

res/xml/searchable.xml

Create a searchable.xml file and place it in the /res/xml subfolder in your project.

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="TYPE TO SEARCH"
android:label="LABEL"
android:searchSuggestSelection=" ?"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

SearchActivity

SearchActivity.java is an activity with a toolbar with an active search input field that is initially empty until search results will populate the RecyclerList. While the user is typing search suggestions will be displayed in a menu below the search field. It also saves the last 5 submitted search queries in the SharedPreferences.

The class looks like this:

SearchActivity.java

public class SearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, OnSearchResultListItemSelectedListener, OnSearchCompletedListener {
private static String TAG = SearchActivity.class.getSimpleName();

static final int GRID_COLUMN_COUNT = 2;
private static final String COLUMN_ID = "_id";
private static final String COLUMN_SUGGESTION = "suggestion";
private static final String COLUMN_CONTEXT = "context";
private static final String COLUMN_SCORE = "score";
private static final String DEFAULT = "default";

private SearchResultListItemRecyclerViewAdapter searchResultListItemRecyclerViewAdapter;
private MovieSearchSuggestionAdapter movieSearchSuggestionAdapter;
private String queryString;

private SearchManager searchManager;
private SearchView searchView;

private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;

public static String[] columns = new String[]{COLUMN_ID, COLUMN_SUGGESTION, COLUMN_CONTEXT, COLUMN_SCORE};

@BindView(R.id.relative_layout)
RelativeLayout relativeLayout;
@BindView(R.id.searchresult_list)
RecyclerView recyclerView;
@BindView(R.id.empty)
View emptyView;
@BindView(R.id.empty_image_view)
ImageView emptyImageView;
@BindView(R.id.empty_label)
TextView emptyLabel;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);

getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);

searchResultListItemRecyclerViewAdapter = new SearchResultListItemRecyclerViewAdapter(new ArrayList<>(), this, this);

GridLayoutManager gridLayoutManager = new GridLayoutManager(this, GRID_COLUMN_COUNT);
recyclerView.setLayoutManager(gridLayoutManager);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
switch (searchResultListItemRecyclerViewAdapter.getItemViewType(position)) {
case ListItem.TYPE_HEADER:
// Header uses 2 columns in 2 column grid
return GRID_COLUMN_COUNT;
default:
// Movie uses 1 column in 2 column grid
return 1;
}
}
});

recyclerView.setAdapter(searchResultListItemRecyclerViewAdapter);
recyclerView.addItemDecoration(new MovieGridListItemSpacingDecoration(GRID_COLUMN_COUNT, getResources().getDimensionPixelSize(R.dimen.grid_list_item_spacing), true));

movieSearchSuggestionAdapter = new MovieSearchSuggestionAdapter(getApplicationContext(), null, null, searchView);

sharedPreferences = getSharedPreferences(Preferences.CINEMAN, Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}

@Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
}

@Override
public void onStart() {
super.onStart();
if (searchResultListItemRecyclerViewAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyLabel.setText("Universelle Filmsuche");
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_view_options_menu, menu);

searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
searchView.setMaxWidth(Integer.MAX_VALUE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(this);
searchView.setIconifiedByDefault(true);
searchView.setQueryHint(getString(R.string.movies_search));
searchView.setElevation(10);
searchView.onActionViewExpanded(); // Activate on launch
searchView.setSuggestionsAdapter(movieSearchSuggestionAdapter);

return true;
}

@OnClick(R.id.empty)
void clickEmptyView() {
searchView.onActionViewExpanded();
}

@Override
public boolean onQueryTextSubmit(String query) {
queryString = query.trim();
if (queryString.length() > 2) {
handleAndUpdateSearchHistoryList(queryString);
searchResultListItemRecyclerViewAdapter.globalMovieSearch(query);
} else {
Handler handler = new Handler();
handler.postDelayed(() -> Snackbar.make(relativeLayout, "Bitte gebe mindestens drei Zeichen für die Suche ein.", Snackbar.LENGTH_LONG).show(), 400);
}

ViewUtils.hideKeyboard(this, searchView);
return true;
}

@Override
public boolean onQueryTextChange(String newText) {
queryString = newText.trim();
autocomplete(queryString);
return true;
}

private void autocomplete(final String query) {
final MatrixCursor matrixCursor = new MatrixCursor(columns);
final Object[] object = new Object[]{0, DEFAULT, DEFAULT, DEFAULT};

// Get the Search History Items
String searchHistoryCommaSeparated = sharedPreferences.getString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, "");
List<String> searchHistoryItemList = new ArrayList<>();
String[] searchHistoryArray = searchHistoryCommaSeparated.split(",");
if (searchHistoryArray.length > 1) {
Collections.addAll(searchHistoryItemList, searchHistoryArray);
}

// Delete duplicate search history items
HashSet<String> hashSet = new HashSet<>();
hashSet.addAll(searchHistoryItemList);
searchHistoryItemList.clear();
searchHistoryItemList.addAll(hashSet);

if ((query != null) && (query.trim().length() > 2)) {
CinemanAPI.movie().autocomplete(query.trim(), new RepositoryEntityListCallback<Autocomplete>() {

@Override
public void onSuccess(List<Autocomplete> tList, boolean isCachedResponse) {

int i = 0;
while (i < tList.size()) {
object[0] = i;
object[1] = tList.get(i).getSuggestion();
object[2] = tList.get(i).getContext();
object[3] = tList.get(i).getScore();

matrixCursor.addRow(object);
i++;
}

for (String historyQuery : searchHistoryItemList) {
Autocomplete ac = new Autocomplete();
ac.setContext("history");
ac.setScore(1000);
ac.setSuggestion(historyQuery);
tList.add(ac);

object[0] = i;
object[1] = ac.getSuggestion();
object[2] = ac.getContext();
object[3] = ac.getScore();

matrixCursor.addRow(object);
i++;
}

updateAdapter(matrixCursor, tList);
}

@Override
public void onFailure(Exception e) {
Log.w(TAG, e.getLocalizedMessage());
}
});
} else if ((query != null) && (query.trim().length() > 0)) {
List<Autocomplete> tList = new ArrayList<>();

int i = 0;
for (String historyQuery : searchHistoryItemList) {
if (historyQuery.length() > 0) {
Autocomplete ac = new Autocomplete();
ac.setContext("history");
ac.setScore(1000);
ac.setSuggestion(historyQuery);
tList.add(ac);

object[0] = i;
object[1] = ac.getSuggestion();
object[2] = ac.getContext();
object[3] = ac.getScore();

matrixCursor.addRow(object);
i++;
}
}

updateAdapter(matrixCursor, tList);
} else {
updateAdapter(matrixCursor, new ArrayList<>());
}
}

private void updateAdapter(MatrixCursor matrixCursor, List<Autocomplete> tList) {
movieSearchSuggestionAdapter = new MovieSearchSuggestionAdapter(getApplicationContext(), matrixCursor, tList, searchView);
searchView.setSuggestionsAdapter(movieSearchSuggestionAdapter);
movieSearchSuggestionAdapter.swapCursor(matrixCursor);
movieSearchSuggestionAdapter.notifyDataSetChanged();
}

private void updateUI() {
if (searchResultListItemRecyclerViewAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyImageView.setImageResource(R.drawable.shrug);
emptyLabel.setText("NOTHING FOUND");
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}

@Override
public void onTheatreListItemSelected(Theatre theatre) {
// TODO kommt später mal...
}

@Override
public void onMovieGridListItemSelected(Movie movie) {
if (movie != null) {
// Intent intent = new Intent(this, MovieDetailActivity.class);
// intent.putExtra(movie);
// startActivity(intent);
}
}

private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
queryString = intent.getStringExtra(SearchManager.QUERY);
if (queryString.length() > 1) {
searchResultListItemRecyclerViewAdapter.globalMovieSearch(queryString);
searchView.clearFocus();
}
}
}

private void handleAndUpdateSearchHistoryList(String nextSearchQueryAddition) {
String searchHistoryCommaSeparated = sharedPreferences.getString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, "");
List<String> searchHistoryItemList = new ArrayList<>();

String[] searchHistoryArray = searchHistoryCommaSeparated.split(",");
if (searchHistoryArray.length > 0) {
Collections.addAll(searchHistoryItemList, searchHistoryArray);
}

searchHistoryItemList.add(nextSearchQueryAddition);
if (searchHistoryItemList.size() > 5) {
searchHistoryItemList = searchHistoryItemList.subList(1, searchHistoryItemList.size()); // Limit to 5 items
}

StringBuilder csvBuilder = new StringBuilder();
for (String query : searchHistoryItemList) {
csvBuilder.append(query);
csvBuilder.append(",");
}

String historyQueriesString = csvBuilder.toString();
String historyQueriesCommaSeparated = historyQueriesString.substring(0, historyQueriesString.length() - ",".length());

editor.putString(Preferences.PREFS_SEARCH_HISTORY_ITEMS, historyQueriesCommaSeparated);
editor.apply();
}

@Override
public void onOnSearchCompleted() {
updateUI();

// Hide the autocomplete dropdown list
onQueryTextChange("");
}

@Override
public boolean onSuggestionSelect(int position) {
searchView.clearFocus();
return false;
}

@Override
public boolean onSuggestionClick(int position) {
searchView.clearFocus();
return false;
}
}

OnSearchResultListItemSelectedListener.java

interface OnSearchResultListItemSelectedListener {
void onMovieGridListItemSelected(Movie movie);
}

RepositoryEntityListCallback.java

public abstract class RepositoryEntityListCallback<T> {
public abstract void onSuccess(List<T> entityList, boolean isCachedResponse);

public abstract void onFailure(Exception e);
}

Autocomplete.java

public class Autocomplete {
private int score;
private String suggestion;
private String context;

public int getScore() {
return score;
}

public void setScore(int score) {
this.score = score;
}

public String getSuggestion() {
return suggestion;
}

public void setSuggestion(String suggestion) {
this.suggestion = suggestion;
}

public String getContext() {
return context;
}

public void setContext(String context) {
this.context = context;
}

}

activity_search.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
tools:context=".SearchActivity">

<android.support.v7.widget.RecyclerView xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/searchresult_list"
android:name=".SearchActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="GridLayoutManager"
tools:context=".SearchActivity"
tools:listitem="@layout/movie_grid_list_item" />

<RelativeLayout
android:id="@+id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
android:clickable="true"
android:visibility="gone">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:paddingBottom="33dp">

<ImageView
android:id="@+id/empty_image_view"
app:srcCompat="@drawable/ic_onboarding_map"
android:layout_width="match_parent"
android:layout_height="67dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/empty_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingLeft="48dp"
android:paddingTop="16dp"
android:paddingRight="48dp"
android:text="@string/settings_regions_empty_alert"
android:textAppearance="@style/TextAppearance.primaryText" />

</LinearLayout>

</RelativeLayout>
</RelativeLayout>

search_view_options_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<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">

<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:showAsAction="always"
android:title="@string/movies_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"
tools:ignore="AppCompatResource" />

</menu>

movie_grid_list_item.xml

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

<ImageView
android:id="@+id/moviePoster"
android:layout_width="match_parent"
android:layout_height="@dimen/grid_list_item_poster_height"
android:scaleType="centerCrop" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorGridItemBackground"
android:orientation="horizontal">

<TextView
android:id="@+id/movieNameLabel"
android:gravity="center_vertical|left"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="2"
android:paddingLeft="@dimen/grid_list_item_label_padding_left_right"
android:textAppearance="@style/TextAppearance.posterText" />

<LinearLayout
android:id="@+id/watchlistButton"
android:layout_width="48dp"
android:layout_height="56dp"
android:clickable="true">

<ImageView
android:id="@+id/watchlistButtonImageView"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="8dp"
android:layout_marginTop="14dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_add_circle_outline_black" />

</LinearLayout>

</LinearLayout>

</LinearLayout>

MovieSearchSuggestionAdapter.java

public class MovieSearchSuggestionAdapter extends CursorAdapter implements View.OnClickListener {
private final static String TAG = MovieSearchSuggestionAdapter.class.getSimpleName();

private Context context;
private final LayoutInflater mInflater;
private ImageView imageView;
private TextView suggestionTextView;
private SearchView searchView;

private final List<Autocomplete> autocompleteList;

public MovieSearchSuggestionAdapter(final Context context, final Cursor cursor, final List<Autocomplete> autocompleteList, SearchView searchView) {
super(context, cursor, 0);

this.context = context;
this.searchView = searchView;
this.autocompleteList = autocompleteList;
this.mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final View view = mInflater.inflate(R.layout.search_result_item_layout, parent, false);

imageView = view.findViewById(R.id.icon);
suggestionTextView = view.findViewById(R.id.suggestion);

return view;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
final int position = cursor.getPosition();
if (cursorInBounds(position)) {

final Autocomplete auto = autocompleteList.get(position);
suggestionTextView.setText(auto.getSuggestion());

if (auto.getContext().equalsIgnoreCase("movie")) {
imageView.setImageResource(R.drawable.ic_search);
} else if (auto.getContext().equalsIgnoreCase("history")) {
imageView.setImageResource(R.drawable.ic_time);
}

view.setTag(position);
view.setOnClickListener(this);
} else {
// Something went wrong
}
}

private boolean cursorInBounds(final int position) {
return position < autocompleteList.size();
}

@Override
public void onClick(final View view) {
final int position = (Integer) view.getTag();

if (cursorInBounds(position)) {
final Autocomplete selected = autocompleteList.get(position);

ViewUtils.hideKeyboard(context, searchView);
searchView.setQuery(selected.getSuggestion(), true);
} else {
// Something went wrong
}
}


}

sarch_result.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="@color/colorBackground">

<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_margin="10dp"
android:padding="5dp"
android:src="@drawable/ic_search" />

<TextView
android:id="@+id/suggestion"
android:layout_width="wrap_content"
android:layout_height="23dp"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/icon" />

</RelativeLayout>

SearchResultListItemRecyclerViewAdapter.java

class SearchResultListItemRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final static String TAG = SearchResultListItemRecyclerViewAdapter.class.getSimpleName();

private List<Movie> movieList;
private List<ListItem> listItems;
private final OnSearchCompletedListener onSearchCompletedListener;
private final OnSearchResultListItemSelectedListener onSearchResultListItemSelectedListener;

SearchResultListItemRecyclerViewAdapter(List<Movie> items, OnSearchResultListItemSelectedListener onSearchResultListItemSelectedListener, OnSearchCompletedListener onSearchCompletedListener) {
listItems = new ArrayList<>();
this.onSearchResultListItemSelectedListener = onSearchResultListItemSelectedListener;
this.onSearchCompletedListener = onSearchCompletedListener;
setMovieList(items);

globalMovieSearch(null);
}

@Override
public int getItemViewType(int position) {
return listItems.get(position).getType();
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ListItem.TYPE_HEADER) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.generic_list_header_item, parent, false);
return new GenericListHeaderViewHolder(itemView);
}

View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.movie_search_result_grid_list_item, parent, false);
return new MovieSearchResultGridListItemViewHolder(itemView);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int type = getItemViewType(position);

if (type == ListItem.TYPE_HEADER) {
GenericListHeaderItem genericListHeaderItem = (GenericListHeaderItem) listItems.get(position);
GenericListHeaderViewHolder genericListHeaderItemViewHolder = (GenericListHeaderViewHolder) holder;
genericListHeaderItemViewHolder.setLabelText(genericListHeaderItem.getLabel());
return;
} else {
final MovieGridListItem movieGridListItem = (MovieGridListItem) listItems.get(position);
MovieSearchResultGridListItemViewHolder movieSearchResultGridListItemViewHolder = (MovieSearchResultGridListItemViewHolder) holder;
movieSearchResultGridListItemViewHolder.setMoviePosition(movieGridListItem.getMoviePosition());
movieSearchResultGridListItemViewHolder.setMovie(movieGridListItem.getMovie(), CinemanApplication.getCurrentAppInstance().getApplicationContext());
movieSearchResultGridListItemViewHolder.view.setOnClickListener(v -> onSearchResultListItemSelectedListener.onMovieGridListItemSelected(movieGridListItem.getMovie()));
}
}


@Override
public int getItemCount() {
return listItems.size();
}

void setMovieList(List<Movie> movieList) {
this.movieList = movieList;
}

/**
* Replace "
CinemanAPI.movie().searchMovies()" with your own REST call / data access with the search query from the parameter for the search. The return type in this example is an ArrayList with Movie objects which gets forwarded to the formatMovieList() method where we format the list to head- and body-type of recycler list (or grid for that matter) items. This way we could group the search results in various sections i.e. movies, theatres etc.
*
*
@param query
*/
public void globalMovieSearch(final String query) {
if ((query != null) && (query.trim().length() > 2)) {
CinemanAPI.movie().searchMovies(query.trim(), new RepositoryEntityListCallback<Movie>() {

@Override
public void onSuccess(List<Movie> mList, boolean isCachedResponse) {
movieList.clear();
movieList.addAll(mList);

formatMovieList(mList);
}

@Override
public void onFailure(Exception e) {
Log.e(TAG, e.getLocalizedMessage());
formatMovieList(new ArrayList<>());
}
});
} else {
movieList.clear();
}
}

/**
* Loop through the movie list and create head- and movie items
*
*
@param mList
*/
private void formatMovieList(List<Movie> mList) {
final List<ListItem> newListItems = new ArrayList<>();

Needle.onBackgroundThread().withTaskType("globalMovieSearch").serially().execute(() -> {
GenericListHeaderItem genericListHeaderItem = new GenericListHeaderItem();
genericListHeaderItem.setLabel(CinemanApplication.getCurrentAppInstance().getString(R.string.movies_title));
newListItems.add(genericListHeaderItem);

int i = 2;
for (Movie movie : mList) {
MovieGridListItem movieGridListItem = new MovieGridListItem();
movieGridListItem.setMoviePosition(++i);
movieGridListItem.setMovie(movie);
newListItems.add(movieGridListItem);
}

if (mList.size() == 0) {
newListItems.clear();
}

Needle.onMainThread().execute(() -> {
listItems.clear();
listItems.addAll(newListItems);
notifyDataSetChanged();
onSearchCompletedListener.onOnSearchCompleted();
});
});
}

}

generic_list_header_item.xml

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

<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.subHeader" />
</LinearLayout>

movie_search_result_grid_list_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="@color/ticketingHighlightColor">

<ImageView
android:id="@+id/movie_poster"
android:layout_width="match_parent"
android:layout_height="@dimen/grid_list_item_poster_height"
android:scaleType="centerCrop" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/movie_poster"
android:layout_alignParentBottom="true"
android:background="@color/colorGridItemBackground"
android:orientation="horizontal">

<TextView
android:id="@+id/movie_title_label"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginLeft="@dimen/grid_list_item_label_padding_left_right"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical|left"
android:maxLines="2"
android:text="Movie Title"
android:textAppearance="@style/TextAppearance.posterText" />

</LinearLayout>

</RelativeLayout>

GenericListHeaderViewHolder.java

class GenericListHeaderViewHolder extends RecyclerView.ViewHolder {
final View view;
private String labelText;

@BindView(R.id.label)
TextView labelView;

String getLabelText() {
return labelText;
}

void setLabelText(String labelText) {
this.labelText = labelText;
labelView.setText(labelText);
}

GenericListHeaderViewHolder(View view) {
super(view);
this.view = view;
ButterKnife.bind(this, view);
}

@Override
public String toString() {
return super.toString() + " '" + labelView.getText() + "'";
}
}

MovieSearchResultGridListItemViewHolder.java

class MovieSearchResultGridListItemViewHolder extends RecyclerView.ViewHolder {
private final static double posterRatio = 1.3825;
final View view;
private Movie movie;
private int moviePosition = -1;
private ColorDrawable placeHolderColorDrawable;

@BindView(R.id.movie_poster)
ImageView moviePoster;
@BindView(R.id.movie_title_label)
TextView movieNameLabel;


Movie getMovie() {
return movie;
}

void setMovie(Movie movie, Context context) {
this.movie = movie;
movieNameLabel.setText(movie.getTitle());
if (movie.getColorMeta() != null && movie.getColorMeta().getPosterBest() != null && movie.getColorMeta().getPosterBest().size() > 0) {
String hexColor = String.format("#%06X", (0xFFFFFF & movie.getColorMeta().getPosterBest().get(0)));
placeHolderColorDrawable.setColor(Color.parseColor(hexColor));
Picasso.with(context).load(movie.getPoster().getUrl2x()).placeholder(placeHolderColorDrawable).into(moviePoster);
} else {
Picasso.with(context).load(movie.getPoster().getUrl2x()).into(moviePoster);
}

public int getMoviePosition() {
return moviePosition;
}

public void setMoviePosition(int moviePosition) {
this.moviePosition = moviePosition;
}

MovieSearchResultGridListItemViewHolder(View view) {
super(view);
this.view = view;
ButterKnife.bind(this, view);

placeHolderColorDrawable = new ColorDrawable();
int imageWidth = (CinemanApplication.getCurrentAppInstance().getScreenWidth() - ((int) view.getResources().getDimension(R.dimen.grid_list_item_spacing) * (MovieGridListFragment.GRID_COLUMN_COUNT + 1))) / MovieGridListFragment.GRID_COLUMN_COUNT;
int desiredHeight = (int) (imageWidth * posterRatio);
if (moviePoster.getLayoutParams().height != desiredHeight) {
moviePoster.getLayoutParams().height = desiredHeight;
moviePoster.requestLayout();
}
}

@Override
public String toString() {
return super.toString() + " '" + movieNameLabel.getText() + "'";
}
}

Launching the Search

To start the search add this code block to any other activity or button click event.

Intent intent = new Intent(getActivity(), SearchActivity.class);
startActivity(intent);

Got improvements or questions? Or is anything missing? Let me know in the comments section below.

Android SDK: Add application to the autostart

Add your Android application to the autostart

To add an Android application to the device’s autostart is actually pretty easy. But if some minor but important settings are wrong the autostart function doesn’t work. Here comes a fully functional example which opens your Android application after the device has finished its booting sequence.

  • Create a new Android application in Eclipse called „OnBootCompletedExample“. The namespace is on.boot.completed.
  • Leave the main Activity as is:

    OnBootCompletedExampleActivity.java

    <br />
    import android.app.Activity;<br />
    import android.os.Bundle;</p>
    <p>public class OnBootCompletedExampleActivity extends Activity {<br />
    	/** Called when the activity is first created. */<br />
    	@Override<br />
    	public void onCreate(Bundle savedInstanceState) {</p>
    <p>		super.onCreate(savedInstanceState);<br />
    		setContentView(R.layout.main);<br />
    	}<br />
    }<br />
    
  • Create a new class called Autostart.java which extends BroadcastReceiver:

    Autostart.java

    <br />
    import android.content.BroadcastReceiver;<br />
    import android.content.Context;<br />
    import android.content.Intent;<br />
    import android.util.Log;</p>
    <p>public class Autostart extends BroadcastReceiver {</p>
    <p>	/**<br />
    	 * Listens for Android's BOOT_COMPLETED broadcast and then executes<br />
    	 * the onReceive() method.<br />
    	 */<br />
    	@Override<br />
    	public void onReceive(Context context, Intent arg1) {<br />
    		Log.i("Autostart", "BOOT_COMPLETED broadcast received. Executing following code:");</p>
    <p>		Intent intent = new Intent(context, StarterService.class);<br />
    		context.startService(intent);<br />
    	}<br />
    }<br />
    

    In the AndroidManifest.xml file we are going to add this class as a receiver. This class will listen to the broadcast call the Android OS sends after the boot sequence has finished (meaning after the phone started up).

  • Create another class called StarterService.java which will extend Service:

    StarterService.java

    <br />
    import android.app.Service;<br />
    import android.content.Intent;<br />
    import android.os.IBinder;<br />
    import android.util.Log;<br />
    import android.widget.Toast;</p>
    <p>public class StarterService extends Service {<br />
    	private static final String TAG = "MyService";</p>
    <p>	@Override<br />
    	public IBinder onBind(Intent intent) {<br />
    		return null;<br />
    	}</p>
    <p>	@Override<br />
    	public void onDestroy() {<br />
    		Toast.makeText(this, "My Service stopped", Toast.LENGTH_LONG).show();<br />
    		Log.d(TAG, "onDestroy");<br />
    	}</p>
    <p>	/**<br />
    	 * The started service opens the Activity.<br />
    	 */<br />
    	@Override<br />
    	public void onStart(Intent intent, int startid) {<br />
    		Intent intents = new Intent(getBaseContext(), OnBootCompletedExampleActivity.class);<br />
    		intents.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);<br />
    		startActivity(intents);</p>
    <p>		Toast.makeText(this, "My Service started", Toast.LENGTH_LONG).show();<br />
    		Log.d(TAG, "onStart");<br />
    	}<br />
    }<br />
    

    When the class Autostart receives the BOOT_COMPLETED broadcast from Android OS it will start the StarterService which then starts the Android Activity „OnBootCompletedExampleActivity“.

  • And finally we update the AndroidManifest.xml with the changes we need to do in order for the application to work how we want:

    AndroidManifest.xml

    <br />
    <?xml version="1.0" encoding="utf-8"?><br />
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="on.boot.completed"
        android:installLocation="internalOnly"
        android:versionCode="1"
        android:versionName="1.0" ></p>
    <p>    <uses-sdk
            android:minSdkVersion="7"
            android:targetSdkVersion="7" /></p>
    <p>    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /></p>
    <p>    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" ><br />
            <activity
                android:name="on.boot.completed.OnBootCompletedExampleActivity"
                android:label="@string/app_name"
                android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.MAIN" /></p>
    <p>                <category android:name="android.intent.category.LAUNCHER" /><br />
                </intent-filter><br />
            </activity></p>
    <p>        <receiver android:name="on.boot.completed.Autostart" ><br />
                <intent-filter><br />
                    <action android:name="android.intent.action.QUICKBOOT_POWERON" /><br />
                    <action android:name="android.intent.action.BOOT_COMPLETED" /><br />
                </intent-filter><br />
            </receiver></p>
    <p>        <service
                android:name="on.boot.completed.StarterService"
                android:enabled="true"
                android:exported="true" /><br />
        </application></p>
    <p></manifest><br />
    

    A few notes about AndroidManifest.xml:

    • It’s very important to add
      <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

      before the <application> tag.

    • If the app is installed on the SD Card the autostart will not work! That’s why it’s important that we add
      android:installLocation="internalOnly"

      after package=“on.boot.completed“

    • Also add
      <action android:name="android.intent.action.QUICKBOOT_POWERON" />

      in addition to

      <action android:name="android.intent.action.BOOT_COMPLETED" />

      to the receiver. Some HTC devices won’t catch the BOOT_COMPLETED broadcast.

That’s all. Now build and run your app. After it has been started turn off your phone and turn it back on and the app should start automatically after the device has booted up.

Here you can download the working Android project:
OnBootCompletedExample