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. 476
      Clover/app/src/main/java/org/floens/chan/ui/layout/BrowseBoardsFloatingMenu.java
  9. 136
      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 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<Board> ORDER_SORT = (lhs, rhs) -> lhs.order - rhs.order;
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 List<Pair<Site, List<Board>>> sitesWithBoards = new ArrayList<>();
private final List<Pair<Site, List<Board>>> sitesWithSavedBoards = new ArrayList<>();
@Inject
public BoardManager(DatabaseManager databaseManager) {
this.databaseManager = databaseManager;
databaseBoardManager = databaseManager.getDatabaseBoardManager();
updateSavedBoardsAndNotify();
updateObservables();
}
public void createAll(List<Board> 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<Board> getSiteBoards(Site site) {
return databaseManager.runTask(
databaseManager.getDatabaseBoardManager().getSiteBoards(site));
List<Board> boards = databaseManager.runTask(databaseBoardManager.getSiteBoards(site));
Collections.sort(boards, ORDER_SORT);
return boards;
}
public List<Board> getSiteSavedBoards(Site site) {
List<Board> boards = databaseManager.runTask(
databaseManager.getDatabaseBoardManager().getSiteSavedBoards(site));
List<Board> 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<Board> 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<Board> 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<Board> siteBoards = getSiteSavedBoards(site);
sitesWithSavedBoards.add(new Pair<>(site, siteBoards));
List<Board> all = getSiteBoards(site);
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();
}
public class AllBoards extends Observable {
private void doNotify() {
setChanged();
notifyObservers();
}
public List<Pair<Site, List<Board>>> get() {
return sitesWithBoards;
}
}
public class SavedBoards extends Observable {
private void doNotify() {
setChanged();

@ -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}.<br>
* {@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);
}
public BoardManager.SavedBoards getSavedBoardsObservable() {
return boardManager.getSavedBoardsObservable();
}
@Override
public void update(Observable o, Object arg) {
if (o == savedBoardsObservable) {

@ -132,7 +132,7 @@ public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
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));
}

@ -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) {

@ -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<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) {
List<Pair<Board, Integer>> ratios = new ArrayList<>();
for (Board board : from) {

@ -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<Site, Board> 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<Site, List<Board>> 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<Site, List<Board>> 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<Site, Board> getAtPosition(int position) {
for (Pair<Site, List<Board>> 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<ViewHolder> {
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<Site, Board> 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<Site, Board> 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);

@ -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();
}
}

@ -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"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:ellipsize="end"
android:gravity="center_vertical"
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:ellipsize="end"
android:gravity="center_vertical"
android:hint="@string/thread_boards_search_hint"
android:inputType="text"
android:maxLines="1"
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal"
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_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_network">Network error</string>
<string name="thread_load_failed_parsing">API parse error</string>

Loading…
Cancel
Save