From 78cbfa20d979f2d47e82c17b8a830ef62f34c134 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Thu, 28 Sep 2017 10:06:48 -0300 Subject: [PATCH] Improve search - Use a list instead of a popup - Show search history entries --- app/build.gradle | 2 +- .../java/org/schabi/newpipe/Downloader.java | 6 +- .../history/dao/SearchHistoryDAO.java | 10 + .../fragments/list/search/SearchFragment.java | 414 ++++++++++++------ .../fragments/list/search/SuggestionItem.java | 16 + .../list/search/SuggestionListAdapter.java | 155 ++++--- .../schabi/newpipe/util/AnimationUtils.java | 55 ++- .../util/LayoutManagerSmoothScroller.java | 43 ++ app/src/main/res/layout/fragment_search.xml | 19 + .../res/layout/item_search_suggestion.xml | 36 ++ .../main/res/layout/toolbar_search_layout.xml | 12 +- app/src/main/res/values/strings.xml | 1 + 12 files changed, 555 insertions(+), 214 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java create mode 100644 app/src/main/res/layout/item_search_suggestion.xml diff --git a/app/build.gradle b/app/build.gradle index bad561ae5..2f1dd4005 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ dependencies { exclude module: 'support-annotations' } - compile 'com.github.TeamNewPipe:NewPipeExtractor:7ae274b' + compile 'com.github.TeamNewPipe:NewPipeExtractor:1df3f67' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index dede9617e..77f12fa46 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -12,7 +12,6 @@ import java.io.InterruptedIOException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; 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())); - for (Map.Entry> entry : con.getHeaderFields().entrySet()) { - System.err.println(entry.getKey() + ": " + entry.getValue()); - } - String inputLine; + String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java index 921ce63a1..70799d971 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java @@ -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.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.TABLE_NAME; @@ -27,11 +28,20 @@ public interface SearchHistoryDAO extends HistoryDAO { @Override int deleteAll(); + @Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query") + int deleteAllWhereQuery(String query); + @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) @Override Flowable> getAll(); + @Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit") + Flowable> getUniqueEntries(int limit); + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) @Override Flowable> listByService(int serviceId); + + @Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit") + Flowable> getSimilarEntries(String query, int limit); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 93ac00207..2903d8a73 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.search; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.text.Editable; import android.text.TextUtils; @@ -25,12 +27,14 @@ import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AutoCompleteTextView; +import android.widget.EditText; import android.widget.TextView; +import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; 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.ListExtractor; 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.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.history.HistoryListener; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.util.LayoutManagerSmoothScroller; 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.Iterator; import java.util.List; import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import icepick.State; +import io.reactivex.Flowable; import io.reactivex.Notification; import io.reactivex.Observable; +import io.reactivex.ObservableSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; @@ -65,21 +78,22 @@ import io.reactivex.subjects.PublishSubject; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SearchFragment extends BaseListFragment { +public class SearchFragment extends BaseListFragment implements BackPressable { /*////////////////////////////////////////////////////////////////////////// // 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. */ - private static final int SUGGESTIONS_DEBOUNCE = 150; //ms + private static final int SUGGESTIONS_DEBOUNCE = 120; //ms @State protected int filterItemCheckedId = -1; @@ -88,47 +102,54 @@ public class SearchFragment extends BaseListFragment suggestionPublisher = PublishSubject.create(); private Disposable searchDisposable; - private Disposable suggestionWorkerDisposable; + private Disposable suggestionDisposable; private CompositeDisposable disposables = new CompositeDisposable(); private SuggestionListAdapter suggestionListAdapter; + private SearchHistoryDAO searchHistoryDAO; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ private View searchToolbarContainer; - private AutoCompleteTextView searchEditText; + private EditText searchEditText; private View searchClear; + private View suggestionsPanel; + private RecyclerView suggestionsRecyclerView; + /*////////////////////////////////////////////////////////////////////////*/ public static SearchFragment getInstance(int serviceId, String query) { SearchFragment searchFragment = new SearchFragment(); searchFragment.setQuery(serviceId, query); - searchFragment.searchOnResume(); + + if (!TextUtils.isEmpty(query)) { + searchFragment.setSearchOnResume(); + } + return searchFragment; } /** * 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() { - if (!TextUtils.isEmpty(searchQuery)) { - wasLoading.set(true); - } + private void setSearchOnResume() { + wasLoading.set(true); } /*////////////////////////////////////////////////////////////////////////// @@ -139,6 +160,16 @@ public class SearchFragment extends BaseListFragment currentPage) loadMoreItems(); @@ -180,7 +215,16 @@ public class SearchFragment extends BaseListFragment= Build.VERSION_CODES.JELLY_BEAN_MR1) { - searchEditText.setText("", false); - } else searchEditText.setText(""); - suggestionListAdapter.updateAdapter(new ArrayList()); - showSoftKeyboard(searchEditText); + searchEditText.setText(""); + suggestionListAdapter.setItems(new ArrayList()); + showKeyboardSearch(); } }); @@ -372,7 +416,9 @@ public class SearchFragment extends BaseListFragment parent, View view, int position, long id) { - if (DEBUG) { - Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); - } - String s = suggestionListAdapter.getSuggestion(position); - if (DEBUG) Log.d(TAG, "onItemClick text = " + s); - submitQuery(s); + public void onSuggestionItemSelected(SuggestionItem item) { + search(item.query); + searchEditText.setText(item.query); + } + + @Override + public void onSuggestionItemLongClick(SuggestionItem item) { + if (item.fromHistory) showDeleteSuggestionDialog(item); } }); - searchEditText.setThreshold(THRESHOLD_SUGGESTION); if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher); textWatcher = new TextWatcher() { @@ -410,32 +458,32 @@ public class SearchFragment extends BaseListFragment() { + @Override + public Integer call() throws Exception { + return searchHistoryDAO.deleteAllWhereQuery(item.query); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + @Override + public void accept(Integer howManyDeleted) throws Exception { + suggestionPublisher.onNext(searchEditText.getText().toString()); + } + }, new Consumer() { + @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() { - showSoftKeyboard(searchEditText); + showKeyboardSearch(); } private void initSuggestionObserver() { - if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose(); - final Predicate checkEnabledAndLength = new Predicate() { - @Override - public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception { - boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION; - // Clear the suggestions adapter if the length check fails - if (!lengthCheck && !suggestionListAdapter.isEmpty()) { - suggestionListAdapter.updateAdapter(new ArrayList()); - } - // Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION - return showSuggestions && lengthCheck; - } - }; + if (DEBUG) Log.d(TAG, "initSuggestionObserver() called"); + if (suggestionDisposable != null) suggestionDisposable.dispose(); - suggestionWorkerDisposable = suggestionPublisher + final Observable observable = suggestionPublisher .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) - .startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "") - .filter(checkEnabledAndLength) - .switchMap(new Function>>>() { + .startWith(searchQuery != null ? searchQuery : "") + .filter(new Predicate() { @Override - public Observable>> apply(@io.reactivex.annotations.NonNull String query) throws Exception { - return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize(); + public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception { + return isSuggestionsEnabled; + } + }); + + suggestionDisposable = observable + .switchMap(new Function>>>() { + @Override + public ObservableSource>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception { + final Flowable> flowable = query.length() > 0 + ? searchHistoryDAO.getSimilarEntries(query, 3) + : searchHistoryDAO.getUniqueEntries(25); + final Observable> local = flowable.toObservable() + .map(new Function, List>() { + @Override + public List apply(@io.reactivex.annotations.NonNull List searchHistoryEntries) throws Exception { + List 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> network = ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable() + .map(new Function, List>() { + @Override + public List apply(@io.reactivex.annotations.NonNull List strings) throws Exception { + List result = new ArrayList<>(); + for (String entry : strings) result.add(new SuggestionItem(false, entry)); + return result; + } + }); + + return Observable.zip(local, network, new BiFunction, List, List>() { + @Override + public List apply(@io.reactivex.annotations.NonNull List localResult, @io.reactivex.annotations.NonNull List networkResult) throws Exception { + List result = new ArrayList<>(); + if (localResult.size() > 0) result.addAll(localResult); + + // Remove duplicates + final Iterator 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>>() { + .subscribe(new Consumer>>() { @Override - public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { + public void accept(@io.reactivex.annotations.NonNull Notification> listNotification) throws Exception { if (listNotification.isOnNext()) { handleSuggestions(listNotification.getValue()); - if (errorPanelRoot.getVisibility() == View.VISIBLE) { - hideLoading(); - } } else if (listNotification.isOnError()) { Throwable error = listNotification.getError(); - if (!ExtractorHelper.isInterruptedCaused(error)) { + if (!ExtractorHelper.hasAssignableCauseThrowable(error, + IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) { onSuggestionError(error); } } @@ -519,6 +665,7 @@ public class SearchFragment extends BaseListFragment suggestions) { + public void handleSuggestions(@NonNull final List 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) { @@ -681,6 +828,13 @@ public class SearchFragment extends BaseListFragment 0) { infoListAdapter.addInfoItemList(result.resultList); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java new file mode 100644 index 000000000..722638926 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java @@ -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 + "]"; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 0a7e3d613..71d9bf780 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -1,89 +1,108 @@ package org.schabi.newpipe.fragments.list.search; import android.content.Context; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.support.v4.widget.ResourceCursorAdapter; +import android.content.res.TypedArray; +import android.support.annotation.AttrRes; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import org.schabi.newpipe.R; + +import java.util.ArrayList; import java.util.List; -/* - * Created by Christian Schabesberger on 02.08.16. - * - * Copyright (C) Christian Schabesberger 2016 - * 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 . - */ - -/** - * {@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 class SuggestionListAdapter extends RecyclerView.Adapter { + private final ArrayList items = new ArrayList<>(); + private final Context context; + private OnSuggestionItemSelected listener; + public interface OnSuggestionItemSelected { + void onSuggestionItemSelected(SuggestionItem item); + void onSuggestionItemLongClick(SuggestionItem item); + } public SuggestionListAdapter(Context context) { - super(context, android.R.layout.simple_list_item_1, null, 0); + this.context = context; + } + + public void setItems(List items) { + this.items.clear(); + this.items.addAll(items); + notifyDataSetChanged(); + } + + public void setListener(OnSuggestionItemSelected listener) { + this.listener = listener; } @Override - public void bindView(View view, Context context, Cursor cursor) { - ViewHolder viewHolder = new ViewHolder(view); - viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE)); - } - - /** - * Update the suggestion list - * @param suggestions the list of suggestions - */ - public void updateAdapter(List 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); + public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false)); } @Override - public CharSequence convertToString(Cursor cursor) { - return cursor.getString(INDEX_TITLE); + public void onBindViewHolder(SuggestionItemHolder holder, int position) { + 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 final TextView suggestionTitle; - private ViewHolder(View view) { - this.suggestionTitle = view.findViewById(android.R.id.text1); + private SuggestionItem getItem(int position) { + return items.get(position); + } + + @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; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index ac70bd05f..c954211fa 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -19,7 +19,7 @@ public class AnimationUtils { private static final boolean DEBUG = MainActivity.DEBUG; 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) { @@ -95,9 +95,16 @@ public class AnimationUtils { case LIGHT_SCALE_AND_ALPHA: animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd); 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 */ @@ -237,4 +244,50 @@ public class AnimationUtils { }).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(); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java new file mode 100644 index 000000000..9eca2d610 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java @@ -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; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 9ba44e76f..04b10347c 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -51,6 +51,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_search_layout.xml b/app/src/main/res/layout/toolbar_search_layout.xml index 7780a0226..797eea48e 100644 --- a/app/src/main/res/layout/toolbar_search_layout.xml +++ b/app/src/main/res/layout/toolbar_search_layout.xml @@ -4,16 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" - android:focusable="true" - android:focusableInTouchMode="true"> + android:background="?attr/colorPrimary"> - - - The history is empty History cleared Item deleted + Do you want to delete this item from search history?