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 9d9f7c49c8
commit 0578e0e64d
5 changed files with 583 additions and 0 deletions

View file

@ -0,0 +1,55 @@
// 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 org.schabi.newpipe.extractor.StreamingService;
import java.util.ArrayList;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
/**
* A search filter dialog that also looks like a dialog aka. 'dialog style'.
*/
public class SearchFilterDialogFragment extends BaseSearchFilterDialogFragment {
private SearchFilterDialogFragmentBinding binding;
public static DialogFragment newInstance(
final int serviceId,
final ArrayList<Integer> userSelectedContentFilter,
final ArrayList<Integer> userSelectedSortFilter) {
return initDialogArguments(
new SearchFilterDialogFragment(),
serviceId,
userSelectedContentFilter,
userSelectedSortFilter);
}
@Override
protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator(
final StreamingService service,
final SearchFilterLogic.Callback callback) {
return new SearchFilterDialogGenerator(service,
binding.verticalScroll, requireContext(), callback);
}
@Override
protected Toolbar getToolbar() {
return binding.toolbarLayout.toolbar;
}
@Override
protected View getRootView(final LayoutInflater inflater,
final ViewGroup container) {
binding = SearchFilterDialogFragmentBinding
.inflate(inflater, container, false);
return binding.getRoot();
}
}

View file

@ -0,0 +1,272 @@
// 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.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.StreamingService;
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.HashMap;
import java.util.List;
import java.util.Map;
import androidx.appcompat.view.ContextThemeWrapper;
public class SearchFilterDialogGenerator extends BaseSearchFilterUiDialogGenerator {
private final GridLayout globalLayout;
private final Map<Spinner, AdapterView.OnItemSelectedListener> spinners = new HashMap<>();
public SearchFilterDialogGenerator(final StreamingService service,
final ViewGroup root,
final Context context,
final SearchFilterLogic.Callback callback) {
super(service.getSearchQHFactory(), callback, context);
this.globalLayout = createGridLayout();
root.addView(globalLayout);
}
@Override
public void onResume() {
for (final Map.Entry<Spinner, AdapterView.OnItemSelectedListener> spinner
: spinners.entrySet()) {
spinner.getKey().setOnItemSelectedListener(spinner.getValue());
}
super.onResume();
}
@Override
public void onPause() {
for (final Map.Entry<Spinner, AdapterView.OnItemSelectedListener> spinner
: spinners.entrySet()) {
spinner.getKey().setOnItemSelectedListener(null);
}
super.onPause();
}
@Override
protected void createTitle(final String name,
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(final FilterGroup filterGroup,
final UiWrapperMapDelegate wrapperDelegate,
final UiSelectorDelegate selectorDelegate) {
final GridLayout.LayoutParams layoutParams = getLayoutParamsViews();
boolean doSpanDataOverMultipleCells = false;
final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews(
filterGroup.getIdentifier());
if (filterGroup.getName() != null) {
final TextView filterLabel = new TextView(context);
filterLabel.setId(filterGroup.getIdentifier());
filterLabel.setText(
ServiceHelper.getTranslatedFilterString(filterGroup.getName(), context));
filterLabel.setGravity(Gravity.CENTER_VERTICAL);
setDefaultMargin(layoutParams);
setZeroPadding(filterLabel);
filterLabel.setLayoutParams(layoutParams);
globalLayout.addView(filterLabel);
viewsWrapper.add(filterLabel);
} else {
doSpanDataOverMultipleCells = true;
}
if (filterGroup.isOnlyOneCheckable()) {
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);
viewsWrapper.add(chipGroup);
globalLayout.addView(chipGroup);
chipGroup.setLayoutParams(
clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells));
chipGroup.setSingleLine(false);
createUiElementsForMultipleSelectableItemsFilterGroup(
filterGroup, wrapperDelegate, selectorDelegate, chipGroup);
}
wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper);
}
private void createUiElementsForSingleSelectableItemsFilterGroup(
final FilterGroup filterGroup,
final UiWrapperMapDelegate wrapperDelegate,
final UiSelectorDelegate selectorDelegate,
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);
spinners.put(filterDataSpinner, listener);
}
private void createUiElementsForMultipleSelectableItemsFilterGroup(
final FilterGroup filterGroup,
final UiWrapperMapDelegate wrapperDelegate,
final UiSelectorDelegate selectorDelegate,
final ChipGroup chipGroup) {
for (final FilterItem item : filterGroup.getFilterItems()) {
final Chip chip = new Chip(new ContextThemeWrapper(
context, R.style.Theme_MaterialComponents_Light));
chip.setText(ServiceHelper.getTranslatedFilterString(item.getName(), context));
chip.setId(item.getIdentifier());
chip.setCheckable(true);
final View.OnClickListener listener;
listener = view -> selectorDelegate.selectFilter(view.getId());
chip.setOnClickListener(listener);
chipGroup.addView(chip);
viewListeners.put(chip, listener);
wrapperDelegate.put(item.getIdentifier(), new UiItemWrapperChip(
item, chip, chipGroup));
}
}
private View createSeparatorLine() {
return createSeparatorLine(clipFreeRightColumnLayoutParams(true));
}
private TextView createTitleText(final String name) {
final TextView title = createTitleText(name,
clipFreeRightColumnLayoutParams(true));
title.setGravity(Gravity.CENTER);
return title;
}
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;
}
private 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;
}
private GridLayout.LayoutParams getLayoutParamsViews() {
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.setGravity(Gravity.TOP);
setDefaultMargin(layoutParams);
return layoutParams;
}
private GridLayout.LayoutParams setDefaultMargin(final GridLayout.LayoutParams layoutParams) {
layoutParams.setMargins(
DeviceUtils.dpToPx(4, context),
DeviceUtils.dpToPx(2, context),
DeviceUtils.dpToPx(4, context),
DeviceUtils.dpToPx(2, context)
);
return layoutParams;
}
private View setZeroPadding(final View view) {
view.setPadding(0, 0, 0, 0);
return view;
}
public static class UiItemWrapperChip extends BaseUiItemWrapper {
private final ChipGroup chipGroup;
public UiItemWrapperChip(final FilterItem item,
final View view,
final ChipGroup chipGroup) {
super(item, view);
this.chipGroup = chipGroup;
}
@Override
public boolean isChecked() {
return ((Chip) view).isChecked();
}
@Override
public void setChecked(final boolean checked) {
view.setSelected(checked);
((Chip) view).setChecked(checked);
if (checked) {
chipGroup.check(view.getId());
}
}
}
}

