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.
refactor-toolbar
Floens 7 years ago
parent 4d32b80866
commit 84fabd4809
  1. 67
      Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
  2. 4
      Clover/app/src/main/java/org/floens/chan/core/model/orm/Board.java
  3. 194
      Clover/app/src/main/java/org/floens/chan/core/presenter/BoardsMenuPresenter.java
  4. 4
      Clover/app/src/main/java/org/floens/chan/core/presenter/BrowsePresenter.java
  5. 2
      Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java
  6. 33
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  7. 26
      Clover/app/src/main/java/org/floens/chan/ui/helper/BoardHelper.java
  8. 458
      Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java
  9. 64
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  10. 1
      Clover/app/src/main/res/layout/cell_browse_board.xml
  11. 1
      Clover/app/src/main/res/layout/cell_browse_input.xml
  12. 1
      Clover/app/src/main/res/layout/cell_browse_site.xml
  13. 2
      Clover/app/src/main/res/values/strings.xml

@ -19,6 +19,7 @@ package org.floens.chan.core.manager;
import android.util.Pair; import android.util.Pair;
import org.floens.chan.core.database.DatabaseBoardManager;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
@ -52,20 +53,24 @@ public class BoardManager {
private static final Comparator<Board> ORDER_SORT = (lhs, rhs) -> lhs.order - rhs.order; private static final Comparator<Board> ORDER_SORT = (lhs, rhs) -> lhs.order - rhs.order;
private final DatabaseManager databaseManager; private final DatabaseManager databaseManager;
private final DatabaseBoardManager databaseBoardManager;
private final List<Pair<Site, List<Board>>> sitesWithSavedBoards = new ArrayList<>(); private final AllBoards allBoardsObservable = new AllBoards();
private final SavedBoards savedBoardsObservable = new SavedBoards(); private final SavedBoards savedBoardsObservable = new SavedBoards();
private final List<Pair<Site, List<Board>>> sitesWithBoards = new ArrayList<>();
private final List<Pair<Site, List<Board>>> sitesWithSavedBoards = new ArrayList<>();
@Inject @Inject
public BoardManager(DatabaseManager databaseManager) { public BoardManager(DatabaseManager databaseManager) {
this.databaseManager = databaseManager; this.databaseManager = databaseManager;
databaseBoardManager = databaseManager.getDatabaseBoardManager();
updateSavedBoardsAndNotify(); updateObservables();
} }
public void createAll(List<Board> boards) { public void createAll(List<Board> boards) {
databaseManager.runTask( databaseManager.runTask(databaseBoardManager.createAll(boards));
databaseManager.getDatabaseBoardManager().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. * @return the board code with the same site and board code, or {@code null} if not found.
*/ */
public Board getBoard(Site site, String code) { public Board getBoard(Site site, String code) {
return databaseManager.runTask( return databaseManager.runTask(databaseBoardManager.getBoard(site, code));
databaseManager.getDatabaseBoardManager().getBoard(site, code));
} }
public List<Board> getSiteBoards(Site site) { public List<Board> getSiteBoards(Site site) {
return databaseManager.runTask( List<Board> boards = databaseManager.runTask(databaseBoardManager.getSiteBoards(site));
databaseManager.getDatabaseBoardManager().getSiteBoards(site)); Collections.sort(boards, ORDER_SORT);
return boards;
} }
public List<Board> getSiteSavedBoards(Site site) { public List<Board> getSiteSavedBoards(Site site) {
List<Board> boards = databaseManager.runTask( List<Board> boards = databaseManager.runTask(databaseBoardManager.getSiteSavedBoards(site));
databaseManager.getDatabaseBoardManager().getSiteSavedBoards(site));
Collections.sort(boards, ORDER_SORT); Collections.sort(boards, ORDER_SORT);
return boards; return boards;
} }
public AllBoards getAllBoardsObservable() {
return allBoardsObservable;
}
public SavedBoards getSavedBoardsObservable() { public SavedBoards getSavedBoardsObservable() {
return savedBoardsObservable; return savedBoardsObservable;
} }
public void updateBoardOrders(List<Board> boards) { public void updateBoardOrders(List<Board> boards) {
databaseManager.runTask(databaseManager.getDatabaseBoardManager() databaseManager.runTask(databaseBoardManager.updateOrders(boards));
.updateOrders(boards)); updateObservables();
updateSavedBoardsAndNotify();
} }
public void setSaved(Board board, boolean saved) { public void setSaved(Board board, boolean saved) {
board.saved = saved; board.saved = saved;
databaseManager.runTask(databaseManager.getDatabaseBoardManager().updateIncludingUserFields(board)); databaseManager.runTask(databaseBoardManager.updateIncludingUserFields(board));
updateSavedBoardsAndNotify(); updateObservables();
} }
public void setAllSaved(List<Board> boards, boolean saved) { public void setAllSaved(List<Board> boards, boolean saved) {
for (Board board : boards) { for (Board board : boards) {
board.saved = saved; board.saved = saved;
} }
databaseManager.runTask(databaseManager.getDatabaseBoardManager().updateIncludingUserFields(boards)); databaseManager.runTask(databaseBoardManager.updateIncludingUserFields(boards));
updateSavedBoardsAndNotify(); updateObservables();
} }
private void updateSavedBoardsAndNotify() { private void updateObservables() {
sitesWithSavedBoards.clear(); sitesWithBoards.clear();
for (Site site : Sites.allSites()) { for (Site site : Sites.allSites()) {
List<Board> siteBoards = getSiteSavedBoards(site); List<Board> all = getSiteBoards(site);
sitesWithSavedBoards.add(new Pair<>(site, siteBoards)); sitesWithBoards.add(new Pair<>(site, all));
List<Board> saved = new ArrayList<>();
for (Board siteBoard : all) {
if (siteBoard.saved) saved.add(siteBoard);
}
sitesWithSavedBoards.add(new Pair<>(site, saved));
} }
allBoardsObservable.doNotify();
savedBoardsObservable.doNotify(); savedBoardsObservable.doNotify();
} }
public class AllBoards extends Observable {
private void doNotify() {
setChanged();
notifyObservers();
}
public List<Pair<Site, List<Board>>> get() {
return sitesWithBoards;
}
}
public class SavedBoards extends Observable { public class SavedBoards extends Observable {
private void doNotify() { private void doNotify() {
setChanged(); setChanged();

@ -170,6 +170,10 @@ public class Board implements SiteReference {
return site; return site;
} }
public boolean siteCodeEquals(Board other) {
return code.equals(other.code) && other.siteId == siteId;
}
/** /**
* Updates the board with data from {@code o}.<br> * Updates the board with data from {@code o}.<br>
* {@link #id}, {@link #saved}, {@link #order} are skipped because these are user-set. * {@link #id}, {@link #saved}, {@link #order} are skipped because these are user-set.

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Item> items = new ArrayList<>();
private int itemIdCounter = 1;
public Items() {
}
public void update(List<Pair<Site, List<Board>>> allBoards, String filter) {
items.clear();
items.add(new Item(0, Item.Type.SEARCH));
for (Pair<Site, List<Board>> siteAndBoards : allBoards) {
Site site = siteAndBoards.first;
List<Board> 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<Board> 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);
}
}

@ -84,10 +84,6 @@ public class BrowsePresenter implements Observer {
callback.loadSiteSetup(site); callback.loadSiteSetup(site);
} }
public BoardManager.SavedBoards getSavedBoardsObservable() {
return boardManager.getSavedBoardsObservable();
}
@Override @Override
public void update(Observable o, Object arg) { public void update(Observable o, Object arg) {
if (o == savedBoardsObservable) { if (o == savedBoardsObservable) {

@ -132,7 +132,7 @@ public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
case TYPE_LINK: case TYPE_LINK:
return new LinkHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_link, parent, false)); return new LinkHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_link, parent, false));
case TYPE_BOARD_INPUT: 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: case TYPE_DIVIDER:
return new DividerHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_divider, parent, false)); return new DividerHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_divider, parent, false));
} }

@ -51,7 +51,11 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.inject; import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.getString; 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 SEARCH_ID = 1;
private static final int REFRESH_ID = 2; private static final int REFRESH_ID = 2;
private static final int REPLY_ID = 101; private static final int REPLY_ID = 101;
@ -151,20 +155,9 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@Override @Override
public void show(View anchor) { public void show(View anchor) {
BrowseBoardsFloatingMenu boardsFloatingMenu = BrowseBoardsFloatingMenu boardsFloatingMenu = new BrowseBoardsFloatingMenu(context);
new BrowseBoardsFloatingMenu(presenter.getSavedBoardsObservable()); boardsFloatingMenu.show(view, anchor, BrowseController.this,
boardsFloatingMenu.show(anchor, presenter.currentBoard()); 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);
}
});
} }
}; };
} }
@ -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() { private void openArchive() {
Board board = presenter.currentBoard(); Board board = presenter.currentBoard();
if (board == null) { if (board == null) {

@ -25,6 +25,7 @@ import org.jsoup.parser.Parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator;
import java.util.List; import java.util.List;
import me.xdrop.fuzzywuzzy.FuzzySearch; import me.xdrop.fuzzywuzzy.FuzzySearch;
@ -40,6 +41,31 @@ public class BoardHelper {
return board.description == null ? null : Parser.unescapeEntities(board.description, false); return board.description == null ? null : Parser.unescapeEntities(board.description, false);
} }
public static List<Board> quickSearch(List<Board> from, String query) {
from = new ArrayList<>(from);
query = query.toLowerCase();
List<Board> res = new ArrayList<>();
for (Iterator<Board> iterator = from.iterator(); iterator.hasNext(); ) {
Board board = iterator.next();
if (board.code.toLowerCase().equals(query)) {
iterator.remove();
res.add(board);
}
}
for (Iterator<Board> 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<Board> search(List<Board> from, final String query) { public static List<Board> search(List<Board> from, final String query) {
List<Pair<Board, Integer>> ratios = new ArrayList<>(); List<Pair<Board, Integer>> ratios = new ArrayList<>();
for (Board board : from) { for (Board board : from) {

@ -17,219 +17,453 @@
*/ */
package org.floens.chan.ui.layout; package org.floens.chan.ui.layout;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable; import android.content.Context;
import android.util.Pair; import android.graphics.Point;
import android.view.Gravity; 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.LayoutInflater;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.view.ViewTreeObserver;
import android.widget.BaseAdapter; import android.view.animation.DecelerateInterpolator;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; 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.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.Site;
import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.ui.helper.BoardHelper; import org.floens.chan.ui.helper.BoardHelper;
import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import java.util.List;
import java.util.Observable; import java.util.Observable;
import java.util.Observer; 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.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 BrowseBoardsAdapter adapter;
private Callback callback; private ClickCallback clickCallback;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener;
public BrowseBoardsFloatingMenu(BoardManager.SavedBoards savedBoards) { public BrowseBoardsFloatingMenu(Context context) {
this.savedBoards = savedBoards; this(context, null);
this.savedBoards.addObserver(this);
} }
private void onDismissed() { public BrowseBoardsFloatingMenu(Context context, AttributeSet attrs) {
savedBoards.deleteObserver(this); this(context, attrs, 0);
} }
public void setCallback(Callback callback) { public BrowseBoardsFloatingMenu(Context context, AttributeSet attrs, int defStyle) {
this.callback = callback; super(context, attrs, defStyle);
}
public void show(View anchor, Board selectedBoard) { inject(this);
floatingMenu = new FloatingMenu(anchor.getContext());
floatingMenu.setCallback(new FloatingMenu.FloatingMenuCallbackAdapter() { layoutListener = this::repositionToAnchor;
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) { setFocusableInTouchMode(true);
onDismissed(); setFocusable(true);
} }
});
floatingMenu.setManageItems(false); public void show(ViewGroup baseView, View anchor, ClickCallback clickCallback,
floatingMenu.setAnchor(anchor, Gravity.LEFT, dp(5), dp(5)); Board selectedBoard) {
floatingMenu.setPopupWidth(FloatingMenu.POPUP_WIDTH_ANCHOR); this.anchor = anchor;
this.clickCallback = clickCallback;
ViewGroup rootView = baseView.getRootView().findViewById(android.R.id.content);
setupChildViews();
adapter = new BrowseBoardsAdapter(); adapter = new BrowseBoardsAdapter();
floatingMenu.setAdapter(adapter);
floatingMenu.setOnItemClickListener(this); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
floatingMenu.setSelectedPosition(resolveCurrentIndex(selectedBoard)); recyclerView.setAdapter(adapter);
floatingMenu.show(); 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 @Override
public void update(Observable o, Object arg) { public void update(Observable o, Object arg) {
if (o == savedBoards) { if (o == presenter.items()) {
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
} }
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void scrollToPosition(int position) {
Pair<Site, Board> siteOrBoard = getAtPosition(position); recyclerView.scrollToPosition(position);
if (siteOrBoard.second != null) { }
callback.onBoardClicked(siteOrBoard.second);
private void itemClicked(Site site, Board board) {
if (!isInteractive()) return;
if (board != null) {
clickCallback.onBoardClicked(board);
} else { } else {
callback.onSiteClicked(siteOrBoard.first); clickCallback.onSiteClicked(site);
} }
floatingMenu.dismiss(); dismiss();
} }
private int getCount() { private void inputChanged(String input) {
int count = 0; presenter.filterChanged(input);
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) {
count += 1;
count += siteListPair.second.size();
} }
return count; 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);
} }
private int resolveCurrentIndex(Board board) { animateOut(() -> removeFromParentView(this));
int position = 0; }
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) {
position += 1; private void setupChildViews() {
// View creation
recyclerView = new RecyclerView(getContext());
for (Board other : siteListPair.second) { // View setup
if (board == other) { recyclerView.setBackgroundColor(AndroidUtils.getAttrColor(getContext(), R.attr.backcolor));
return position; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
recyclerView.setElevation(dp(ELEVATION_DP));
} }
position++;
// 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 void watchAnchor() {
repositionToAnchor();
anchor.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
} }
return 0; 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);
}
} }
private Pair<Site, Board> getAtPosition(int position) { @Override
for (Pair<Site, List<Board>> siteListPair : savedBoards.get()) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (position == 0) { if (isInteractive() && keyCode == KeyEvent.KEYCODE_BACK) {
return new Pair<>(siteListPair.first, null); event.startTracking();
return true;
}
return super.onKeyDown(keyCode, event);
} }
position -= 1;
if (position < siteListPair.second.size()) { @Override
return new Pair<>(null, siteListPair.second.get(position)); public boolean onKeyUp(int keyCode, KeyEvent event) {
if (isInteractive() && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() &&
!event.isCanceled()) {
dismiss();
return true;
} }
position -= siteListPair.second.size(); return super.onKeyUp(keyCode, event);
} }
throw new IllegalArgumentException();
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isInteractive()) return super.onTouchEvent(event);
dismiss();
return true;
} }
private class BrowseBoardsAdapter extends BaseAdapter { private boolean isInteractive() {
final int TYPE_SITE = 0; return !dismissed;
final int TYPE_BOARD = 1; }
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 @Override
public int getViewTypeCount() { public void onAnimationEnd(Animator animation) {
return 2; done.run();
}
})
.start();
}
private class BrowseBoardsAdapter extends RecyclerView.Adapter<ViewHolder> {
public BrowseBoardsAdapter() {
setHasStableIds(true);
}
@Override
public int getItemCount() {
return items.getCount();
}
@Override
public long getItemId(int position) {
Item item = items.getAtPosition(position);
return item.id;
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
Pair<Site, Board> siteOrBoard = getAtPosition(position); Item item = items.getAtPosition(position);
if (siteOrBoard.first != null) { return item.type.typeId;
return TYPE_SITE; }
} else if (siteOrBoard.second != null) {
return TYPE_BOARD; @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 { } else {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
} }
@Override @Override
public int getCount() { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
return BrowseBoardsFloatingMenu.this.getCount(); 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 @Override
public Object getItem(int position) { public void afterTextChanged(Editable s) {
return null; inputChanged(input.getText().toString());
} }
@Override @Override
public long getItemId(int position) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
return position; }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
AndroidUtils.hideKeyboard(v);
}
}
@Override
public void onClick(View v) {
((LinearLayoutManager) recyclerView.getLayoutManager())
.scrollToPositionWithOffset(0, 0);
} }
@SuppressLint("ViewHolder")
@Override @Override
public View getView(int position, View view, ViewGroup parent) { public boolean onKey(View v, int keyCode, KeyEvent event) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (keyCode == KeyEvent.KEYCODE_BACK) {
dismiss();
}
return true;
}
}
private class SiteViewHolder extends ViewHolder {
View divider;
ImageView image;
TextView text;
Site site;
SiteIcon icon;
Pair<Site, Board> siteOrBoard = getAtPosition(position); public SiteViewHolder(View itemView) {
if (siteOrBoard.first != null) { super(itemView);
Site site = siteOrBoard.first;
if (view == null) { itemView.setOnClickListener((v) -> itemClicked(site, null));
view = inflater.inflate(R.layout.cell_browse_site, parent, false);
// View binding
divider = itemView.findViewById(R.id.divider);
image = itemView.findViewById(R.id.image);
text = itemView.findViewById(R.id.text);
// View setup
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
} }
View divider = view.findViewById(R.id.divider); public void bind(Site site) {
final ImageView image = view.findViewById(R.id.image); this.site = site;
TextView text = view.findViewById(R.id.text);
divider.setVisibility(position == 0 ? View.GONE : View.VISIBLE); divider.setVisibility(getAdapterPosition() == 0 ? View.GONE : View.VISIBLE);
final SiteIcon icon = site.icon(); icon = site.icon();
image.setTag(icon);
icon.get(new SiteIcon.SiteIconResult() { image.setTag(icon);
@Override icon.get((siteIcon, drawable) -> {
public void onSiteIcon(SiteIcon siteIcon, Drawable drawable) { if (image.getTag() == siteIcon) {
if (image.getTag() == icon) {
image.setImageDrawable(drawable); image.setImageDrawable(drawable);
} }
}
}); });
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM);
text.setText(site.name()); text.setText(site.name());
}
}
return view; private class BoardViewHolder extends ViewHolder {
} else { TextView text;
Board board = siteOrBoard.second;
if (view == null) { Board board;
view = inflater.inflate(R.layout.cell_browse_board, parent, false);
}
TextView text = (TextView) view; public BoardViewHolder(View itemView) {
super(itemView);
text.setTypeface(AndroidUtils.ROBOTO_MEDIUM); itemView.setOnClickListener((v) -> itemClicked(null, board));
text.setText(BoardHelper.getName(board));
// View binding
text = (TextView) itemView;
return text; // 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 onBoardClicked(Board item);
void onSiteClicked(Site site); void onSiteClicked(Site site);

@ -25,6 +25,7 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -458,31 +459,14 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
private LinearLayout createNavigationItemView(final NavigationItem item) { private LinearLayout createNavigationItemView(final NavigationItem item) {
if (item.search) { if (item.search) {
SearchLayout searchLayout = new SearchLayout(getContext()); return createSearchLayout(item);
} else {
searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() { return createNavigationLayout(item);
@Override
public void onSearchEntered(String entered) {
item.searchText = entered;
callback.onSearchEntered(item, entered);
} }
});
if (item.searchText != null) {
searchLayout.setText(item.searchText);
} }
searchLayout.setHint(callback.getSearchHint(item)); @NonNull
private LinearLayout createNavigationLayout(NavigationItem item) {
if (openKeyboardAfterSearchViewCreated) {
openKeyboardAfterSearchViewCreated = false;
searchLayout.openKeyboard();
}
searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom());
return searchLayout;
} else {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null); LinearLayout menu = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu, null);
menu.setGravity(Gravity.CENTER_VERTICAL); menu.setGravity(Gravity.CENTER_VERTICAL);
@ -499,7 +483,6 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color); int arrowPressedColor = getAttrColor(getContext(), R.attr.dropdown_light_pressed_color);
Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor); Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, arrowColor, arrowPressedColor);
titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
titleView.setOnClickListener(new OnClickListener() { titleView.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -533,6 +516,33 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
return menu; return menu;
} }
@NonNull
private LinearLayout createSearchLayout(NavigationItem item) {
SearchLayout searchLayout = new SearchLayout(getContext());
searchLayout.setCallback(new SearchLayout.SearchLayoutCallback() {
@Override
public void onSearchEntered(String entered) {
item.searchText = entered;
callback.onSearchEntered(item, entered);
}
});
if (item.searchText != null) {
searchLayout.setText(item.searchText);
}
searchLayout.setHint(callback.getSearchHint(item));
if (openKeyboardAfterSearchViewCreated) {
openKeyboardAfterSearchViewCreated = false;
searchLayout.openKeyboard();
}
searchLayout.setPadding(dp(16), searchLayout.getPaddingTop(), searchLayout.getPaddingRight(), searchLayout.getPaddingBottom());
return searchLayout;
} }
private void animateArrow(boolean toArrow) { private void animateArrow(boolean toArrow) {
@ -541,12 +551,8 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to); ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to);
arrowAnimation.setDuration(300); arrowAnimation.setDuration(300);
arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); arrowAnimation.setInterpolator(new DecelerateInterpolator(2f));
arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { arrowAnimation.addUpdateListener(animation ->
@Override setArrowMenuProgress((float) animation.getAnimatedValue()));
public void onAnimationUpdate(ValueAnimator animation) {
setArrowMenuProgress((float) animation.getAnimatedValue());
}
});
arrowAnimation.start(); arrowAnimation.start();
} }
} }

@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:lines="1" android:lines="1"

@ -26,6 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_height="match_parent" android:layout_height="match_parent"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:hint="@string/thread_boards_search_hint"
android:inputType="text" android:inputType="text"
android:maxLines="1" android:maxLines="1"
android:paddingLeft="16dp" android:paddingLeft="16dp"

@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal" android:orientation="horizontal"
tools:ignore="RtlHardcoded"> tools:ignore="RtlHardcoded">

@ -158,7 +158,7 @@ Re-enable this permission in the app settings if you permanently disabled it."</
<string name="image_open_failed">Failed to open image</string> <string name="image_open_failed">Failed to open image</string>
<string name="image_spoiler_filename">Spoiler image</string> <string name="image_spoiler_filename">Spoiler image</string>
<string name="thread_board_select_add">Add more&#8230;</string> <string name="thread_boards_search_hint">Search</string>
<string name="thread_load_failed_ssl">HTTPS error</string> <string name="thread_load_failed_ssl">HTTPS error</string>
<string name="thread_load_failed_network">Network error</string> <string name="thread_load_failed_network">Network error</string>
<string name="thread_load_failed_parsing">API parse error</string> <string name="thread_load_failed_parsing">API parse error</string>

Loading…
Cancel
Save