ankidroid / ankidroid/Anki-Android

Browser chips

Ouverte
#12,554 5 commentaires 2 réactions 0 personnes assignées Voir sur GitHub
Card Browser Keep Open
Langage dominant
Kotlin
Étoiles
11.8k
Forks
2.9k
Merge moyen
2 j 3 h
PR mergées (30 j)
171

Description

This is a brief summary of my attempt to bring in [chips](https://m3.material.io/components/chips/overview) to Browser. These chips behave similarly to chips in Gmail and replicate most of the functionality of the sidebar in Anki.

The implementation is [here](https://github.com/oakkitten/Anki-Android/tree/chips), see the few last commits. It's currently not PR quality, is presumably blocked by converting the card list to `RecyclerView` and might be split into several PRs.

I would love it if someone took a brief look at `WizardAdapter` and `CheckableTreeAdapter` and told me whether they like the general design of the thing.

# Behavior

A single-line chip bar, horizontally scrollable, is placed just below the toolbar. Tapping on a chip opens the according bottom sheet, where you can select one or multiple items. When you select items, search is instantly re-done, and the chip changes its color and label to reflect the selection. For example, `Decks` may change to `Cool deck` if one deck is selected or `Cool deck+1` if two are selected. In the latter case, `Cool deck` would be the one the user selected first.

The toolbar, along with the chip bar and the spinners bellow, is collapsed when the user scrolls down the list of cards.

The bottom sheets show lists of items with icons, text and checkboxes. To select items, you can:
* Tap on the item icon/label. This will select the tapped item and close the dialog. If any other items were selected, those are de-selected. This is a quick way to only select one thing.
* Tap on the checkbox. This will not close dialog, allowing the user select multiple items. To dismiss the dialog, they can swipe the sheep downwards, or tap in the scrim area.

All the bottom sheets have an item such as `All decks` without a checkbox. Tapping on it clears the selection and closes the dialog, effectively stopping filtering by this category.

The bottom sheets can display directory tree-like items. Items that have subitems can be collapsed to hide those and show a clickable chevron to indicate collapsed state (`›`/`⌄`). If there ara any chevrons, all items are shifted slightly to the right to make place for the leftmost chevrons.

If there are many items, a search input is visible. The hint says `Search decks`, or `Search tags`, etc. Tapping on it will expand the sheet to full screen (if enough items). Search input has a clear search button that appears when there is some text. When typing, items that do not contain the input are hidden, with the exception of any parent items, e.g., searching for `foo` would change

* abc
* def * def
* hello to * hello
* foo * foo
* bar * foo fighters
* foo fighters

The bottom sheet survives rotation, preserving selection & scroll position.

### Going forward

I would love to have a list of recent searches, and for the saved searches be more accessible. I imagine this could be done by *not* showing cards when opening the browser, but instead of showing something like this:

[ Toolbar & chips, etc. ]

Show all cards in (current deck)
Show all cards in all decks

Recent searches {not clickable, 5 recent items}
Search A [chip1] [chip2] 🡔
Search B [chip3] 🡔
... 🡔
Show more {shows, say, 20 recent items}

Saved searches {not clickable}
Saved search A 🡔
Saved search B 🡔

### Caveats & considerations

* I am not entirely sure about the “quick way to only select one thing” behavior. I like it myself, but maybe some user can find it confusing, Maybe simply toggle the checkbox when user taps on the label?

* Different categories (Decks, Tags) are searched with `AND`, and items within categories with `OR`. That is, if you select two decks and two tags, the search will be `(deck1 OR deck2) AND (tag1 OR tag2)`. I think this is very not confusing. However, by clicking with Ctrl and Alt modifiers, Anki allows adding a single term with an `AND` and negating it. That is, if you select one deck an Ctrl-Alt click on another one, you will get `deck1 AND NOT deck2`.

I don't see a way for our interface to support that. How should the checkboxes look in such case, and what should the chip say? However, in perspective we might offer long-tapping on bottom sheet items, which would show a menu that would allow adding the terms with negation and whatnot into the search field, bypassing the chips.

* Selecting a tag in the bottom sheet will filter by a tag but will not add `tag:tag1` to the input field. This behavior is very fitting for tags, but filtering by `Again` means a search for `rated:1:1`, and this search item has parameters that the user might want to change. Current design does not allow that. This can be remedied via the above.

* Following [this lively questionnaire](https://forums.ankiweb.net/t/do-you-use-these-filters-in-browser-and-why/23518), I think it would be nice to rename `Today` to `Time`, make `Due` find *all* due cards, and remove `Reschedule` & `Overdue`.

* In deck list there's no `Current deck` because why?

# Visuals

Pretty much what you can see from the screenshots. Some notes:

* I think the chip bar along with the spinners below them can have the background color of the toolbar.

* The chips can have transparent background which makes them behave *just* like Gmail chips in the sense that when you tap on them, the ripple effect only happens to the outline. I am not sure that this is the intended effect by Google, but I like how it feels. Can be changed to a more usual behavior.

* When changing the items in the bottom sheet, default `RecyclerView` animations are run with the exception of the chevron animation. Instead of the default crossfade, when item's subitems are collapsed or shown, chevron is rotated 90°.

* I created some new icons that you can see in the screenshots.
* A deck is modeled after a real-life deck of cards, and a filtered deck like funnel with an opening shaped like the opening of a deck of cards.
* Also, I made some icons for card states to replace the filled circles. I don't want to use colors because colored icons don't look very well and because the association between the color of the circles in the sidebar and the numbers in the deck browser is just too weak in my opinion. Yes, the colors *are* the same but I don't feel that my mind jumps to “yes, this means ‘new’” when I see a blue circle.
* In tag list, the tag `marked` has a star icon.

* Text gravity depends on layout RTL regardless of text itself. Stuff like menus work this way.

### Caveats & considerations

* I'm yet to figure out the roundness of the corners of the bottom sheet but honestly no roundness looks OK too.

* When the contents of the bottom sheet change height, the bottom sheet height change is not animated. Not sure what would be a good way to solve this.

* In light mode, the bottom sheet may expand to the status bar as well. In some circumstances, due to what I think is a bug in the material library, the status bar icons fail to change their color to the dark ones. While this is annoying, the circumstances are rather rare so this is not a super big deal, and this problem will be masked when we change the color of the status bar to a light one.

* In dark mode, the bottom sheet does not expand to the status bar for some reason.

* Normally, the bottom sheet height is limited by its contents. It would be nice if when the user taps on the search input, the sheet would switch temporarily drop this limit, and keep the search input on top of the screen even if the list has few items. While the bottom sheet API should allow just that, the result is super buggy in many ways. Not sure if there's an easy workaround.

# Implementation

At the moment, the implementation is minimally wired into the app, which is to say I did the absolute minimum amound of changes in the existing code to make it work. The new code is like this:

* `SearchParameters` is a parcelable data class that stores user input and all the filtering information. It makes `String` queries that can be passed to the backend.

* `Chips.kt` has functions that set up and update chips based on changes in `searchParameters`.

* `BottomSheetDialogFragmentFix` is a fix for an issue in `BottomSheetDialogFragment`; in landscape, the peek height of it is too small.

* `PieceOfSheetFragment` (hehe) which is a subclass of ↑ is a base for all bottom sheets. It inflates the layout (with a `RecyclerView`, and with or without the search input) and wires together the views. Its subclasses are responsible to setting up the adapter and reacting to ~~filter changes~~ clicks, etc. *Edit: moved filtering to adapter*

* `WizardAdapter` is a universal modular `RecyclerView` adapter with stable IDs that has what I named wizards. A wizard is a like a sub-adapter; it is responsible for binding *some* of the data to the view, with or without animation, and it knows how to react to clicks, etc. By picking and choosing various wizards, you can compose a new adapter that has behavior from all wizards. A wizard:

* Tells `WizardAdapter` properties of items it can animate;
* Creates an optional `ViewHolderBuddy` object that is attached to every `ViewHolder`. A buddy `bind()`s the properties that the wizard is aware of to the view, or if the change is within what the wizard can animate, `animate()`s them.

*Edit: added `TreeFilteringWitch`; a witch is a component that deals with `WizardAdapter.Item`s like a wizard, but does not participate in view actions.*

* ~~`TreeAdapter` (to be renamed?)~~ `CheckableTreeAdapter` is a final subclass of `WizardAdapter` that selects a few wizards for use with the bottom sheets. ~~Currently it is also responsible for hiding/showing collapsing items; while `CollapseWizard` can animate the chevon and knows which items are collapsed, it can't alter the list itself (to be changed?).~~

`CheckableTreeAdapter` accepts a list of items [like this](https://github.com/oakkitten/Anki-Android/blob/79445c7339f1d120b42e5e9802c51c5519a5a440/AnkiDroid/src/main/java/com/ichi2/anki/browser/CheckableTreeAdapter.kt#L23-L29). This list is not a tree structure. It could be a tree structure, which is a more straightforward choice, but I think that we can get away with flat lists. Consider a list of tags:

foo::bar::baz
zoo
foo

Let's call every string in this list “a chain”; a chain is a `::` separated string, and let's call what's between `::`s a chain link. If we add missing subchains (`foo::bar`), and case-insensitive sort, we get:

foo
foo::bar
foo::bar::baz
zoo

And this is something that we can already show! The `text` would be the leaf chain (`foo`, `bar`), the `indent` would be the number of links in the chain.

This is what `Iterable.toCheckableSourceItems()` and the code around it does; it takes a bunch of rows (to be renamed, row is a chain with the associated ID and icon) and converts them to `CheckableTreeAdapter.SourceItem`. So the path of transformation of data would be like this:

* Get a set of chains from backend
* Add missing subchains and sort
* Convert the list to a list of `Row`s with IDs and icons
* Convert the list of rows to a ~~(filtered)~~ list of `CheckableTreeAdapter.SourceItem`s
* Convert (some of the items of) the list of `CheckableTreeAdapter.SourceItem` to a list of `WizardAdapter.Item`, ready to be shown

All containers in question are (for the purpose of this) deeply immutable.

### Caveats & considerations

At the moment the design is not yet finalized, and I'm not even sure if some the ideas are good. Having a modular adapter seems nice but modularity drastically increases code size; collapsing `CheckableTreeAdapter` and `WizardAdapter` into a single class halves the code. But being modular should allow reusing the adapter in other places of the app, such as deck picker, etc. The above path from a set of chains to the adapter items is perhaps a bit too confusing and can be simplified. Some strings need to be extracted.

If the card list has few items, dragging the empty area does not collaps/uncollapse the toolbar. This is probably a problem with the `ListView`. Wrapping it in `NestedScrollView` doesn't seem to work. I hope that changing it to a `RecyclerView` will resolve the issue.

I didn't implement a chip with note types because I was lazy and because I'm not sure it's useful.

Currently, bottom sheet fragments are aware of the collection and that's where these get the data in sync. I think they should be able to get that data cached. This is simple and this ensures that the adapter has the latest data. However, this means that on view recreation the data can change. For the most part this is not an issue, but tags are strings and don't have intrinsic IDs, and tag position in tag list is used instead. In theory, if for instance the tags change between activity recreations, you can have `RecyclerView` restored to a wrong position. I don't think this can lead to any important issues, but just to be safe the IDs can be somewhat stabilized by keeping a static map of tag to id, or interning strings maybe. (Is string interning fast?)

The collapse state of items is not saved and vanishes on activity recreation, and I don't think it's worth saving it.

The new icons should be optimized.

### Code reuse

If deemed good, the `WizardAdapter` may serve in other parts of the app, such as the deck picker, or notification preferences. It will have to be finalized first, and some of the code should be generalized more yet, e.g., currently the chain collection → deck bottom sheet is tied to bottom sheets and must be untied. This is not hard but is hard to do right. Also, if the bottom sheets are to be reused in different activities, these should be united from `CardBrowser`.

The `WizardAdapter` & pals may be an independent PR.

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.