View file

@ -0,0 +1,218 @@
// 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.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.HashMap;
import java.util.Map;
import java.util.Objects;
public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
private final Context context;
private final FilterGroup group;
private final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate;
private final Spinner spinner;
private final Map<Integer, Integer> id2PosMap = new HashMap<>();
private final Map<Integer, UiItemWrapperSpinner>
viewWrapperMap = new HashMap<>();
public SearchFilterDialogSpinnerAdapter(
final Context context,
final FilterGroup group,
final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate,
final Spinner filterDataSpinner) {
this.context = context;
this.group = group;
this.wrapperDelegate = wrapperDelegate;
this.spinner = filterDataSpinner;
createViewWrappers();
}
@Override
public int getCount() {
return group.getFilterItems().length;
}
@Override
public Object getItem(final int position) {
return group.getFilterItems()[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()[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.getName(), context));
view.setVisibility(wrappedView.getVisibility());
view.setEnabled(wrappedView.isEnabled());
if (item instanceof FilterItem.DividerItem) {
wrappedView.setEnabled(false);
view.setEnabled(wrappedView.isEnabled());
final String menuDividerTitle = ">>>"
+ ServiceHelper.getTranslatedFilterString(item.getName(), context) + "<<<";
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 FilterItem.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++;
}
}
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.nonNull(wrappedView);
return wrappedView.isEnabled();
}
private static class UiItemWrapperSpinner
extends BaseItemWrapper {
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(final FilterItem item,
final int initialVisibility,
final boolean isInitialEnabled,
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,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="match_parent" >
<include
android:id="@+id/toolbar_layout"
layout="@layout/toolbar_layout" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
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>