searchfilters: 1st Ui: default dialog for search content and sort filters

This commit is contained in:
evermind 2022-10-11 15:07:18 +02:00
parent 7c650f6e9d
commit 05ffe276c0
8 changed files with 666 additions and 0 deletions

View File

@ -0,0 +1,41 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.fragments.list.search.filter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.databinding.SearchFilterDialogFragmentBinding;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
/**
* A search filter dialog that also looks like a dialog aka. 'dialog style'.
*/
public class SearchFilterDialogFragment extends BaseSearchFilterDialogFragment {
protected SearchFilterDialogFragmentBinding binding;
@Override
protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator() {
return new SearchFilterDialogGenerator(
searchViewModel.getSearchFilterLogic(), binding.verticalScroll, requireContext());
}
@Override
@Nullable
protected Toolbar getToolbar() {
return binding.toolbarLayout.toolbar;
}
@Override
protected View getRootView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container) {
binding = SearchFilterDialogFragmentBinding
.inflate(inflater, container, false);
return binding.getRoot();
}
}

View File

@ -0,0 +1,337 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.fragments.list.search.filter;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SearchFilterDialogGenerator extends BaseSearchFilterUiDialogGenerator {
private static final int CHIP_GROUP_ELEMENTS_THRESHOLD = 2;
private static final int CHIP_MIN_TOUCH_TARGET_SIZE_DP = 40;
protected final GridLayout globalLayout;
public SearchFilterDialogGenerator(
@NonNull final SearchFilterLogic logic,
@NonNull final ViewGroup root,
@NonNull final Context context) {
super(logic, context);
this.globalLayout = createGridLayout();
root.addView(globalLayout);
}
@Override
protected void createTitle(@NonNull final String name,
@NonNull final List<View> titleViewElements) {
final TextView titleView = createTitleText(name);
final View separatorLine = createSeparatorLine();
final View separatorLine2 = createSeparatorLine();
globalLayout.addView(separatorLine);
globalLayout.addView(titleView);
globalLayout.addView(separatorLine2);
titleViewElements.add(titleView);
titleViewElements.add(separatorLine);
titleViewElements.add(separatorLine2);
}
@Override
protected void createFilterGroup(@NonNull final FilterGroup filterGroup,
@NonNull final UiWrapperMapDelegate wrapperDelegate,
@NonNull final UiSelectorDelegate selectorDelegate) {
final GridLayout.LayoutParams layoutParams = getLayoutParamsViews();
boolean doSpanDataOverMultipleCells = false;
final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews(
filterGroup.getIdentifier());
final TextView filterLabel;
if (filterGroup.getNameId() != null) {
filterLabel = createFilterLabel(filterGroup, layoutParams);
viewsWrapper.add(filterLabel);
} else {
filterLabel = null;
doSpanDataOverMultipleCells = true;
}
if (filterGroup.isOnlyOneCheckable()) {
if (filterLabel != null) {
globalLayout.addView(filterLabel);
}
final Spinner filterDataSpinner = new Spinner(context, Spinner.MODE_DROPDOWN);
final GridLayout.LayoutParams spinnerLp =
clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells);
setDefaultMargin(spinnerLp);
filterDataSpinner.setLayoutParams(spinnerLp);
setZeroPadding(filterDataSpinner);
createUiElementsForSingleSelectableItemsFilterGroup(
filterGroup, wrapperDelegate, selectorDelegate, filterDataSpinner);
viewsWrapper.add(filterDataSpinner);
globalLayout.addView(filterDataSpinner);
} else { // multiple items in FilterGroup selectable
final ChipGroup chipGroup = new ChipGroup(context);
doSpanDataOverMultipleCells = chooseParentViewForFilterLabelAndAdd(
filterGroup, doSpanDataOverMultipleCells, filterLabel, chipGroup);
viewsWrapper.add(chipGroup);
globalLayout.addView(chipGroup);
chipGroup.setLayoutParams(
clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells));
chipGroup.setSingleLine(false);
createUiChipElementsForFilterGroupItems(
filterGroup, wrapperDelegate, selectorDelegate, chipGroup);
}
wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper);
}
@NonNull
protected TextView createFilterLabel(@NonNull final FilterGroup filterGroup,
@NonNull final GridLayout.LayoutParams layoutParams) {
final TextView filterLabel;
filterLabel = new TextView(context);
filterLabel.setId(filterGroup.getIdentifier());
filterLabel.setText(
ServiceHelper.getTranslatedFilterString(filterGroup.getNameId(), context));
filterLabel.setGravity(Gravity.CENTER_VERTICAL);
setDefaultMargin(layoutParams);
setZeroPadding(filterLabel);
filterLabel.setLayoutParams(layoutParams);
return filterLabel;
}
private boolean chooseParentViewForFilterLabelAndAdd(
@NonNull final FilterGroup filterGroup,
final boolean doSpanDataOverMultipleCells,
@Nullable final TextView filterLabel,
@NonNull final ChipGroup possibleParentView) {
boolean spanOverMultipleCells = doSpanDataOverMultipleCells;
if (filterLabel != null) {
// If we have more than CHIP_GROUP_ELEMENTS_THRESHOLD elements to be
// displayed as Chips add its filterLabel as first element to ChipGroup.
// Now the ChipGroup can be spanned over all the cells to use
// the space better.
if (filterGroup.getFilterItems().size() > CHIP_GROUP_ELEMENTS_THRESHOLD) {
possibleParentView.addView(filterLabel);
spanOverMultipleCells = true;
} else {
globalLayout.addView(filterLabel);
}
}
return spanOverMultipleCells;
}
private void createUiElementsForSingleSelectableItemsFilterGroup(
@NonNull final FilterGroup filterGroup,
@NonNull final UiWrapperMapDelegate wrapperDelegate,
@NonNull final UiSelectorDelegate selectorDelegate,
@NonNull final Spinner filterDataSpinner) {
filterDataSpinner.setAdapter(new SearchFilterDialogSpinnerAdapter(
context, filterGroup, wrapperDelegate, filterDataSpinner));
final AdapterView.OnItemSelectedListener listener;
listener = new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(final AdapterView<?> parent, final View view,
final int position, final long id) {
if (view != null) {
selectorDelegate.selectFilter(view.getId());
}
}
@Override
public void onNothingSelected(final AdapterView<?> parent) {
// we are only interested onItemSelected() -> no implementation here
}
};
filterDataSpinner.setOnItemSelectedListener(listener);
}
protected void createUiChipElementsForFilterGroupItems(
@NonNull final FilterGroup filterGroup,
@NonNull final UiWrapperMapDelegate wrapperDelegate,
@NonNull final UiSelectorDelegate selectorDelegate,
@NonNull final ChipGroup chipGroup) {
for (final FilterItem item : filterGroup.getFilterItems()) {
if (item instanceof InjectFilterItem.DividerItem) {
final InjectFilterItem.DividerItem dividerItem =
(InjectFilterItem.DividerItem) item;
// For the width MATCH_PARENT is necessary as this allows the
// dividerLabel to fill one row of ChipGroup exclusively
final ChipGroup.LayoutParams layoutParams = new ChipGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final TextView dividerLabel = createDividerLabel(dividerItem, layoutParams);
chipGroup.addView(dividerLabel);
} else {
final Chip chip = createChipView(chipGroup, item);
final View.OnClickListener listener;
listener = view -> selectorDelegate.selectFilter(view.getId());
chip.setOnClickListener(listener);
chipGroup.addView(chip);
wrapperDelegate.put(item.getIdentifier(),
new UiItemWrapperChip(item, chip, chipGroup));
}
}
}
@NonNull
private Chip createChipView(@NonNull final ChipGroup chipGroup,
@NonNull final FilterItem item) {
final Chip chip = (Chip) LayoutInflater.from(context).inflate(
R.layout.chip_search_filter, chipGroup, false);
chip.ensureAccessibleTouchTarget(
DeviceUtils.dpToPx(CHIP_MIN_TOUCH_TARGET_SIZE_DP, context));
chip.setText(ServiceHelper.getTranslatedFilterString(item.getNameId(), context));
chip.setId(item.getIdentifier());
chip.setCheckable(true);
return chip;
}
@NonNull
private TextView createDividerLabel(
@NonNull final InjectFilterItem.DividerItem dividerItem,
@NonNull final ViewGroup.MarginLayoutParams layoutParams) {
final TextView dividerLabel;
dividerLabel = new TextView(context);
dividerLabel.setEnabled(true);
dividerLabel.setGravity(Gravity.CENTER_VERTICAL);
setDefaultMargin(layoutParams);
dividerLabel.setLayoutParams(layoutParams);
final String menuDividerTitle =
context.getString(dividerItem.getStringResId());
dividerLabel.setText(menuDividerTitle);
return dividerLabel;
}
@NonNull
protected SeparatorLineView createSeparatorLine() {
return createSeparatorLine(clipFreeRightColumnLayoutParams(true));
}
@NonNull
private TextView createTitleText(final String name) {
final TextView title = createTitleText(name,
clipFreeRightColumnLayoutParams(true));
title.setGravity(Gravity.CENTER);
return title;
}
@NonNull
private GridLayout createGridLayout() {
final GridLayout layout = new GridLayout(context);
layout.setColumnCount(2);
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
setDefaultMargin(layoutParams);
layout.setLayoutParams(layoutParams);
return layout;
}
@NonNull
protected GridLayout.LayoutParams clipFreeRightColumnLayoutParams(final boolean doColumnSpan) {
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
// https://stackoverflow.com/questions/37744672/gridlayout-children-are-being-clipped
layoutParams.width = 0;
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
layoutParams.setGravity(Gravity.FILL_HORIZONTAL | Gravity.CENTER_VERTICAL);
setDefaultMargin(layoutParams);
if (doColumnSpan) {
layoutParams.columnSpec = GridLayout.spec(0, 2, 1.0f);
}
return layoutParams;
}
@NonNull
private GridLayout.LayoutParams getLayoutParamsViews() {
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.setGravity(Gravity.CENTER_VERTICAL);
setDefaultMargin(layoutParams);
return layoutParams;
}
@NonNull
protected ViewGroup.MarginLayoutParams setDefaultMargin(
@NonNull final ViewGroup.MarginLayoutParams layoutParams) {
layoutParams.setMargins(
DeviceUtils.dpToPx(4, context),
DeviceUtils.dpToPx(2, context),
DeviceUtils.dpToPx(4, context),
DeviceUtils.dpToPx(2, context)
);
return layoutParams;
}
@NonNull
protected View setZeroPadding(@NonNull final View view) {
view.setPadding(0, 0, 0, 0);
return view;
}
public static class UiItemWrapperChip extends BaseUiItemWrapper {
@NonNull
private final ChipGroup chipGroup;
public UiItemWrapperChip(@NonNull final FilterItem item,
@NonNull final View view,
@NonNull final ChipGroup chipGroup) {
super(item, view);
this.chipGroup = chipGroup;
}
@Override
public boolean isChecked() {
return ((Chip) view).isChecked();
}
@Override
public void setChecked(final boolean checked) {
((Chip) view).setChecked(checked);
if (checked) {
chipGroup.check(view.getId());
}
}
}
}

