mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-24 17:10:33 +00:00
Merge pull request #748 from mauriciocolli/improve-search
Improve search
This commit is contained in:
commit
952c8428d8
@ -48,7 +48,7 @@ dependencies {
|
|||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
compile 'com.github.TeamNewPipe:NewPipeExtractor:7ae274b'
|
compile 'com.github.TeamNewPipe:NewPipeExtractor:1df3f67'
|
||||||
|
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||||
|
@ -12,7 +12,6 @@ import java.io.InterruptedIOException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
@ -135,11 +134,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
|
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
|
||||||
for (Map.Entry<String, List<String>> entry : con.getHeaderFields().entrySet()) {
|
|
||||||
System.err.println(entry.getKey() + ": " + entry.getValue());
|
|
||||||
}
|
|
||||||
String inputLine;
|
|
||||||
|
|
||||||
|
String inputLine;
|
||||||
while ((inputLine = in.readLine()) != null) {
|
while ((inputLine = in.readLine()) != null) {
|
||||||
response.append(inputLine);
|
response.append(inputLine);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import io.reactivex.Flowable;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||||
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
|
||||||
|
|
||||||
@ -27,11 +28,20 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
|||||||
@Override
|
@Override
|
||||||
int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query")
|
||||||
|
int deleteAllWhereQuery(String query);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> getAll();
|
Flowable<List<SearchHistoryEntry>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit")
|
||||||
|
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||||
@Override
|
@Override
|
||||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit")
|
||||||
|
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.search;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.TooltipCompat;
|
import android.support.v7.widget.TooltipCompat;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -25,12 +27,14 @@ import android.view.ViewGroup;
|
|||||||
import android.view.animation.DecelerateInterpolator;
|
import android.view.animation.DecelerateInterpolator;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.AdapterView;
|
import android.widget.EditText;
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ReCaptchaActivity;
|
import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
@ -38,25 +42,34 @@ import org.schabi.newpipe.extractor.StreamingService;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||||
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.history.HistoryListener;
|
import org.schabi.newpipe.history.HistoryListener;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
import io.reactivex.Flowable;
|
||||||
import io.reactivex.Notification;
|
import io.reactivex.Notification;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.ObservableSource;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.functions.BiFunction;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.functions.Function;
|
import io.reactivex.functions.Function;
|
||||||
import io.reactivex.functions.Predicate;
|
import io.reactivex.functions.Predicate;
|
||||||
@ -65,21 +78,22 @@ import io.reactivex.subjects.PublishSubject;
|
|||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> {
|
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Search
|
// Search
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The suggestions will appear only if the query meet this threshold (>=).
|
* The suggestions will only be fetched from network if the query meet this threshold (>=).
|
||||||
|
* (local ones will be fetched regardless of the length)
|
||||||
*/
|
*/
|
||||||
private static final int THRESHOLD_SUGGESTION = 3;
|
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
|
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
|
||||||
*/
|
*/
|
||||||
private static final int SUGGESTIONS_DEBOUNCE = 150; //ms
|
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int filterItemCheckedId = -1;
|
protected int filterItemCheckedId = -1;
|
||||||
@ -88,48 +102,55 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@State
|
@State
|
||||||
protected int serviceId = -1;
|
protected int serviceId = -1;
|
||||||
@State
|
@State
|
||||||
protected String searchQuery = "";
|
protected String searchQuery;
|
||||||
|
@State
|
||||||
|
protected String lastSearchedQuery;
|
||||||
@State
|
@State
|
||||||
protected boolean wasSearchFocused = false;
|
protected boolean wasSearchFocused = false;
|
||||||
|
|
||||||
private int currentPage = 0;
|
private int currentPage = 0;
|
||||||
private int currentNextPage = 0;
|
private int currentNextPage = 0;
|
||||||
private String searchLanguage;
|
private String searchLanguage;
|
||||||
private boolean showSuggestions = true;
|
private boolean isSuggestionsEnabled = true;
|
||||||
|
|
||||||
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
||||||
private Disposable searchDisposable;
|
private Disposable searchDisposable;
|
||||||
private Disposable suggestionWorkerDisposable;
|
private Disposable suggestionDisposable;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private SuggestionListAdapter suggestionListAdapter;
|
private SuggestionListAdapter suggestionListAdapter;
|
||||||
|
private SearchHistoryDAO searchHistoryDAO;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private View searchToolbarContainer;
|
private View searchToolbarContainer;
|
||||||
private AutoCompleteTextView searchEditText;
|
private EditText searchEditText;
|
||||||
private View searchClear;
|
private View searchClear;
|
||||||
|
|
||||||
|
private View suggestionsPanel;
|
||||||
|
private RecyclerView suggestionsRecyclerView;
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static SearchFragment getInstance(int serviceId, String query) {
|
public static SearchFragment getInstance(int serviceId, String query) {
|
||||||
SearchFragment searchFragment = new SearchFragment();
|
SearchFragment searchFragment = new SearchFragment();
|
||||||
searchFragment.setQuery(serviceId, query);
|
searchFragment.setQuery(serviceId, query);
|
||||||
searchFragment.searchOnResume();
|
|
||||||
|
if (!TextUtils.isEmpty(query)) {
|
||||||
|
searchFragment.setSearchOnResume();
|
||||||
|
}
|
||||||
|
|
||||||
return searchFragment;
|
return searchFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
|
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
|
||||||
* (it will only start searching if the query is not null or empty)
|
|
||||||
*/
|
*/
|
||||||
private void searchOnResume() {
|
private void setSearchOnResume() {
|
||||||
if (!TextUtils.isEmpty(searchQuery)) {
|
|
||||||
wasLoading.set(true);
|
wasLoading.set(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment's LifeCycle
|
// Fragment's LifeCycle
|
||||||
@ -139,6 +160,16 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
public void onAttach(Context context) {
|
public void onAttach(Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||||
|
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||||
|
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -146,15 +177,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
showSearchOnStart();
|
||||||
|
initSearchListeners();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
wasSearchFocused = searchEditText.hasFocus();
|
wasSearchFocused = searchEditText.hasFocus();
|
||||||
|
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) searchDisposable.dispose();
|
||||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) disposables.clear();
|
||||||
hideSoftKeyboard(searchEditText);
|
hideKeyboardSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -162,10 +201,6 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
|
||||||
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(searchQuery)) {
|
if (!TextUtils.isEmpty(searchQuery)) {
|
||||||
if (wasLoading.getAndSet(false)) {
|
if (wasLoading.getAndSet(false)) {
|
||||||
if (currentNextPage > currentPage) loadMoreItems();
|
if (currentNextPage > currentPage) loadMoreItems();
|
||||||
@ -180,7 +215,16 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
|
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) {
|
||||||
|
showKeyboardSearch();
|
||||||
|
showSuggestionsPanel();
|
||||||
|
} else {
|
||||||
|
hideKeyboardSearch();
|
||||||
|
hideSuggestionsPanel();
|
||||||
|
}
|
||||||
|
wasSearchFocused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -193,10 +237,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState);
|
|
||||||
|
|
||||||
if (searchDisposable != null) searchDisposable.dispose();
|
if (searchDisposable != null) searchDisposable.dispose();
|
||||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) disposables.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +246,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||||
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
|
if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) {
|
||||||
search(searchQuery);
|
search(searchQuery);
|
||||||
} else Log.e(TAG, "ReCaptcha failed");
|
} else Log.e(TAG, "ReCaptcha failed");
|
||||||
break;
|
break;
|
||||||
@ -215,6 +257,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Init
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
|
||||||
|
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
|
||||||
|
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
|
||||||
|
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
|
||||||
|
|
||||||
|
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
||||||
|
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
||||||
|
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// State Saving
|
// State Saving
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -235,8 +294,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle bundle) {
|
public void onSaveInstanceState(Bundle bundle) {
|
||||||
searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
|
searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery;
|
||||||
? searchEditText.getText().toString() : searchQuery;
|
|
||||||
super.onSaveInstanceState(bundle);
|
super.onSaveInstanceState(bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +309,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
} else {
|
} else {
|
||||||
if (searchEditText != null) {
|
if (searchEditText != null) {
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
showSoftKeyboard(searchEditText);
|
showKeyboardSearch();
|
||||||
}
|
}
|
||||||
animateView(errorPanelRoot, false, 200);
|
animateView(errorPanelRoot, false, 200);
|
||||||
}
|
}
|
||||||
@ -272,12 +330,6 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
}
|
}
|
||||||
|
|
||||||
inflater.inflate(R.menu.menu_search, menu);
|
inflater.inflate(R.menu.menu_search, menu);
|
||||||
|
|
||||||
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
|
|
||||||
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
|
|
||||||
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
|
|
||||||
setupSearchView();
|
|
||||||
|
|
||||||
restoreFilterChecked(menu, filterItemCheckedId);
|
restoreFilterChecked(menu, filterItemCheckedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,14 +359,13 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
|
|
||||||
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
|
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
|
||||||
switch (itemId) {
|
switch (itemId) {
|
||||||
case R.id.menu_filter_all:
|
|
||||||
return SearchEngine.Filter.ANY;
|
|
||||||
case R.id.menu_filter_video:
|
case R.id.menu_filter_video:
|
||||||
return SearchEngine.Filter.STREAM;
|
return SearchEngine.Filter.STREAM;
|
||||||
case R.id.menu_filter_channel:
|
case R.id.menu_filter_channel:
|
||||||
return SearchEngine.Filter.CHANNEL;
|
return SearchEngine.Filter.CHANNEL;
|
||||||
case R.id.menu_filter_playlist:
|
case R.id.menu_filter_playlist:
|
||||||
return SearchEngine.Filter.PLAYLIST;
|
return SearchEngine.Filter.PLAYLIST;
|
||||||
|
case R.id.menu_filter_all:
|
||||||
default:
|
default:
|
||||||
return SearchEngine.Filter.ANY;
|
return SearchEngine.Filter.ANY;
|
||||||
}
|
}
|
||||||
@ -326,9 +377,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
|
|
||||||
private TextWatcher textWatcher;
|
private TextWatcher textWatcher;
|
||||||
|
|
||||||
private void setupSearchView() {
|
private void showSearchOnStart() {
|
||||||
searchEditText.setText(searchQuery != null ? searchQuery : "");
|
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery);
|
||||||
searchEditText.setAdapter(suggestionListAdapter);
|
searchEditText.setText(searchQuery);
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
searchToolbarContainer.setTranslationX(100);
|
searchToolbarContainer.setTranslationX(100);
|
||||||
@ -340,15 +391,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
searchToolbarContainer.setAlpha(1f);
|
searchToolbarContainer.setAlpha(1f);
|
||||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
initSearchListeners();
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText);
|
|
||||||
else hideSoftKeyboard(searchEditText);
|
|
||||||
wasSearchFocused = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSearchListeners() {
|
private void initSearchListeners() {
|
||||||
|
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
|
||||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
@ -358,11 +404,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
searchEditText.setText("");
|
||||||
searchEditText.setText("", false);
|
suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
|
||||||
} else searchEditText.setText("");
|
showKeyboardSearch();
|
||||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
|
||||||
showSoftKeyboard(searchEditText);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -372,7 +416,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||||
searchEditText.showDropDown();
|
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||||
|
showSuggestionsPanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -380,22 +426,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
public void onFocusChange(View v, boolean hasFocus) {
|
public void onFocusChange(View v, boolean hasFocus) {
|
||||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||||
if (hasFocus) searchEditText.showDropDown();
|
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||||
|
showSuggestionsPanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onSuggestionItemSelected(SuggestionItem item) {
|
||||||
if (DEBUG) {
|
search(item.query);
|
||||||
Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
searchEditText.setText(item.query);
|
||||||
}
|
}
|
||||||
String s = suggestionListAdapter.getSuggestion(position);
|
|
||||||
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
|
@Override
|
||||||
submitQuery(s);
|
public void onSuggestionItemLongClick(SuggestionItem item) {
|
||||||
|
if (item.fromHistory) showDeleteSuggestionDialog(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
searchEditText.setThreshold(THRESHOLD_SUGGESTION);
|
|
||||||
|
|
||||||
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
|
||||||
textWatcher = new TextWatcher() {
|
textWatcher = new TextWatcher() {
|
||||||
@ -410,32 +458,32 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s) {
|
public void afterTextChanged(Editable s) {
|
||||||
String newText = searchEditText.getText().toString();
|
String newText = searchEditText.getText().toString();
|
||||||
if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText);
|
suggestionPublisher.onNext(newText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
searchEditText.addTextChangedListener(textWatcher);
|
searchEditText.addTextChangedListener(textWatcher);
|
||||||
|
|
||||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||||
if (DEBUG)
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||||
|
}
|
||||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||||
submitQuery(searchEditText.getText().toString());
|
search(searchEditText.getText().toString());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
|
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unsetSearchListeners() {
|
private void unsetSearchListeners() {
|
||||||
|
if (DEBUG) Log.d(TAG, "unsetSearchListeners() called");
|
||||||
searchClear.setOnClickListener(null);
|
searchClear.setOnClickListener(null);
|
||||||
searchClear.setOnLongClickListener(null);
|
searchClear.setOnLongClickListener(null);
|
||||||
searchEditText.setOnClickListener(null);
|
searchEditText.setOnClickListener(null);
|
||||||
searchEditText.setOnItemClickListener(null);
|
|
||||||
searchEditText.setOnFocusChangeListener(null);
|
searchEditText.setOnFocusChangeListener(null);
|
||||||
searchEditText.setOnEditorActionListener(null);
|
searchEditText.setOnEditorActionListener(null);
|
||||||
|
|
||||||
@ -443,68 +491,166 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
textWatcher = null;
|
textWatcher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showSoftKeyboard(View view) {
|
private void showSuggestionsPanel() {
|
||||||
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
|
if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called");
|
||||||
if (view == null) return;
|
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
||||||
|
}
|
||||||
|
|
||||||
if (view.requestFocus()) {
|
private void hideSuggestionsPanel() {
|
||||||
|
if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called");
|
||||||
|
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showKeyboardSearch() {
|
||||||
|
if (DEBUG) Log.d(TAG, "showKeyboardSearch() called");
|
||||||
|
if (searchEditText == null) return;
|
||||||
|
|
||||||
|
if (searchEditText.requestFocus()) {
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSoftKeyboard(View view) {
|
private void hideKeyboardSearch() {
|
||||||
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
|
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
|
||||||
if (view == null) return;
|
if (searchEditText == null) return;
|
||||||
|
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||||
|
|
||||||
view.clearFocus();
|
searchEditText.clearFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void giveSearchEditTextFocus() {
|
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||||
showSoftKeyboard(searchEditText);
|
new AlertDialog.Builder(activity)
|
||||||
}
|
.setTitle(item.query)
|
||||||
|
.setMessage(R.string.delete_item_search_history)
|
||||||
private void initSuggestionObserver() {
|
.setCancelable(true)
|
||||||
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
|
.setNegativeButton(R.string.cancel, null)
|
||||||
final Predicate<String> checkEnabledAndLength = new Predicate<String>() {
|
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION;
|
disposables.add(Observable
|
||||||
// Clear the suggestions adapter if the length check fails
|
.fromCallable(new Callable<Integer>() {
|
||||||
if (!lengthCheck && !suggestionListAdapter.isEmpty()) {
|
|
||||||
suggestionListAdapter.updateAdapter(new ArrayList<String>());
|
|
||||||
}
|
|
||||||
// Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION
|
|
||||||
return showSuggestions && lengthCheck;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
suggestionWorkerDisposable = suggestionPublisher
|
|
||||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
|
||||||
.startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "")
|
|
||||||
.filter(checkEnabledAndLength)
|
|
||||||
.switchMap(new Function<String, Observable<Notification<List<String>>>>() {
|
|
||||||
@Override
|
@Override
|
||||||
public Observable<Notification<List<String>>> apply(@io.reactivex.annotations.NonNull String query) throws Exception {
|
public Integer call() throws Exception {
|
||||||
return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize();
|
return searchHistoryDAO.deleteAllWhereQuery(item.query);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Notification<List<String>>>() {
|
.subscribe(new Consumer<Integer>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> listNotification) throws Exception {
|
public void accept(Integer howManyDeleted) throws Exception {
|
||||||
|
suggestionPublisher.onNext(searchEditText.getText().toString());
|
||||||
|
}
|
||||||
|
}, new Consumer<Throwable>() {
|
||||||
|
@Override
|
||||||
|
public void accept(Throwable throwable) throws Exception {
|
||||||
|
showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onBackPressed() {
|
||||||
|
if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) {
|
||||||
|
hideSuggestionsPanel();
|
||||||
|
hideKeyboardSearch();
|
||||||
|
searchEditText.setText(lastSearchedQuery);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void giveSearchEditTextFocus() {
|
||||||
|
showKeyboardSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSuggestionObserver() {
|
||||||
|
if (DEBUG) Log.d(TAG, "initSuggestionObserver() called");
|
||||||
|
if (suggestionDisposable != null) suggestionDisposable.dispose();
|
||||||
|
|
||||||
|
final Observable<String> observable = suggestionPublisher
|
||||||
|
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||||
|
.startWith(searchQuery != null ? searchQuery : "")
|
||||||
|
.filter(new Predicate<String>() {
|
||||||
|
@Override
|
||||||
|
public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception {
|
||||||
|
return isSuggestionsEnabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
suggestionDisposable = observable
|
||||||
|
.switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() {
|
||||||
|
@Override
|
||||||
|
public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception {
|
||||||
|
final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0
|
||||||
|
? searchHistoryDAO.getSimilarEntries(query, 3)
|
||||||
|
: searchHistoryDAO.getUniqueEntries(25);
|
||||||
|
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||||
|
.map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() {
|
||||||
|
@Override
|
||||||
|
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SearchHistoryEntry> searchHistoryEntries) throws Exception {
|
||||||
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
|
for (SearchHistoryEntry entry : searchHistoryEntries)
|
||||||
|
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||||
|
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
|
||||||
|
return local.materialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Observable<List<SuggestionItem>> network = ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable()
|
||||||
|
.map(new Function<List<String>, List<SuggestionItem>>() {
|
||||||
|
@Override
|
||||||
|
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<String> strings) throws Exception {
|
||||||
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
|
for (String entry : strings) result.add(new SuggestionItem(false, entry));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Observable.zip(local, network, new BiFunction<List<SuggestionItem>, List<SuggestionItem>, List<SuggestionItem>>() {
|
||||||
|
@Override
|
||||||
|
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SuggestionItem> localResult, @io.reactivex.annotations.NonNull List<SuggestionItem> networkResult) throws Exception {
|
||||||
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
|
if (localResult.size() > 0) result.addAll(localResult);
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
final Iterator<SuggestionItem> iterator = networkResult.iterator();
|
||||||
|
while (iterator.hasNext() && localResult.size() > 0) {
|
||||||
|
final SuggestionItem next = iterator.next();
|
||||||
|
for (SuggestionItem item : localResult) {
|
||||||
|
if (item.query.equals(next.query)) {
|
||||||
|
iterator.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkResult.size() > 0) result.addAll(networkResult);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}).materialize();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(new Consumer<Notification<List<SuggestionItem>>>() {
|
||||||
|
@Override
|
||||||
|
public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> listNotification) throws Exception {
|
||||||
if (listNotification.isOnNext()) {
|
if (listNotification.isOnNext()) {
|
||||||
handleSuggestions(listNotification.getValue());
|
handleSuggestions(listNotification.getValue());
|
||||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
} else if (listNotification.isOnError()) {
|
} else if (listNotification.isOnError()) {
|
||||||
Throwable error = listNotification.getError();
|
Throwable error = listNotification.getError();
|
||||||
if (!ExtractorHelper.isInterruptedCaused(error)) {
|
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
|
||||||
|
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) {
|
||||||
onSuggestionError(error);
|
onSuggestionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -519,6 +665,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
|
|
||||||
private void search(final String query) {
|
private void search(final String query) {
|
||||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
|
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
|
||||||
|
if (query.isEmpty()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final StreamingService service = NewPipe.getServiceByUrl(query);
|
final StreamingService service = NewPipe.getServiceByUrl(query);
|
||||||
@ -543,7 +690,6 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
public void accept(Throwable throwable) throws Exception {
|
public void accept(Throwable throwable) throws Exception {
|
||||||
showError(getString(R.string.url_not_supported_toast), false);
|
showError(getString(R.string.url_not_supported_toast), false);
|
||||||
hideLoading();
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
@ -552,18 +698,18 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
// Exception occurred, it's not a url
|
// Exception occurred, it's not a url
|
||||||
}
|
}
|
||||||
|
|
||||||
hideSoftKeyboard(searchEditText);
|
lastSearchedQuery = query;
|
||||||
this.searchQuery = query;
|
searchQuery = query;
|
||||||
this.currentPage = 0;
|
currentPage = 0;
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
|
hideSuggestionsPanel();
|
||||||
|
hideKeyboardSearch();
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
if (activity instanceof HistoryListener) {
|
||||||
((HistoryListener) activity).onSearch(serviceId, query);
|
((HistoryListener) activity).onSearch(serviceId, query);
|
||||||
|
suggestionPublisher.onNext(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
|
||||||
final String searchLanguageKey = getContext().getString(R.string.search_language_key);
|
|
||||||
searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
|
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +769,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
@Override
|
@Override
|
||||||
protected void onItemSelected(InfoItem selectedItem) {
|
protected void onItemSelected(InfoItem selectedItem) {
|
||||||
super.onItemSelected(selectedItem);
|
super.onItemSelected(selectedItem);
|
||||||
hideSoftKeyboard(searchEditText);
|
hideKeyboardSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -634,13 +780,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
this.filterItemCheckedId = item.getItemId();
|
this.filterItemCheckedId = item.getItemId();
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void submitQuery(String query) {
|
if (!TextUtils.isEmpty(searchQuery)) {
|
||||||
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
|
search(searchQuery);
|
||||||
if (query.isEmpty()) return;
|
}
|
||||||
search(query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setQuery(int serviceId, String searchQuery) {
|
private void setQuery(int serviceId, String searchQuery) {
|
||||||
@ -648,19 +791,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
this.searchQuery = searchQuery;
|
this.searchQuery = searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showError(String message, boolean showRetryButton) {
|
|
||||||
super.showError(message, showRetryButton);
|
|
||||||
hideSoftKeyboard(searchEditText);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Suggestion Results
|
// Suggestion Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void handleSuggestions(@NonNull List<String> suggestions) {
|
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
||||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||||
suggestionListAdapter.updateAdapter(suggestions);
|
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||||
|
suggestionsRecyclerView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
suggestionListAdapter.setItems(suggestions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSuggestionError(Throwable exception) {
|
public void onSuggestionError(Throwable exception) {
|
||||||
@ -681,6 +828,13 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showError(String message, boolean showRetryButton) {
|
||||||
|
super.showError(message, showRetryButton);
|
||||||
|
hideSuggestionsPanel();
|
||||||
|
hideKeyboardSearch();
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Search Results
|
// Search Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -691,6 +845,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
|||||||
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
|
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastSearchedQuery = searchQuery;
|
||||||
|
|
||||||
if (infoListAdapter.getItemsList().size() == 0) {
|
if (infoListAdapter.getItemsList().size() == 0) {
|
||||||
if (result.resultList.size() > 0) {
|
if (result.resultList.size() > 0) {
|
||||||
infoListAdapter.addInfoItemList(result.resultList);
|
infoListAdapter.addInfoItemList(result.resultList);
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
|
public class SuggestionItem {
|
||||||
|
public final boolean fromHistory;
|
||||||
|
public final String query;
|
||||||
|
|
||||||
|
public SuggestionItem(boolean fromHistory, String query) {
|
||||||
|
this.fromHistory = fromHistory;
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + fromHistory + "→" + query + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +1,108 @@
|
|||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.content.res.TypedArray;
|
||||||
import android.database.MatrixCursor;
|
import android.support.annotation.AttrRes;
|
||||||
import android.support.v4.widget.ResourceCursorAdapter;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||||
* Created by Christian Schabesberger on 02.08.16.
|
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||||
*
|
private final Context context;
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
private OnSuggestionItemSelected listener;
|
||||||
* SuggestionListAdapter.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link ResourceCursorAdapter} to display suggestions.
|
|
||||||
*/
|
|
||||||
public class SuggestionListAdapter extends ResourceCursorAdapter {
|
|
||||||
|
|
||||||
private static final String[] columns = new String[]{"_id", "title"};
|
|
||||||
private static final int INDEX_ID = 0;
|
|
||||||
private static final int INDEX_TITLE = 1;
|
|
||||||
|
|
||||||
|
public interface OnSuggestionItemSelected {
|
||||||
|
void onSuggestionItemSelected(SuggestionItem item);
|
||||||
|
void onSuggestionItemLongClick(SuggestionItem item);
|
||||||
|
}
|
||||||
|
|
||||||
public SuggestionListAdapter(Context context) {
|
public SuggestionListAdapter(Context context) {
|
||||||
super(context, android.R.layout.simple_list_item_1, null, 0);
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<SuggestionItem> items) {
|
||||||
|
this.items.clear();
|
||||||
|
this.items.addAll(items);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(OnSuggestionItemSelected listener) {
|
||||||
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
ViewHolder viewHolder = new ViewHolder(view);
|
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
|
||||||
viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the suggestion list
|
|
||||||
* @param suggestions the list of suggestions
|
|
||||||
*/
|
|
||||||
public void updateAdapter(List<String> suggestions) {
|
|
||||||
MatrixCursor cursor = new MatrixCursor(columns, suggestions.size());
|
|
||||||
int i = 0;
|
|
||||||
for (String suggestion : suggestions) {
|
|
||||||
String[] columnValues = new String[columns.length];
|
|
||||||
columnValues[INDEX_TITLE] = suggestion;
|
|
||||||
columnValues[INDEX_ID] = Integer.toString(i);
|
|
||||||
cursor.addRow(columnValues);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
changeCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the suggestion for a position
|
|
||||||
* @param position the position of the suggestion
|
|
||||||
* @return the suggestion
|
|
||||||
*/
|
|
||||||
public String getSuggestion(int position) {
|
|
||||||
return ((Cursor) getItem(position)).getString(INDEX_TITLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence convertToString(Cursor cursor) {
|
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
|
||||||
return cursor.getString(INDEX_TITLE);
|
final SuggestionItem currentItem = getItem(position);
|
||||||
|
holder.updateFrom(currentItem);
|
||||||
|
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View v) {
|
||||||
|
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ViewHolder {
|
private SuggestionItem getItem(int position) {
|
||||||
private final TextView suggestionTitle;
|
return items.get(position);
|
||||||
private ViewHolder(View view) {
|
}
|
||||||
this.suggestionTitle = view.findViewById(android.R.id.text1);
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return getItemCount() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||||
|
private final TextView itemSuggestionQuery;
|
||||||
|
private final ImageView suggestionIcon;
|
||||||
|
|
||||||
|
// Cache some ids, as they can potentially be constantly updated/recycled
|
||||||
|
private final int historyResId;
|
||||||
|
private final int searchResId;
|
||||||
|
|
||||||
|
private SuggestionItemHolder(View rootView) {
|
||||||
|
super(rootView);
|
||||||
|
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
||||||
|
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
||||||
|
|
||||||
|
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
|
||||||
|
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFrom(SuggestionItem item) {
|
||||||
|
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||||
|
itemSuggestionQuery.setText(item.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
|
||||||
|
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||||
|
int attributeResourceId = a.getResourceId(0, 0);
|
||||||
|
a.recycle();
|
||||||
|
return attributeResourceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,7 +19,7 @@ public class AnimationUtils {
|
|||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA
|
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void animateView(View view, boolean enterOrExit, long duration) {
|
public static void animateView(View view, boolean enterOrExit, long duration) {
|
||||||
@ -95,9 +95,16 @@ public class AnimationUtils {
|
|||||||
case LIGHT_SCALE_AND_ALPHA:
|
case LIGHT_SCALE_AND_ALPHA:
|
||||||
animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
||||||
break;
|
break;
|
||||||
|
case SLIDE_AND_ALPHA:
|
||||||
|
animateSlideAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
||||||
|
break;
|
||||||
|
case LIGHT_SLIDE_AND_ALPHA:
|
||||||
|
animateLightSlideAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animate the background color of a view
|
* Animate the background color of a view
|
||||||
*/
|
*/
|
||||||
@ -237,4 +244,50 @@ public class AnimationUtils {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void animateSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
|
||||||
|
if (enterOrExit) {
|
||||||
|
view.setTranslationY(-view.getHeight());
|
||||||
|
view.setAlpha(0f);
|
||||||
|
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0)
|
||||||
|
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
if (execOnEnd != null) execOnEnd.run();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight())
|
||||||
|
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
view.setVisibility(View.GONE);
|
||||||
|
if (execOnEnd != null) execOnEnd.run();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void animateLightSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
|
||||||
|
if (enterOrExit) {
|
||||||
|
view.setTranslationY(-view.getHeight() / 2);
|
||||||
|
view.setAlpha(0f);
|
||||||
|
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0)
|
||||||
|
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
if (execOnEnd != null) execOnEnd.run();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight() / 2)
|
||||||
|
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
view.setVisibility(View.GONE);
|
||||||
|
if (execOnEnd != null) execOnEnd.run();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.PointF;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.LinearSmoothScroller;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
public class LayoutManagerSmoothScroller extends LinearLayoutManager {
|
||||||
|
|
||||||
|
public LayoutManagerSmoothScroller(Context context) {
|
||||||
|
super(context, VERTICAL, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutManagerSmoothScroller(Context context, int orientation, boolean reverseLayout) {
|
||||||
|
super(context, orientation, reverseLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
|
||||||
|
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
|
||||||
|
smoothScroller.setTargetPosition(position);
|
||||||
|
startSmoothScroll(smoothScroller);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
|
||||||
|
public TopSnappedSmoothScroller(Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||||
|
return LayoutManagerSmoothScroller.this
|
||||||
|
.computeScrollVectorForPosition(targetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getVerticalSnapPreference() {
|
||||||
|
return SNAP_TO_START;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,25 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/suggestions_panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:background="@android:color/transparent"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/suggestions_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
tools:listitem="@layout/item_search_suggestion"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!--ERROR PANEL-->
|
<!--ERROR PANEL-->
|
||||||
<include
|
<include
|
||||||
android:id="@+id/error_panel"
|
android:id="@+id/error_panel"
|
||||||
|
36
app/src/main/res/layout/item_search_suggestion.xml
Normal file
36
app/src/main/res/layout/item_search_suggestion.xml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:paddingTop="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/item_suggestion_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
tools:ignore="ContentDescription,RtlHardcoded"
|
||||||
|
tools:src="?attr/history"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_suggestion_query"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="Search query"/>
|
||||||
|
</LinearLayout>
|
@ -4,16 +4,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary">
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true">
|
|
||||||
|
|
||||||
<View
|
<EditText
|
||||||
android:id="@+id/dropdown_anchor"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
|
||||||
android:id="@+id/toolbar_search_edit_text"
|
android:id="@+id/toolbar_search_edit_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -24,7 +17,6 @@
|
|||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:drawableLeft="?attr/search"
|
android:drawableLeft="?attr/search"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
android:dropDownAnchor="@+id/dropdown_anchor"
|
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:hint="@string/search"
|
android:hint="@string/search"
|
||||||
|
@ -265,4 +265,5 @@
|
|||||||
<string name="history_empty">The history is empty</string>
|
<string name="history_empty">The history is empty</string>
|
||||||
<string name="history_cleared">History cleared</string>
|
<string name="history_cleared">History cleared</string>
|
||||||
<string name="item_deleted">Item deleted</string>
|
<string name="item_deleted">Item deleted</string>
|
||||||
|
<string name="delete_item_search_history">Do you want to delete this item from search history?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user