From 84fabd48099e4b5a7b3dcae4d156174ee0d9023d Mon Sep 17 00:00:00 2001 From: Floens Date: Wed, 28 Mar 2018 21:20:06 +0200 Subject: [PATCH] add a search field to the board dropdown refactored the dropdown to its own view, to allow a edittext to be placed in the list. --- .../chan/core/manager/BoardManager.java | 67 ++- .../org/floens/chan/core/model/orm/Board.java | 4 + .../core/presenter/BoardsMenuPresenter.java | 194 +++++++ .../chan/core/presenter/BrowsePresenter.java | 4 - .../floens/chan/ui/adapter/DrawerAdapter.java | 2 +- .../chan/ui/controller/BrowseController.java | 33 +- .../floens/chan/ui/helper/BoardHelper.java | 26 + .../ui/layout/BrowseBoardsFloatingMenu.java | 476 +++++++++++++----- .../org/floens/chan/ui/toolbar/Toolbar.java | 136 ++--- .../src/main/res/layout/cell_browse_board.xml | 1 + ..._board_input.xml => cell_browse_input.xml} | 1 + .../src/main/res/layout/cell_browse_site.xml | 1 + Clover/app/src/main/res/values/strings.xml | 2 +- 13 files changed, 719 insertions(+), 228 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/presenter/BoardsMenuPresenter.java rename Clover/app/src/main/res/layout/{cell_board_input.xml => cell_browse_input.xml} (96%) diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java index f8cd7ffc..fbe44356 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java @@ -19,6 +19,7 @@ package org.floens.chan.core.manager; import android.util.Pair; +import org.floens.chan.core.database.DatabaseBoardManager; import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.site.Site; @@ -52,20 +53,24 @@ public class BoardManager { private static final Comparator ORDER_SORT = (lhs, rhs) -> lhs.order - rhs.order; private final DatabaseManager databaseManager; + private final DatabaseBoardManager databaseBoardManager; - private final List>> sitesWithSavedBoards = new ArrayList<>(); + private final AllBoards allBoardsObservable = new AllBoards(); private final SavedBoards savedBoardsObservable = new SavedBoards(); + private final List>> sitesWithBoards = new ArrayList<>(); + private final List>> sitesWithSavedBoards = new ArrayList<>(); + @Inject public BoardManager(DatabaseManager databaseManager) { this.databaseManager = databaseManager; + databaseBoardManager = databaseManager.getDatabaseBoardManager(); - updateSavedBoardsAndNotify(); + updateObservables(); } public void createAll(List boards) { - databaseManager.runTask( - databaseManager.getDatabaseBoardManager().createAll(boards)); + databaseManager.runTask(databaseBoardManager.createAll(boards)); } /** @@ -77,56 +82,76 @@ public class BoardManager { * @return the board code with the same site and board code, or {@code null} if not found. */ public Board getBoard(Site site, String code) { - return databaseManager.runTask( - databaseManager.getDatabaseBoardManager().getBoard(site, code)); + return databaseManager.runTask(databaseBoardManager.getBoard(site, code)); } public List getSiteBoards(Site site) { - return databaseManager.runTask( - databaseManager.getDatabaseBoardManager().getSiteBoards(site)); + List boards = databaseManager.runTask(databaseBoardManager.getSiteBoards(site)); + Collections.sort(boards, ORDER_SORT); + return boards; } public List getSiteSavedBoards(Site site) { - List boards = databaseManager.runTask( - databaseManager.getDatabaseBoardManager().getSiteSavedBoards(site)); + List boards = databaseManager.runTask(databaseBoardManager.getSiteSavedBoards(site)); Collections.sort(boards, ORDER_SORT); return boards; } + public AllBoards getAllBoardsObservable() { + return allBoardsObservable; + } + public SavedBoards getSavedBoardsObservable() { return savedBoardsObservable; } public void updateBoardOrders(List boards) { - databaseManager.runTask(databaseManager.getDatabaseBoardManager() - .updateOrders(boards)); - updateSavedBoardsAndNotify(); + databaseManager.runTask(databaseBoardManager.updateOrders(boards)); + updateObservables(); } public void setSaved(Board board, boolean saved) { board.saved = saved; - databaseManager.runTask(databaseManager.getDatabaseBoardManager().updateIncludingUserFields(board)); - updateSavedBoardsAndNotify(); + databaseManager.runTask(databaseBoardManager.updateIncludingUserFields(board)); + updateObservables(); } public void setAllSaved(List boards, boolean saved) { for (Board board : boards) { board.saved = saved; } - databaseManager.runTask(databaseManager.getDatabaseBoardManager().updateIncludingUserFields(boards)); - updateSavedBoardsAndNotify(); + databaseManager.runTask(databaseBoardManager.updateIncludingUserFields(boards)); + updateObservables(); } - private void updateSavedBoardsAndNotify() { - sitesWithSavedBoards.clear(); + private void updateObservables() { + sitesWithBoards.clear(); for (Site site : Sites.allSites()) { - List siteBoards = getSiteSavedBoards(site); - sitesWithSavedBoards.add(new Pair<>(site, siteBoards)); + List all = getSiteBoards(site); + sitesWithBoards.add(new Pair<>(site, all)); + + List saved = new ArrayList<>(); + for (Board siteBoard : all) { + if (siteBoard.saved) saved.add(siteBoard); + } + sitesWithSavedBoards.add(new Pair<>(site, saved)); } + allBoardsObservable.doNotify(); savedBoardsObservable.doNotify(); } + public class AllBoards extends Observable { + private void doNotify() { + setChanged(); + notifyObservers(); + } + + public List>> get() { + return sitesWithBoards; + } + } + public class SavedBoards extends Observable { private void doNotify() { setChanged(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java b/Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java index 35ad825b..a6aecdaa 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java @@ -170,6 +170,10 @@ public class Board implements SiteReference { return site; } + public boolean siteCodeEquals(Board other) { + return code.equals(other.code) && other.siteId == siteId; + } + /** * Updates the board with data from {@code o}.
* {@link #id}, {@link #saved}, {@link #order} are skipped because these are user-set. diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/BoardsMenuPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/BoardsMenuPresenter.java new file mode 100644 index 00000000..ae12fe52 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/BoardsMenuPresenter.java @@ -0,0 +1,194 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.floens.chan.core.presenter; + +import android.support.annotation.Nullable; +import android.util.Pair; + +import org.floens.chan.core.manager.BoardManager; +import org.floens.chan.core.model.orm.Board; +import org.floens.chan.core.site.Site; +import org.floens.chan.ui.helper.BoardHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Observable; +import java.util.Observer; + +import javax.inject.Inject; + +public class BoardsMenuPresenter implements Observer { + private Callback callback; + private BoardManager.AllBoards allBoards; + + private Items items; + + @Nullable + private String filter; + + @Inject + public BoardsMenuPresenter(BoardManager boardManager) { + allBoards = boardManager.getAllBoardsObservable(); + } + + public void create(Callback callback, Board selectedBoard) { + this.callback = callback; + + this.allBoards.addObserver(this); + + items = new Items(); + + updateWithFilter(); + + callback.scrollToPosition(items.findBoardPosition(selectedBoard)); + } + + public void destroy() { + allBoards.deleteObserver(this); + } + + public Items items() { + return items; + } + + public void filterChanged(String filter) { + this.filter = filter; + updateWithFilter(); + } + + @Override + public void update(Observable o, Object arg) { + if (o == allBoards) { + updateWithFilter(); + } + } + + private void updateWithFilter() { + items.update(this.allBoards.get(), filter); + } + + public static class Items extends Observable { + public List items = new ArrayList<>(); + private int itemIdCounter = 1; + + public Items() { + } + + public void update(List>> allBoards, String filter) { + items.clear(); + + items.add(new Item(0, Item.Type.SEARCH)); + + for (Pair> siteAndBoards : allBoards) { + Site site = siteAndBoards.first; + List boards = siteAndBoards.second; + + items.add(new Item(itemIdCounter++, site)); + + if (filter == null || filter.length() == 0) { + for (Board board : boards) { + if (board.saved) { + items.add(new Item(itemIdCounter++, board)); + } + } + } else { + List res = BoardHelper.quickSearch(boards, filter); + for (Board b : res) { + items.add(new Item(itemIdCounter++, b)); + } + } + } + + setChanged(); + notifyObservers(); + } + + private boolean shouldShowBoard(String filter, Board board) { + if (filter == null || filter.length() == 0) { + return board.saved; + } + + String fl = filter.toLowerCase(); + return board.code.toLowerCase().contains(fl) || + (board.name != null && board.name.toLowerCase().contains(fl));/* || + (board.description != null && board.description.toLowerCase().contains(fl));*/ + } + + public int getCount() { + return items.size(); + } + + public int findBoardPosition(Board board) { + int position = 0; + for (Item item : items) { + + if (item.board != null && item.board.siteCodeEquals(board)) { + return position; + } + + position++; + } + + return 0; + } + + public Item getAtPosition(int position) { + return items.get(position); + } + } + + public static class Item { + public enum Type { + BOARD(0), + SITE(1), + SEARCH(2); + + public int typeId; + + Type(int typeId) { + this.typeId = typeId; + } + } + + public final Type type; + public Board board; + public Site site; + public int id; + + public Item(int id, Type type) { + this.id = id; + this.type = type; + } + + public Item(int id, Board board) { + this.id = id; + type = Type.BOARD; + this.board = board; + } + + public Item(int id, Site site) { + this.id = id; + type = Type.SITE; + this.site = site; + } + } + + public interface Callback { + void scrollToPosition(int position); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java index 27f03c48..eb18707a 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java @@ -84,10 +84,6 @@ public class BrowsePresenter implements Observer { callback.loadSiteSetup(site); } - public BoardManager.SavedBoards getSavedBoardsObservable() { - return boardManager.getSavedBoardsObservable(); - } - @Override public void update(Observable o, Object arg) { if (o == savedBoardsObservable) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java index 3aea838d..98b7f07d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java @@ -132,7 +132,7 @@ public class DrawerAdapter extends RecyclerView.Adapter case TYPE_LINK: return new LinkHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_link, parent, false)); case TYPE_BOARD_INPUT: - return new BoardInputHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_board_input, parent, false)); + return new BoardInputHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_browse_input, parent, false)); case TYPE_DIVIDER: return new DividerHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_divider, parent, false)); } 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 bfb4ad05..2f4bd885 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 @@ -51,7 +51,11 @@ import javax.inject.Inject; import static org.floens.chan.Chan.inject; import static org.floens.chan.utils.AndroidUtils.getString; -public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, BrowsePresenter.Callback { +public class BrowseController extends ThreadController implements + ToolbarMenuItem.ToolbarMenuItemCallback, + ThreadLayout.ThreadLayoutCallback, + BrowsePresenter.Callback, + BrowseBoardsFloatingMenu.ClickCallback { private static final int SEARCH_ID = 1; private static final int REFRESH_ID = 2; private static final int REPLY_ID = 101; @@ -151,20 +155,9 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte @SuppressLint("InflateParams") @Override public void show(View anchor) { - BrowseBoardsFloatingMenu boardsFloatingMenu = - new BrowseBoardsFloatingMenu(presenter.getSavedBoardsObservable()); - boardsFloatingMenu.show(anchor, presenter.currentBoard()); - boardsFloatingMenu.setCallback(new BrowseBoardsFloatingMenu.Callback() { - @Override - public void onBoardClicked(Board item) { - presenter.onBoardsFloatingMenuBoardClicked(item); - } - - @Override - public void onSiteClicked(Site site) { - presenter.onBoardsFloatingMenuSiteClicked(site); - } - }); + BrowseBoardsFloatingMenu boardsFloatingMenu = new BrowseBoardsFloatingMenu(context); + boardsFloatingMenu.show(view, anchor, BrowseController.this, + presenter.currentBoard()); } }; } @@ -220,6 +213,16 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte } } + @Override + public void onBoardClicked(Board item) { + presenter.onBoardsFloatingMenuBoardClicked(item); + } + + @Override + public void onSiteClicked(Site site) { + presenter.onBoardsFloatingMenuSiteClicked(site); + } + private void openArchive() { Board board = presenter.currentBoard(); if (board == null) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java index 4358354d..a87080b7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java @@ -25,6 +25,7 @@ import org.jsoup.parser.Parser; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; import java.util.List; import me.xdrop.fuzzywuzzy.FuzzySearch; @@ -40,6 +41,31 @@ public class BoardHelper { return board.description == null ? null : Parser.unescapeEntities(board.description, false); } + public static List quickSearch(List from, String query) { + from = new ArrayList<>(from); + query = query.toLowerCase(); + + List res = new ArrayList<>(); + + for (Iterator iterator = from.iterator(); iterator.hasNext(); ) { + Board board = iterator.next(); + if (board.code.toLowerCase().equals(query)) { + iterator.remove(); + res.add(board); + } + } + + for (Iterator iterator = from.iterator(); iterator.hasNext(); ) { + Board board = iterator.next(); + if (board.name.toLowerCase().contains(query)) { + iterator.remove(); + res.add(board); + } + } + + return res; + } + public static List search(List from, final String query) { List> ratios = new ArrayList<>(); for (Board board : from) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java index e63f62f5..a10c29d6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java @@ -17,219 +17,453 @@ */ package org.floens.chan.ui.layout; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; -import android.graphics.drawable.Drawable; -import android.util.Pair; -import android.view.Gravity; +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; +import android.view.ViewTreeObserver; +import android.view.animation.DecelerateInterpolator; +import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import org.floens.chan.R; -import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.model.orm.Board; +import org.floens.chan.core.presenter.BoardsMenuPresenter; +import org.floens.chan.core.presenter.BoardsMenuPresenter.Item; import org.floens.chan.core.site.Site; import org.floens.chan.core.site.SiteIcon; import org.floens.chan.ui.helper.BoardHelper; -import org.floens.chan.ui.view.FloatingMenu; import org.floens.chan.utils.AndroidUtils; -import java.util.List; import java.util.Observable; import java.util.Observer; +import javax.inject.Inject; + +import static org.floens.chan.Chan.inject; import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.removeFromParentView; + +/** + * A ViewGroup that attaches above the entire window, containing a list of boards the user can + * select. The list is aligned to a view, given to + * {@link #show(ViewGroup, View, ClickCallback, Board)}. + * This view completely covers the window to catch any input that goes outside the inner list view. + * It also features a search field at the top. The data shown is controlled by + * {@link BoardsMenuPresenter}. + */ +public class BrowseBoardsFloatingMenu extends FrameLayout implements BoardsMenuPresenter.Callback, + Observer { + private static final int MINIMAL_WIDTH_DP = 4 * 56; + private static final int ELEVATION_DP = 4; + private static final int OFFSET_X_DP = 5; + private static final int OFFSET_Y_DP = 5; + private static final int MARGIN_DP = 5; + private static final int ANIMATE_IN_TRANSLATION_Y_DP = 25; + + private View anchor; + private RecyclerView recyclerView; + + private Point position = new Point(0, 0); + private boolean dismissed = false; + + @Inject + private BoardsMenuPresenter presenter; + private BoardsMenuPresenter.Items items; -public class BrowseBoardsFloatingMenu implements Observer, AdapterView.OnItemClickListener { - private FloatingMenu floatingMenu; - private BoardManager.SavedBoards savedBoards; private BrowseBoardsAdapter adapter; - private Callback callback; + private ClickCallback clickCallback; + private ViewTreeObserver.OnGlobalLayoutListener layoutListener; - public BrowseBoardsFloatingMenu(BoardManager.SavedBoards savedBoards) { - this.savedBoards = savedBoards; - this.savedBoards.addObserver(this); + public BrowseBoardsFloatingMenu(Context context) { + this(context, null); } - private void onDismissed() { - savedBoards.deleteObserver(this); + public BrowseBoardsFloatingMenu(Context context, AttributeSet attrs) { + this(context, attrs, 0); } - public void setCallback(Callback callback) { - this.callback = callback; + public BrowseBoardsFloatingMenu(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + inject(this); + + layoutListener = this::repositionToAnchor; + + setFocusableInTouchMode(true); + setFocusable(true); } - public void show(View anchor, Board selectedBoard) { - floatingMenu = new FloatingMenu(anchor.getContext()); - floatingMenu.setCallback(new FloatingMenu.FloatingMenuCallbackAdapter() { - @Override - public void onFloatingMenuDismissed(FloatingMenu menu) { - onDismissed(); - } - }); - floatingMenu.setManageItems(false); - floatingMenu.setAnchor(anchor, Gravity.LEFT, dp(5), dp(5)); - floatingMenu.setPopupWidth(FloatingMenu.POPUP_WIDTH_ANCHOR); + public void show(ViewGroup baseView, View anchor, ClickCallback clickCallback, + Board selectedBoard) { + this.anchor = anchor; + this.clickCallback = clickCallback; + + ViewGroup rootView = baseView.getRootView().findViewById(android.R.id.content); + + setupChildViews(); + adapter = new BrowseBoardsAdapter(); - floatingMenu.setAdapter(adapter); - floatingMenu.setOnItemClickListener(this); - floatingMenu.setSelectedPosition(resolveCurrentIndex(selectedBoard)); - floatingMenu.show(); + + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + recyclerView.setItemAnimator(null); + + rootView.addView(this, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + requestFocus(); + + watchAnchor(); + + animateIn(); + + presenter.create(this, selectedBoard); + items = presenter.items(); + items.addObserver(this); } @Override public void update(Observable o, Object arg) { - if (o == savedBoards) { + if (o == presenter.items()) { adapter.notifyDataSetChanged(); } } @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Pair siteOrBoard = getAtPosition(position); - if (siteOrBoard.second != null) { - callback.onBoardClicked(siteOrBoard.second); + public void scrollToPosition(int position) { + recyclerView.scrollToPosition(position); + } + + private void itemClicked(Site site, Board board) { + if (!isInteractive()) return; + + if (board != null) { + clickCallback.onBoardClicked(board); } else { - callback.onSiteClicked(siteOrBoard.first); + clickCallback.onSiteClicked(site); } - floatingMenu.dismiss(); + dismiss(); + } + + private void inputChanged(String input) { + presenter.filterChanged(input); } - private int getCount() { - int count = 0; - for (Pair> siteListPair : savedBoards.get()) { - count += 1; - count += siteListPair.second.size(); + private void dismiss() { + if (dismissed) return; + dismissed = true; + + items.deleteObserver(this); + presenter.destroy(); + + AndroidUtils.hideKeyboard(this); + + // ??? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + anchor.getViewTreeObserver().removeOnGlobalLayoutListener(layoutListener); } - return count; + animateOut(() -> removeFromParentView(this)); } - private int resolveCurrentIndex(Board board) { - int position = 0; - for (Pair> siteListPair : savedBoards.get()) { - position += 1; + private void setupChildViews() { + // View creation + recyclerView = new RecyclerView(getContext()); - for (Board other : siteListPair.second) { - if (board == other) { - return position; - } - position++; - } + // View setup + recyclerView.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + recyclerView.setElevation(dp(ELEVATION_DP)); } - return 0; + // View attaching + int recyclerWidth = Math.max( + anchor.getWidth(), + dp(MINIMAL_WIDTH_DP)); + + LayoutParams params = new LayoutParams( + recyclerWidth, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + params.leftMargin = dp(MARGIN_DP); + params.topMargin = dp(MARGIN_DP); + params.rightMargin = dp(MARGIN_DP); + params.bottomMargin = dp(MARGIN_DP); + addView(recyclerView, params); } - private Pair getAtPosition(int position) { - for (Pair> siteListPair : savedBoards.get()) { - if (position == 0) { - return new Pair<>(siteListPair.first, null); - } - position -= 1; + private void watchAnchor() { + repositionToAnchor(); + anchor.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener); + } - if (position < siteListPair.second.size()) { - return new Pair<>(null, siteListPair.second.get(position)); - } - position -= siteListPair.second.size(); + private void repositionToAnchor() { + int[] anchorPos = new int[2]; + int[] recyclerViewPos = new int[2]; + anchor.getLocationInWindow(anchorPos); + recyclerView.getLocationInWindow(recyclerViewPos); + anchorPos[0] += dp(OFFSET_X_DP); + anchorPos[1] += dp(OFFSET_Y_DP); + recyclerViewPos[0] += -recyclerView.getTranslationX() - getTranslationX(); + recyclerViewPos[1] += -recyclerView.getTranslationY() - getTranslationY(); + + int x = anchorPos[0] - recyclerViewPos[0]; + int y = anchorPos[1] - recyclerViewPos[1]; + + if (!position.equals(x, y)) { + position.set(x, y); + + recyclerView.setTranslationX(x); + recyclerView.setTranslationY(y); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (isInteractive() && keyCode == KeyEvent.KEYCODE_BACK) { + event.startTracking(); + return true; } - throw new IllegalArgumentException(); + return super.onKeyDown(keyCode, event); } - private class BrowseBoardsAdapter extends BaseAdapter { - final int TYPE_SITE = 0; - final int TYPE_BOARD = 1; + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isInteractive() && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && + !event.isCanceled()) { + dismiss(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isInteractive()) return super.onTouchEvent(event); + + dismiss(); + return true; + } + + private boolean isInteractive() { + return !dismissed; + } + + private void animateIn() { + setAlpha(0f); + setTranslationY(-dp(ANIMATE_IN_TRANSLATION_Y_DP)); + post(() -> { + animate() + .alpha(1f) + .translationY(0f) + .setInterpolator(new DecelerateInterpolator(2f)) + .setDuration(250).start(); + }); + } + + private void animateOut(Runnable done) { + animate().alpha(0f) + .setInterpolator(new DecelerateInterpolator(2f)).setDuration(250) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + done.run(); + } + }) + .start(); + } + + private class BrowseBoardsAdapter extends RecyclerView.Adapter { + public BrowseBoardsAdapter() { + setHasStableIds(true); + } @Override - public int getViewTypeCount() { - return 2; + public int getItemCount() { + return items.getCount(); + } + + @Override + public long getItemId(int position) { + Item item = items.getAtPosition(position); + return item.id; } @Override public int getItemViewType(int position) { - Pair siteOrBoard = getAtPosition(position); - if (siteOrBoard.first != null) { - return TYPE_SITE; - } else if (siteOrBoard.second != null) { - return TYPE_BOARD; + Item item = items.getAtPosition(position); + return item.type.typeId; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + if (viewType == Item.Type.SEARCH.typeId) { + return new InputViewHolder(inflater.inflate( + R.layout.cell_browse_input, parent, false)); + } else if (viewType == Item.Type.SITE.typeId) { + return new SiteViewHolder(inflater.inflate( + R.layout.cell_browse_site, parent, false)); + } else if (viewType == Item.Type.BOARD.typeId) { + return new BoardViewHolder(inflater.inflate( + R.layout.cell_browse_board, parent, false)); } else { throw new IllegalArgumentException(); } } @Override - public int getCount() { - return BrowseBoardsFloatingMenu.this.getCount(); + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Item item = items.getAtPosition(position); + if (holder instanceof InputViewHolder) { + InputViewHolder inputViewHolder = ((InputViewHolder) holder); + } else if (holder instanceof SiteViewHolder) { + SiteViewHolder siteViewHolder = ((SiteViewHolder) holder); + siteViewHolder.bind(item.site); + } else if (holder instanceof BoardViewHolder) { + BoardViewHolder boardViewHolder = ((BoardViewHolder) holder); + boardViewHolder.bind(item.board); + } else { + throw new IllegalArgumentException(); + } + } + } + + private class InputViewHolder extends ViewHolder implements TextWatcher, + OnFocusChangeListener, OnClickListener, OnKeyListener { + private EditText input; + + public InputViewHolder(View itemView) { + super(itemView); + + input = itemView.findViewById(R.id.input); + input.addTextChangedListener(this); + input.setOnFocusChangeListener(this); + input.setOnClickListener(this); + input.setOnKeyListener(this); } @Override - public Object getItem(int position) { - return null; + public void afterTextChanged(Editable s) { + inputChanged(input.getText().toString()); } @Override - public long getItemId(int position) { - return position; + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { } - @SuppressLint("ViewHolder") @Override - public View getView(int position, View view, ViewGroup parent) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus) { + AndroidUtils.hideKeyboard(v); + } + } - Pair siteOrBoard = getAtPosition(position); - if (siteOrBoard.first != null) { - Site site = siteOrBoard.first; + @Override + public void onClick(View v) { + ((LinearLayoutManager) recyclerView.getLayoutManager()) + .scrollToPositionWithOffset(0, 0); + } - if (view == null) { - view = inflater.inflate(R.layout.cell_browse_site, parent, false); - } + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismiss(); + } + return true; + } + } - View divider = view.findViewById(R.id.divider); - final ImageView image = view.findViewById(R.id.image); - TextView text = view.findViewById(R.id.text); + private class SiteViewHolder extends ViewHolder { + View divider; + ImageView image; + TextView text; - divider.setVisibility(position == 0 ? View.GONE : View.VISIBLE); + Site site; + SiteIcon icon; - final SiteIcon icon = site.icon(); - image.setTag(icon); + public SiteViewHolder(View itemView) { + super(itemView); - icon.get(new SiteIcon.SiteIconResult() { - @Override - public void onSiteIcon(SiteIcon siteIcon, Drawable drawable) { - if (image.getTag() == icon) { - image.setImageDrawable(drawable); - } - } - }); + itemView.setOnClickListener((v) -> itemClicked(site, null)); - text.setTypeface(AndroidUtils.ROBOTO_MEDIUM); - text.setText(site.name()); + // View binding + divider = itemView.findViewById(R.id.divider); + image = itemView.findViewById(R.id.image); + text = itemView.findViewById(R.id.text); - return view; - } else { - Board board = siteOrBoard.second; + // View setup + text.setTypeface(AndroidUtils.ROBOTO_MEDIUM); + } - if (view == null) { - view = inflater.inflate(R.layout.cell_browse_board, parent, false); + public void bind(Site site) { + this.site = site; + + divider.setVisibility(getAdapterPosition() == 0 ? View.GONE : View.VISIBLE); + + icon = site.icon(); + + image.setTag(icon); + icon.get((siteIcon, drawable) -> { + if (image.getTag() == siteIcon) { + image.setImageDrawable(drawable); } + }); - TextView text = (TextView) view; + text.setText(site.name()); + } + } - text.setTypeface(AndroidUtils.ROBOTO_MEDIUM); - text.setText(BoardHelper.getName(board)); + private class BoardViewHolder extends ViewHolder { + TextView text; - return text; - } + Board board; + + public BoardViewHolder(View itemView) { + super(itemView); + + itemView.setOnClickListener((v) -> itemClicked(null, board)); + + // View binding + text = (TextView) itemView; + + // View setup + text.setTypeface(AndroidUtils.ROBOTO_MEDIUM); + } + + public void bind(Board board) { + this.board = board; + text.setText(BoardHelper.getName(board)); } } - public interface Callback { + public interface ClickCallback { void onBoardClicked(Board item); void onSiteClicked(Site site); 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 16e2eb68..abd5581b 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 @@ -25,6 +25,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.AttributeSet; @@ -458,81 +459,90 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { private LinearLayout createNavigationItemView(final NavigationItem item) { if (item.search) { - SearchLayout searchLayout = new SearchLayout(getContext()); + return createSearchLayout(item); + } else { + return createNavigationLayout(item); + } + } - searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() { - @Override - public void onSearchEntered(String entered) { - item.searchText = entered; - callback.onSearchEntered(item, entered); - } - }); + @NonNull + private LinearLayout createNavigationLayout(NavigationItem item) { + @SuppressLint("InflateParams") + LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null); + menu.setGravity(Gravity.CENTER_VERTICAL); - if (item.searchText != null) { - searchLayout.setText(item.searchText); - } + FrameLayout titleContainer = menu.findViewById(R.id.title_container); - searchLayout.setHint(callback.getSearchHint(item)); + final TextView titleView = menu.findViewById(R.id.title); + titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); + titleView.setText(item.title); + titleView.setTextColor(0xffffffff); - if (openKeyboardAfterSearchViewCreated) { - openKeyboardAfterSearchViewCreated = false; - searchLayout.openKeyboard(); - } - - searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom()); + if (item.middleMenu != null) { + int arrowColor = getAttrColor(getContext(), R.attr.dropdown_light_color); + int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color); + Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor); + titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); + titleView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + item.middleMenu.show(titleView); + } + }); + } - return searchLayout; + TextView subtitleView = menu.findViewById(R.id.subtitle); + if (!TextUtils.isEmpty(item.subtitle)) { + ViewGroup.LayoutParams titleParams = titleView.getLayoutParams(); + titleParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + titleView.setLayoutParams(titleParams); + subtitleView.setText(item.subtitle); + subtitleView.setTextColor(0xffffffff); + titleView.setPadding(titleView.getPaddingLeft(), dp(5f), titleView.getPaddingRight(), titleView.getPaddingBottom()); } else { - @SuppressLint("InflateParams") - LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null); - menu.setGravity(Gravity.CENTER_VERTICAL); + titleContainer.removeView(subtitleView); + } - FrameLayout titleContainer = menu.findViewById(R.id.title_container); + if (item.rightView != null) { + removeFromParentView(item.rightView); + item.rightView.setPadding(0, 0, dp(16), 0); + menu.addView(item.rightView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + } - final TextView titleView = menu.findViewById(R.id.title); - titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); - titleView.setText(item.title); - titleView.setTextColor(0xffffffff); + if (item.menu != null) { + removeFromParentView(item.menu); + menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + } - if (item.middleMenu != null) { - int arrowColor = getAttrColor(getContext(), R.attr.dropdown_light_color); - int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color); - Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor); - titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); + return menu; + } - titleView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - item.middleMenu.show(titleView); - } - }); - } + @NonNull + private LinearLayout createSearchLayout(NavigationItem item) { + SearchLayout searchLayout = new SearchLayout(getContext()); - TextView subtitleView = menu.findViewById(R.id.subtitle); - if (!TextUtils.isEmpty(item.subtitle)) { - ViewGroup.LayoutParams titleParams = titleView.getLayoutParams(); - titleParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; - titleView.setLayoutParams(titleParams); - subtitleView.setText(item.subtitle); - subtitleView.setTextColor(0xffffffff); - titleView.setPadding(titleView.getPaddingLeft(), dp(5f), titleView.getPaddingRight(), titleView.getPaddingBottom()); - } else { - titleContainer.removeView(subtitleView); + searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() { + @Override + public void onSearchEntered(String entered) { + item.searchText = entered; + callback.onSearchEntered(item, entered); } + }); - if (item.rightView != null) { - removeFromParentView(item.rightView); - item.rightView.setPadding(0, 0, dp(16), 0); - menu.addView(item.rightView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); - } + if (item.searchText != null) { + searchLayout.setText(item.searchText); + } - if (item.menu != null) { - removeFromParentView(item.menu); - menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); - } + searchLayout.setHint(callback.getSearchHint(item)); - return menu; + if (openKeyboardAfterSearchViewCreated) { + openKeyboardAfterSearchViewCreated = false; + searchLayout.openKeyboard(); } + + searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom()); + + return searchLayout; } private void animateArrow(boolean toArrow) { @@ -541,12 +551,8 @@ public class Toolbar extends LinearLayout implements View.OnClickListener { 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.addUpdateListener(animation -> + setArrowMenuProgress((float) animation.getAnimatedValue())); arrowAnimation.start(); } } diff --git a/Clover/app/src/main/res/layout/cell_browse_board.xml b/Clover/app/src/main/res/layout/cell_browse_board.xml index ab107a76..d4d25fed 100644 --- a/Clover/app/src/main/res/layout/cell_browse_board.xml +++ b/Clover/app/src/main/res/layout/cell_browse_board.xml @@ -18,6 +18,7 @@ along with this program. If not, see . . android:layout_height="match_parent" android:ellipsize="end" android:gravity="center_vertical" + android:hint="@string/thread_boards_search_hint" android:inputType="text" android:maxLines="1" android:paddingLeft="16dp" diff --git a/Clover/app/src/main/res/layout/cell_browse_site.xml b/Clover/app/src/main/res/layout/cell_browse_site.xml index b20379cd..53a88248 100644 --- a/Clover/app/src/main/res/layout/cell_browse_site.xml +++ b/Clover/app/src/main/res/layout/cell_browse_site.xml @@ -19,6 +19,7 @@ along with this program. If not, see . xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" android:orientation="horizontal" tools:ignore="RtlHardcoded"> diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 9771ab97..6b9b7f6b 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -158,7 +158,7 @@ Re-enable this permission in the app settings if you permanently disabled it."Failed to open image Spoiler image - Add more… + Search HTTPS error Network error API parse error