From 8363c981a3e5273923d19eb77076d78384ae3354 Mon Sep 17 00:00:00 2001 From: Floens Date: Wed, 22 Apr 2015 23:58:31 +0200 Subject: [PATCH] Add searching --- .../chan/controller/NavigationController.java | 21 +++ .../chan/core/presenter/ThreadPresenter.java | 87 +++++++-- .../floens/chan/ui/adapter/PostAdapter.java | 164 ++++------------ .../floens/chan/ui/cell/ThreadStatusCell.java | 2 + .../chan/ui/controller/BrowseController.java | 4 +- .../ui/controller/PostRepliesController.java | 2 +- .../controller/RootNavigationController.java | 25 +++ .../chan/ui/controller/ThreadController.java | 14 +- .../ui/controller/ViewThreadController.java | 4 +- .../chan/ui/fragment/ThreadFragment.java | 5 - .../chan/ui/helper/PostPopupHelper.java | 2 +- .../floens/chan/ui/layout/ThreadLayout.java | 25 ++- .../chan/ui/layout/ThreadListLayout.java | 55 +++++- .../org/floens/chan/ui/toolbar/Toolbar.java | 175 ++++++++++++++++-- .../org/floens/chan/ui/view/LoadView.java | 26 ++- .../org/floens/chan/utils/AndroidUtils.java | 8 +- .../res/drawable-hdpi/ic_close_white_24dp.png | Bin 0 -> 324 bytes .../res/drawable-mdpi/ic_close_white_24dp.png | Bin 0 -> 279 bytes .../drawable-xhdpi/ic_close_white_24dp.png | Bin 0 -> 402 bytes .../drawable-xxhdpi/ic_close_white_24dp.png | Bin 0 -> 492 bytes .../drawable-xxxhdpi/ic_close_white_24dp.png | Bin 0 -> 662 bytes .../main/res/layout/layout_thread_list.xml | 16 +- Clover/app/src/main/res/values/strings.xml | 11 +- 23 files changed, 441 insertions(+), 205 deletions(-) create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png diff --git a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java index 634d62c6..641f7f14 100644 --- a/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java @@ -160,6 +160,10 @@ public abstract class NavigationController extends Controller implements Control public boolean onBack() { if (blockingInput) return true; + if (toolbar.closeSearch()) { + return true; + } + if (controllerList.size() > 0) { Controller top = controllerList.get(controllerList.size() - 1); if (top.onBack()) { @@ -190,6 +194,10 @@ public abstract class NavigationController extends Controller implements Control } + public void showSearch() { + toolbar.showSearch(); + } + @Override public void onMenuOrBackClicked(boolean isArrow) { if (isArrow) { @@ -198,4 +206,17 @@ public abstract class NavigationController extends Controller implements Control onMenuClicked(); } } + + @Override + public void onSearchVisibilityChanged(boolean visible) { + } + + @Override + public String getSearchHint() { + return ""; + } + + @Override + public void onSearchEntered(String entered) { + } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java index a4a5a3c0..cb758eec 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java @@ -44,12 +44,14 @@ import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostView.PostViewCallback, ThreadStatusCell.Callback { private ThreadPresenterCallback threadPresenterCallback; private Loadable loadable; private ChanLoader chanLoader; + private boolean searchOpen = false; public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { this.threadPresenterCallback = threadPresenterCallback; @@ -112,6 +114,21 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt return ChanApplication.getWatchManager().findPinByLoadable(loadable) != null; } + public void onSearchVisibilityChanged(boolean visible) { + searchOpen = visible; + threadPresenterCallback.showSearch(visible); + } + + public void onSearchEntered(String entered) { + if (chanLoader.getThread() != null) { + if (TextUtils.isEmpty(entered)) { + threadPresenterCallback.filterList(null, null, true, true, false); + } else { + processSearch(chanLoader.getThread().posts, entered); + } + } + } + @Override public Loadable getLoadable() { return loadable; @@ -134,11 +151,6 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt /* * PostAdapter callbacks */ - @Override - public void onFilteredResults(String filter, int count, boolean all) { - - } - @Override public void onListScrolledToBottom() { if (loadable.isThreadMode()) { @@ -153,24 +165,25 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } } - @Override - public void scrollTo(int position) { - threadPresenterCallback.scrollTo(position); + public void scrollTo(int position, boolean smooth) { + threadPresenterCallback.scrollTo(position, smooth); } - public void scrollTo(PostImage postImage) { - int position = -1; - for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { - Post post = chanLoader.getThread().posts.get(i); - if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) { - position = i; - break; + public void scrollToImage(PostImage postImage, boolean smooth) { + if (!searchOpen) { + int position = -1; + for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { + Post post = chanLoader.getThread().posts.get(i); + if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) { + position = i; + break; + } } + scrollTo(position, smooth); } - scrollTo(position); } - public void scrollToPost(Post needle) { + public void scrollToPost(Post needle, boolean smooth) { int position = -1; for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { Post post = chanLoader.getThread().posts.get(i); @@ -179,7 +192,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt break; } } - scrollTo(position); + scrollTo(position, smooth); } public void highlightPost(Post post) { @@ -195,6 +208,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt Loadable threadLoadable = new Loadable(post.board, post.no); threadLoadable.generateTitle(post); threadPresenterCallback.showThread(threadLoadable); + } else { + if (searchOpen) { + threadPresenterCallback.filterList(null, null, true, false, true); + highlightPost(post); + scrollToPost(post, false); + } } } @@ -304,6 +323,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt @Override public void onShowPostReplies(Post post) { + List posts = new ArrayList<>(); for (int no : post.repliesFrom) { Post replyPost = findPostById(no); @@ -382,6 +402,31 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt return null; } + private void processSearch(List all, String originalQuery) { + List filtered = new ArrayList<>(); + + String query = originalQuery.toLowerCase(Locale.ENGLISH); + + boolean add; + for (Post item : all) { + add = false; + if (item.comment.toString().toLowerCase(Locale.ENGLISH).contains(query)) { + add = true; + } else if (item.subject.toLowerCase(Locale.ENGLISH).contains(query)) { + add = true; + } else if (item.name.toLowerCase(Locale.ENGLISH).contains(query)) { + add = true; + } else if (item.filename != null && item.filename.toLowerCase(Locale.ENGLISH).contains(query)) { + add = true; + } + if (add) { + filtered.add(item); + } + } + + threadPresenterCallback.filterList(originalQuery, filtered, false, false, false); + } + public interface ThreadPresenterCallback { void showPosts(ChanThread thread); @@ -403,10 +448,14 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt void showImages(List images, int index, Loadable loadable, ThumbnailView thumbnail); - void scrollTo(int position); + void scrollTo(int position, boolean smooth); void highlightPost(Post post); void highlightPostId(String id); + + void showSearch(boolean show); + + void filterList(String query, List filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java index 40711e34..aee26df7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java @@ -18,7 +18,6 @@ package org.floens.chan.ui.adapter; import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -45,10 +44,9 @@ public class PostAdapter extends RecyclerView.Adapter { private final List displayList = new ArrayList<>(); private int lastPostCount = 0; private String error = null; - private String filter = ""; - private int pendingScrollToPost = -1; private Post highlightedPost; private String highlightedPostId; + private boolean filtering = false; public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback, ThreadStatusCell.Callback statusCellCallback) { this.recyclerView = recyclerView; @@ -80,7 +78,7 @@ public class PostAdapter extends RecyclerView.Adapter { boolean highlight = post == highlightedPost || post.id.equals(highlightedPostId); postViewHolder.postView.setPost(post, postViewCallback, highlight); } else if (getItemViewType(position) == TYPE_STATUS) { - ((StatusViewHolder)holder).threadStatusCell.update(); + ((StatusViewHolder) holder).threadStatusCell.update(); onScrolledToBottom(); } } @@ -121,8 +119,10 @@ public class PostAdapter extends RecyclerView.Adapter { sourceList.clear(); sourceList.addAll(thread.posts); - displayList.clear(); - displayList.addAll(sourceList); + if (!filtering) { + displayList.clear(); + displayList.addAll(sourceList); + } // Update all, recyclerview will figure out all the animations notifyDataSetChanged(); @@ -130,6 +130,7 @@ public class PostAdapter extends RecyclerView.Adapter { public void cleanup() { highlightedPost = null; + filtering = false; sourceList.clear(); displayList.clear(); lastPostCount = 0; @@ -148,6 +149,33 @@ public class PostAdapter extends RecyclerView.Adapter { } } + public void filterList(List filter) { + filtering = true; + + displayList.clear(); + for (Post item : sourceList) { + for (Post filterItem : filter) { + if (filterItem.no == item.no) { + displayList.add(item); + break; + } + } + } + + notifyDataSetChanged(); + } + + public void clearFilter() { + if (filtering) { + filtering = false; + + displayList.clear(); + displayList.addAll(sourceList); + + notifyDataSetChanged(); + } + } + public void highlightPost(Post post) { highlightedPostId = null; highlightedPost = post; @@ -161,7 +189,7 @@ public class PostAdapter extends RecyclerView.Adapter { } private void onScrolledToBottom() { - if (lastPostCount != sourceList.size()) { + if (!filtering && lastPostCount != sourceList.size()) { lastPostCount = sourceList.size(); postAdapterCallback.onListScrolledToBottom(); } @@ -171,10 +199,6 @@ public class PostAdapter extends RecyclerView.Adapter { return postAdapterCallback.getLoadable().isThreadMode(); } - private boolean isFiltering() { - return !TextUtils.isEmpty(filter); - } - public static class PostViewHolder extends RecyclerView.ViewHolder { private PostView postView; @@ -193,127 +217,9 @@ public class PostAdapter extends RecyclerView.Adapter { } } -/* - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (position >= getCount() - 1) { - onScrolledToBottom(); - } - - switch (getItemViewType(position)) { - case VIEW_TYPE_ITEM: { - if (convertView == null || convertView.getTag() == null || (Integer) convertView.getTag() != VIEW_TYPE_ITEM) { - convertView = new PostView(context); - convertView.setTag(VIEW_TYPE_ITEM); - } - - PostView postView = (PostView) convertView; - postView.setPost(getItem(position), postViewCallback); - - return postView; - } - case VIEW_TYPE_STATUS: { - return new StatusView(context); - } - } - - return null; - } - - public Filter getFilter() { - return new Filter() { - @Override - protected FilterResults performFiltering(CharSequence constraintRaw) { - FilterResults results = new FilterResults(); - - if (TextUtils.isEmpty(constraintRaw)) { - ArrayList tmp; - synchronized (lock) { - tmp = new ArrayList<>(sourceList); - } - results.values = tmp; - } else { - List all; - synchronized (lock) { - all = new ArrayList<>(sourceList); - } - - List accepted = new ArrayList<>(); - String constraint = constraintRaw.toString().toLowerCase(Locale.ENGLISH); - - for (Post post : all) { - if (post.comment.toString().toLowerCase(Locale.ENGLISH).contains(constraint) || - post.subject.toLowerCase(Locale.ENGLISH).contains(constraint)) { - accepted.add(post); - } - } - - results.values = accepted; - } - - return results; - } - - @SuppressWarnings("unchecked") - @Override - protected void publishResults(CharSequence constraint, final FilterResults results) { - filter = constraint.toString(); - synchronized (lock) { - displayList.clear(); - displayList.addAll((List) results.values); - } - notifyDataSetChanged(); - postAdapterCallback.onFilteredResults(filter, ((List) results.values).size(), TextUtils.isEmpty(filter)); - if (pendingScrollToPost >= 0) { - final int to = pendingScrollToPost; - pendingScrollToPost = -1; - postAdapterCallback.scrollTo(to); - } - } - }; - } - - public void setFilter(String filter) { - getFilter().filter(filter); - notifyDataSetChanged(); - } - - public void setThread(ChanThread thread) { - synchronized (lock) { - if (thread.archived) { - statusPrefix = context.getString(R.string.thread_archived) + " - "; - } else if (thread.closed) { - statusPrefix = context.getString(R.string.thread_closed) + " - "; - } else { - statusPrefix = ""; - } - - sourceList.clear(); - sourceList.addAll(thread.posts); - - if (!isFiltering()) { - displayList.clear(); - displayList.addAll(sourceList); - } else { - setFilter(filter); - } - } - - notifyDataSetChanged(); - }*/ - public interface PostAdapterCallback { - void onFilteredResults(String filter, int count, boolean all); - Loadable getLoadable(); void onListScrolledToBottom(); - - void scrollTo(int position); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java index 5ee6702d..fe653a0d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java @@ -13,6 +13,7 @@ import org.floens.chan.R; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Post; +import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; public class ThreadStatusCell extends LinearLayout implements View.OnClickListener { @@ -55,6 +56,7 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen protected void onFinishInflate() { super.onFinishInflate(); text = (TextView) findViewById(R.id.text); + text.setTypeface(ROBOTO_MEDIUM); setOnClickListener(this); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java index 535c5c3f..79295cac 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -42,7 +42,7 @@ import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; import java.util.List; -public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, FloatingMenu.FloatingMenuCallback, RootNavigationController.DrawerCallbacks { +public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, FloatingMenu.FloatingMenuCallback { private static final int REFRESH_ID = 1; private static final int POST_ID = 2; private static final int SEARCH_ID = 101; @@ -99,7 +99,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { switch ((Integer) item.getId()) { case SEARCH_ID: - // TODO + navigationController.showSearch(); break; case SHARE_ID: String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java index 2461df25..55f14071 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java @@ -54,7 +54,7 @@ public class PostRepliesController extends Controller { view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - postPopupHelper.popAll(); + postPopupHelper.pop(); } }); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java index b3c0e1cd..8a1ed7f7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java @@ -188,9 +188,34 @@ public class RootNavigationController extends NavigationController implements Pi } } + @Override + public String getSearchHint() { + return context.getString(R.string.search_hint); + } + + @Override + public void onSearchVisibilityChanged(boolean visible) { + Controller top = getTop(); + if (top instanceof DrawerCallbacks) { + ((DrawerCallbacks) top).onSearchVisibilityChanged(visible); + } + } + + @Override + public void onSearchEntered(String entered) { + Controller top = getTop(); + if (top instanceof DrawerCallbacks) { + ((DrawerCallbacks) top).onSearchEntered(entered); + } + } + public interface DrawerCallbacks { void onPinClicked(Pin pin); boolean isPinCurrent(Pin pin); + + void onSearchVisibilityChanged(boolean visible); + + void onSearchEntered(String entered); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java index 3a85f350..f7fb58c1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java @@ -13,7 +13,7 @@ import java.util.List; import de.greenrobot.event.EventBus; -public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback { +public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallbacks { protected ThreadLayout threadLayout; public ThreadController(Context context) { @@ -75,11 +75,21 @@ public abstract class ThreadController extends Controller implements ThreadLayou public void scrollToImage(PostImage postImage) { if (!threadLayout.postRepliesOpen()) { - threadLayout.getPresenter().scrollTo(postImage); + threadLayout.getPresenter().scrollToImage(postImage, true); } } @Override public void onShowPosts() { } + + @Override + public void onSearchVisibilityChanged(boolean visible) { + threadLayout.getPresenter().onSearchVisibilityChanged(visible); + } + + @Override + public void onSearchEntered(String entered) { + threadLayout.getPresenter().onSearchEntered(entered); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java index 8336d0db..cafcc3df 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -35,7 +35,7 @@ import org.floens.chan.utils.AndroidUtils; import java.util.Arrays; -public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback, RootNavigationController.DrawerCallbacks { +public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback { private static final int POST_ID = 1; private static final int PIN_ID = 2; private static final int REFRESH_ID = 101; @@ -166,7 +166,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo threadLayout.getPresenter().requestData(); break; case SEARCH_ID: - // TODO + navigationController.showSearch(); break; case SHARE_ID: Loadable loadable = threadLayout.getPresenter().getLoadable(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java index 3718f7b2..0d704b3e 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java @@ -320,11 +320,6 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana } - @Override - public void scrollTo(int position) { - - } - private RelativeLayout createView() { RelativeLayout compound = new RelativeLayout(getActivity()); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java index bba63ee6..6d33df1f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java @@ -82,7 +82,7 @@ public class PostPopupHelper { public void postClicked(Post p) { popAll(); presenter.highlightPost(p); - presenter.scrollToPost(p); + presenter.scrollToPost(p, true); } private void dismiss() { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index 1edd6ca3..595eb5ca 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -72,7 +72,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres private TextView errorText; private Button errorRetryButton; private PostPopupHelper postPopupHelper; - private Visible visible = Visible.LOADING; + private Visible visible; public ThreadLayout(Context context) { super(context); @@ -220,8 +220,8 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres } @Override - public void scrollTo(int position) { - threadListLayout.scrollTo(position); + public void scrollTo(int position, boolean smooth) { + threadListLayout.scrollTo(position, smooth); } @Override @@ -234,6 +234,15 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres threadListLayout.highlightPostId(id); } + @Override + public void showSearch(boolean show) { + threadListLayout.showSearch(show); + } + + public void filterList(String query, List filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard) { + threadListLayout.filterList(query, filter, clearFilter, setEmptyText, hideKeyboard); + } + public ThumbnailView getThumbnail(PostImage postImage) { if (postPopupHelper.isOpen()) { return postPopupHelper.getThumbnail(postImage); @@ -248,10 +257,12 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres private void switchVisible(Visible visible) { if (this.visible != visible) { - switch (this.visible) { - case THREAD: - threadListLayout.cleanup(); - break; + if (this.visible != null) { + switch (this.visible) { + case THREAD: + threadListLayout.cleanup(); + break; + } } this.visible = visible; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java index a1346cc4..58eb2560 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java @@ -22,7 +22,8 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; -import android.widget.RelativeLayout; +import android.widget.LinearLayout; +import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.core.model.ChanThread; @@ -32,11 +33,18 @@ import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.view.PostView; import org.floens.chan.ui.view.ThumbnailView; +import org.floens.chan.utils.AndroidUtils; +import org.floens.chan.utils.AnimationUtils; + +import java.util.List; + +import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; /** * A layout that wraps around a {@link RecyclerView} to manage showing posts. */ -public class ThreadListLayout extends RelativeLayout { +public class ThreadListLayout extends LinearLayout { + private TextView searchStatus; private RecyclerView recyclerView; private PostAdapter postAdapter; private PostAdapter.PostAdapterCallback postAdapterCallback; @@ -49,6 +57,10 @@ public class ThreadListLayout extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); + + searchStatus = (TextView) findViewById(R.id.search_status); + searchStatus.setTypeface(ROBOTO_MEDIUM); + recyclerView = (RecyclerView) findViewById(R.id.recycler_view); LinearLayoutManager lm = new LinearLayoutManager(getContext()); recyclerView.setLayoutManager(lm); @@ -77,6 +89,37 @@ public class ThreadListLayout extends RelativeLayout { postAdapter.showError(error); } + public void showSearch(boolean show) { + AnimationUtils.animateHeight(searchStatus, show); + + if (show) { + searchStatus.setText(R.string.search_empty); + } else { + postAdapter.clearFilter(); + recyclerView.scrollToPosition(0); + } + } + + public void filterList(String query, List filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard) { + if (clearFilter) { + postAdapter.clearFilter(); + } + + if (hideKeyboard) { + AndroidUtils.hideKeyboard(this); + } + + if (setEmptyText) { + searchStatus.setText(R.string.search_empty); + } + + if (query != null) { + postAdapter.filterList(filter); + searchStatus.setText(getContext().getString(R.string.search_results, + getContext().getResources().getQuantityString(R.plurals.posts, filter.size(), filter.size()), query)); + } + } + public void cleanup() { postAdapter.cleanup(); } @@ -99,8 +142,12 @@ public class ThreadListLayout extends RelativeLayout { return thumbnail; } - public void scrollTo(int position) { - recyclerView.smoothScrollToPosition(position); + public void scrollTo(int position, boolean smooth) { + if (smooth) { + recyclerView.smoothScrollToPosition(position); + } else { + recyclerView.scrollToPosition(position); + } } public void highlightPost(Post post) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java index ac257202..6d5da0a0 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java @@ -26,11 +26,18 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.Build; +import android.text.Editable; +import android.text.TextWatcher; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.Gravity; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -39,6 +46,7 @@ import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.ui.drawable.ArrowMenuDrawable; import org.floens.chan.ui.drawable.DropdownArrowDrawable; +import org.floens.chan.ui.view.LoadView; import org.floens.chan.utils.AndroidUtils; import java.util.ArrayList; @@ -47,14 +55,15 @@ import java.util.List; import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; -public class Toolbar extends LinearLayout implements View.OnClickListener { +public class Toolbar extends LinearLayout implements View.OnClickListener, LoadView.Listener { private ImageView arrowMenuView; private ArrowMenuDrawable arrowMenuDrawable; - private FrameLayout navigationItemContainer; + private LoadView navigationItemContainer; private ToolbarCallback callback; private NavigationItem navigationItem; + private boolean search = false; public Toolbar(Context context) { super(context); @@ -72,20 +81,101 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } public void updateNavigation() { + closeSearchInternal(true); setNavigationItem(false, false, navigationItem); } + public boolean showSearch() { + if (!search) { + search = true; + + LinearLayout searchViewWrapper = new LinearLayout(getContext()); + final EditText searchView = new EditText(getContext()); + searchView.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_DONE); + searchView.setHint(callback.getSearchHint()); + searchView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + searchView.setHintTextColor(0x88ffffff); + searchView.setTextColor(0xffffffff); + searchView.setSingleLine(true); + searchView.setBackgroundResource(0); + searchView.setPadding(0, 0, 0, 0); + final ImageView clearButton = new ImageView(getContext()); + searchView.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + callback.onSearchEntered(s.toString()); + clearButton.setAlpha(s.length() == 0 ? 0.6f : 1.0f); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + searchView.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + AndroidUtils.hideKeyboard(searchView); + callback.onSearchEntered(searchView.getText().toString()); + return true; + } + return false; + } + }); + LinearLayout.LayoutParams searchViewParams = new LinearLayout.LayoutParams(0, dp(36), 1); + searchViewParams.gravity = Gravity.CENTER_VERTICAL; + searchViewWrapper.addView(searchView, searchViewParams); + + clearButton.setImageResource(R.drawable.ic_close_white_24dp); + clearButton.setAlpha(0.6f); + clearButton.setScaleType(ImageView.ScaleType.CENTER); + clearButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + searchView.setText(""); + AndroidUtils.requestKeyboardFocus(searchView); + } + }); + searchViewWrapper.addView(clearButton, dp(48), LayoutParams.MATCH_PARENT); + searchViewWrapper.setPadding(dp(16), 0, 0, 0); + + searchView.post(new Runnable() { + @Override + public void run() { + searchView.requestFocus(); + AndroidUtils.requestKeyboardFocus(searchView); + } + }); + + navigationItemContainer.setView(searchViewWrapper, true); + animateArrow(true, 0); + callback.onSearchVisibilityChanged(true); + return true; + } else { + return false; + } + } + + public boolean closeSearch() { + return closeSearchInternal(false); + } + public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { + closeSearchInternal(true); if (item.menu != null) { AndroidUtils.waitForMeasure(this, new AndroidUtils.OnMeasuredCallback() { @Override public boolean onMeasured(View view) { - setNavigationItemView(animate, pushing, item); + setNavigationItemInternal(animate, pushing, false, item); return true; } }); } else { - setNavigationItemView(animate, pushing, item); + setNavigationItemInternal(animate, pushing, false, item); } } @@ -109,6 +199,14 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { public void onConfigurationChanged(Configuration newConfig) { } + @Override + public void onLoadViewRemoved(View view) { + // TODO: this is kinda a hack + if (view instanceof ViewGroup) { + ((ViewGroup) view).removeAllViews(); + } + } + private void init() { setOrientation(HORIZONTAL); @@ -130,11 +228,27 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(dp(56), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); - navigationItemContainer = new FrameLayout(getContext()); + navigationItemContainer = new LoadView(getContext()); + navigationItemContainer.setListener(this); addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); } - private void setNavigationItemView(boolean animate, boolean pushing, NavigationItem toItem) { + private boolean closeSearchInternal(boolean fromSetNavigation) { + if (search) { + search = false; + setNavigationItemInternal(true, false, true, navigationItem); + AndroidUtils.hideKeyboard(navigationItemContainer); + callback.onSearchVisibilityChanged(false); + if (!fromSetNavigation) { + animateArrow(navigationItem.hasBack, 0); + } + return true; + } else { + return false; + } + } + + private void setNavigationItemInternal(boolean animate, boolean pushing, boolean fromSearch, NavigationItem toItem) { final NavigationItem fromItem = navigationItem; if (!animate) { @@ -146,27 +260,25 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { toItem.view = createNavigationItemView(toItem); - navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + // use the LoadView animation when from a search + if (fromSearch) { + navigationItemContainer.setView(toItem.view, true); + } else { + navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } final int duration = 300; final int offset = dp(16); + final int delay = pushing ? 0 : 100; - if (animate) { + // Use the LoadView animation when from a search + if (animate && !fromSearch) { toItem.view.setAlpha(0f); List animations = new ArrayList<>(5); if (fromItem != null && fromItem.hasBack != toItem.hasBack) { - ValueAnimator arrowAnimation = ValueAnimator.ofFloat(fromItem.hasBack ? 1f : 0f, toItem.hasBack ? 1f : 0f); - arrowAnimation.setDuration(duration); - arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); - arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - setArrowMenuProgress((float) animation.getAnimatedValue()); - } - }); - animations.add(arrowAnimation); + animateArrow(toItem.hasBack, delay); } else { setArrowMenuProgress(toItem.hasBack ? 1f : 0f); } @@ -202,7 +314,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { } AnimatorSet set = new AnimatorSet(); - set.setStartDelay(pushing ? 0 : 100); + set.setStartDelay(delay); set.playTogether(animations); set.start(); } @@ -252,7 +364,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { @Override public boolean onMeasured(View view) { if (item.middleMenu != null) { - item.middleMenu.setPopupWidth(Math.max(dp(150), titleView.getWidth())); + item.middleMenu.setPopupWidth(Math.max(dp(200), titleView.getWidth())); } return false; } @@ -261,7 +373,30 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { return wrapper; } + private void animateArrow(boolean toArrow, long delay) { + float to = toArrow ? 1f : 0f; + if (to != arrowMenuDrawable.getProgress()) { + ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to); + arrowAnimation.setDuration(300); + arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); + arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setArrowMenuProgress((float) animation.getAnimatedValue()); + } + }); + arrowAnimation.setStartDelay(delay); + arrowAnimation.start(); + } + } + public interface ToolbarCallback { void onMenuOrBackClicked(boolean isArrow); + + void onSearchVisibilityChanged(boolean visible); + + String getSearchHint(); + + void onSearchEntered(String entered); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java index f139c02e..bfc7fa62 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java @@ -34,24 +34,18 @@ import android.widget.ProgressBar; */ public class LoadView extends FrameLayout { private int fadeDuration = 200; + private Listener listener; public LoadView(Context context) { super(context); - init(); } public LoadView(Context context, AttributeSet attrs) { super(context, attrs); - init(); } public LoadView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(); - } - - private void init() { - setView(null, false); } /** @@ -62,6 +56,14 @@ public class LoadView extends FrameLayout { this.fadeDuration = fadeDuration; } + /** + * Set a listener that gives a call when a view gets removed + * @param listener the listener + */ + public void setListener(Listener listener) { + this.listener = listener; + } + /** * Set the content of this container. It will fade the attached views out with the * new one. Set view to null to show the progressbar. @@ -107,6 +109,9 @@ public class LoadView extends FrameLayout { // Animation ended without interruptions, remove listener for future animations. childAnimation.setListener(null); removeView(child); + if (listener != null) { + listener.onLoadViewRemoved(child); + } } }).start(); } @@ -131,6 +136,9 @@ public class LoadView extends FrameLayout { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.clearAnimation(); + if (listener != null) { + listener.onLoadViewRemoved(child); + } } removeAllViews(); newView.clearAnimation(); @@ -138,4 +146,8 @@ public class LoadView extends FrameLayout { addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } } + + public interface Listener { + void onLoadViewRemoved(View view); + } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index 7811aa8c..c685dfb1 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -154,12 +154,16 @@ public class AndroidUtils { dialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { - InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, 0); + requestKeyboardFocus(view); } }); } + public static void requestKeyboardFocus(final View view) { + InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + public static void hideKeyboard(View view) { InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0fd15563a263e138b1f419e02a807ff8c40f2554 GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK3>dAqwX{BQ3+vmeOgEbxddW?IB4!nj``hO=~G=WkL+ z@9E+gVsZNEWNV?r3OuY&?=Z?L&Ho)f{7Mm9$xa9H@#qTmGi6+RV TUAFrSbQ*)FtDnm{r-UW|v%+BV literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..76e07f0970ac317b1bddfb265c5b3b57079dc4fc GIT binary patch literal 402 zcmV;D0d4+?P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_009a~L_t(o!|m5g4uUWgh2e{kxIvTw;NDPC;}$JB4lqhd zALpV>OkoK9KLVwJ46^RFS-GEp1SB8;6w*sYS86#*?l}h6Uf>SEpn?#4jRC*~x$CHV z$Q9$p5DkXc*(V;TVMyYWH`wz4OGNX2fFas<{^84qfEYpL!$622^D)3j947f)xWLTv zIibiiz?@L!Ilv}9`H~sS0aHSi=K-6b^H{iCXR@WwrLTXmZjnL#%Uje;9<9+4+I(rv zD1y-D=aV){#PY3k-YW<`Z@;xPu`I73{Q3BrLlI2gzuptXH+hP159`UiO>{hv;}-f- wa1v+DhnFD7naqzw=gvd2^p}}{1pEQ;1dp!cmVSGKY5)KL07*qoM6N<$f)#I~?f?J) literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb9d8b083f0e23cad450e6b81e6e31c06e730e5 GIT binary patch literal 492 zcmV004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00Cr4L_t(&-tF2!a>F1D1yH3Md^D1r)vXtGlB0R|yYM1S zLX6OtJ4{Eg^@ukHjKuf=>8pK6fC3bt00k&O0Sb`FSw>0JID<@*RZpVc&sYF_BSDOP zzzx8HTz^)}0YO?h0I=Z5g6CRb6Cl2(9$2K>T9ytIoa-f8nAiZ*`@3I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s zRKe%z;uunK>+Nmdyh8>8ZHbpxdC!t-6PsA@Az*9tuW|{ks)qI5Is(2vHs8$WDV#^lJw82zHADRtFfbG4gHTD`+TfU|z#4lOX$m0W5We-Q=O=n$W0Q z7uuIr=%3ieCeN^Mqu4w1{aY=VZaj|LHa}+~drgvrS>g%y26IV8{rSEYPrErM{n4Mp z-(4_IUuB8@=dO*4!X_uX5V?1HO6%{pS+`z&G2gTLp@3D==llE%%bt1rs9!l7u5jhta^Z@fm-)T; zCa)Lby!CVS3D2o+7tZl>>|*&_lj_u46Y4a##?>iv-;*-e)&&#g_eX{(Tsaf22-92i zY5URsj82yB8m)tB^F214S}yqJlPAmR8ZO1!mEx^`EEYazmfF+*$++&iuhN^1PniD+ zMf^-|Qi!h-vl5)~^Epf4zCTt50yloDHyPMJ_qvm?e1g3|Ri@pcL;t;BGO)*dw>VHG ulVHoYL5@A5pLxx3#%qrmUhk|rpuh24@WsQ^%4LDcg~8L+&t;ucLK6To`3n{R literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/layout/layout_thread_list.xml b/Clover/app/src/main/res/layout/layout_thread_list.xml index 54cf99e6..fda19b4a 100644 --- a/Clover/app/src/main/res/layout/layout_thread_list.xml +++ b/Clover/app/src/main/res/layout/layout_thread_list.xml @@ -1,12 +1,26 @@ + + + android:layout_height="0dp" /> diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 0e6fcf3e..ada811d7 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -32,6 +32,11 @@ along with this program. If not, see . %d minutes + + %d post + %d posts + + Settings Reload Reload board @@ -51,8 +56,9 @@ along with this program. If not, see . Search thread Image search - Search posts - Found %1$s %2$s for "%3$s" + Search + Found %1$s for "%2$s" + Search subjects, comments, names and filenames Unsupported link Clover can\'t open this link. Opening it in your browser instead. @@ -256,7 +262,6 @@ Don't have a 4chan Pass?<br> Open Source Licenses - Edit boards Add or remove boards Thread watcher