View File

@ -0,0 +1,224 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.fragments.list.search.filter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import org.schabi.newpipe.extractor.search.filter.FilterContainer;
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.collection.SparseArrayCompat;
import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem;
public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
private final Context context;
private final FilterGroup group;
private final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate;
private final Spinner spinner;
private final SparseIntArray id2PosMap = new SparseIntArray();
private final SparseArrayCompat<UiItemWrapperSpinner>
viewWrapperMap = new SparseArrayCompat<>();
public SearchFilterDialogSpinnerAdapter(
@NonNull final Context context,
@NonNull final FilterGroup group,
@NonNull final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate,
@NonNull final Spinner filterDataSpinner) {
this.context = context;
this.group = group;
this.wrapperDelegate = wrapperDelegate;
this.spinner = filterDataSpinner;
createViewWrappers();
}
@Override
public int getCount() {
return group.getFilterItems().size();
}
@Override
public Object getItem(final int position) {
return group.getFilterItems().get(position);
}
@Override
public long getItemId(final int position) {
return position;
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final FilterItem item = group.getFilterItems().get(position);
final TextView view;
if (convertView != null) {
view = (TextView) convertView;
} else {
view = createViewItem();
}
initViewWithData(position, item, view);
return view;
}
@SuppressLint("WrongConstant")
private void initViewWithData(final int position,
final FilterItem item,
final TextView view) {
final UiItemWrapperSpinner wrappedView =
viewWrapperMap.get(position);
Objects.requireNonNull(wrappedView);
view.setId(item.getIdentifier());
view.setText(ServiceHelper.getTranslatedFilterString(item.getNameId(), context));
view.setVisibility(wrappedView.getVisibility());
view.setEnabled(wrappedView.isEnabled());
if (item instanceof DividerItem) {
final DividerItem dividerItem = (DividerItem) item;
wrappedView.setEnabled(false);
view.setEnabled(wrappedView.isEnabled());
final String menuDividerTitle = ">>>"
+ context.getString(dividerItem.getStringResId()) + "<<<";
view.setText(menuDividerTitle);
}
}
private void createViewWrappers() {
int position = 0;
for (final FilterItem item : this.group.getFilterItems()) {
final int initialVisibility = View.VISIBLE;
final boolean isInitialEnabled = true;
final UiItemWrapperSpinner wrappedView =
new UiItemWrapperSpinner(
item,
initialVisibility,
isInitialEnabled,
spinner);
if (item instanceof DividerItem) {
wrappedView.setEnabled(false);
}
// store wrapper also locally as we refer here regularly
viewWrapperMap.put(position, wrappedView);
// store wrapper globally in SearchFilterLogic
wrapperDelegate.put(item.getIdentifier(), wrappedView);
id2PosMap.put(item.getIdentifier(), position);
position++;
}
}
@NonNull
private TextView createViewItem() {
final TextView view = new TextView(context);
view.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
view.setGravity(Gravity.CENTER_VERTICAL);
view.setPadding(
DeviceUtils.dpToPx(8, context),
DeviceUtils.dpToPx(4, context),
DeviceUtils.dpToPx(8, context),
DeviceUtils.dpToPx(4, context)
);
return view;
}
public int getItemPositionForFilterId(final int id) {
return id2PosMap.get(id);
}
@Override
public boolean isEnabled(final int position) {
final UiItemWrapperSpinner wrappedView =
viewWrapperMap.get(position);
Objects.requireNonNull(wrappedView);
return wrappedView.isEnabled();
}
private static class UiItemWrapperSpinner
extends BaseItemWrapper {
@NonNull
private final Spinner spinner;
/**
* We have to store the visibility of the view and if it is enabled.
* <p>
* Reason: the Spinner adapter reuses {@link View} elements through the parameter
* convertView in {@link SearchFilterDialogSpinnerAdapter#getView(int, View, ViewGroup)}
* -> this is the Android Adapter's time saving characteristic to rather reuse
* than to recreate a {@link View}.
* -> so we reuse what Android gives us in above mentioned method.
*/
private int visibility;
private boolean enabled;
UiItemWrapperSpinner(@NonNull final FilterItem item,
final int initialVisibility,
final boolean isInitialEnabled,
@NonNull final Spinner spinner) {
super(item);
this.spinner = spinner;
this.visibility = initialVisibility;
this.enabled = isInitialEnabled;
}
@Override
public void setVisible(final boolean visible) {
if (visible) {
visibility = View.VISIBLE;
} else {
visibility = View.GONE;
}
}
@Override
public boolean isChecked() {
return spinner.getSelectedItem() == item;
}
@Override
public void setChecked(final boolean checked) {
if (super.getItemId() != FilterContainer.ITEM_IDENTIFIER_UNKNOWN) {
final SearchFilterDialogSpinnerAdapter adapter =
(SearchFilterDialogSpinnerAdapter) spinner.getAdapter();
spinner.setSelection(adapter.getItemPositionForFilterId(super.getItemId()));
}
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(final boolean enabled) {
this.enabled = enabled;
}
public int getVisibility() {
return visibility;
}
public void setVisibility(final int visibility) {
this.visibility = visibility;
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- adding more contrast than @color/mtrl_chip_background_color if a Chip is selected -->
<item android:alpha="0.30" android:color="?attr/colorOnSurface" android:state_enabled="true" android:state_selected="true"/>
<item android:alpha="0.18" android:color="?attr/colorOnSurface" android:state_enabled="true" android:state_checked="true"/>
<!-- 12% of 87% opacity -->
<item android:alpha="0.10" android:color="?attr/colorOnSurface" android:state_enabled="true"/>
<item android:alpha="0.12" android:color="?attr/colorOnSurface"/>
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This is used to inflate a chip with a Material theme, otherwise it would crash -->
<!-- Theme.MaterialComponents.DayNight is used to guarantee auto day/night switching -->
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/ChipSearchFilter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:theme="@style/Theme.MaterialComponents.DayNight" />

View File

@ -0,0 +1,23 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<include
android:id="@+id/toolbar_layout"
layout="@layout/toolbar_layout" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar_layout">
<LinearLayout
android:id="@+id/vertical_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,15 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/reset"
android:title="@string/playback_reset"
android:icon="@drawable/ic_settings_backup_restore"
app:showAsAction="always" />
<item
android:id="@+id/search"
android:title="@string/search"
android:icon="@drawable/ic_search"
app:showAsAction="always" />
</menu>

View File

@ -155,4 +155,12 @@
<style name="RouterActivityThemeDark" parent="Base.RouterActivityThemeDark" />
<!-- custom Chip style for SearchFilterDialogFragment -->
<style name="ChipSearchFilter" parent="Widget.MaterialComponents.Chip.Filter">
<item name="checkedIconEnabled">false</item>
<item name="checkedIcon">@null</item>
<item name="chipBackgroundColor">@color/mtrl_search_filter_chip_background_color</item>
<item name="chipStrokeWidth">0.1dp</item>
</style>
</resources>