Animate filter recycler view changes

pull/996/head
Alex Baker 5 years ago
parent c2c78512dc
commit 1a05278ab0

@ -11,11 +11,6 @@ import android.os.Parcelable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* CustomFilterCriteria allow users to build a custom filter by chaining together criteria
*
* @author Tim Su <tim@todoroo.com>
*/
public abstract class CustomFilterCriterion implements Parcelable { public abstract class CustomFilterCriterion implements Parcelable {
/** /**
@ -54,7 +49,6 @@ public abstract class CustomFilterCriterion implements Parcelable {
// --- parcelable utilities // --- parcelable utilities
public String getName() { public String getName() {
return name; return name;
} }
@ -76,4 +70,42 @@ public abstract class CustomFilterCriterion implements Parcelable {
source.readMap(valuesForNewTasks, getClass().getClassLoader()); source.readMap(valuesForNewTasks, getClass().getClassLoader());
name = source.readString(); name = source.readString();
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CustomFilterCriterion)) {
return false;
}
CustomFilterCriterion that = (CustomFilterCriterion) o;
if (valuesForNewTasks != null
? !valuesForNewTasks.equals(that.valuesForNewTasks)
: that.valuesForNewTasks != null) {
return false;
}
if (identifier != null ? !identifier.equals(that.identifier) : that.identifier != null) {
return false;
}
if (text != null ? !text.equals(that.text) : that.text != null) {
return false;
}
if (sql != null ? !sql.equals(that.sql) : that.sql != null) {
return false;
}
return name != null ? name.equals(that.name) : that.name == null;
}
@Override
public int hashCode() {
int result = valuesForNewTasks != null ? valuesForNewTasks.hashCode() : 0;
result = 31 * result + (identifier != null ? identifier.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
result = 31 * result + (sql != null ? sql.hashCode() : 0);
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
} }

@ -0,0 +1,19 @@
package com.todoroo.astrid.core;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
public class CriterionDiffCallback extends DiffUtil.ItemCallback<CriterionInstance> {
@Override
public boolean areItemsTheSame(
@NonNull CriterionInstance oldItem, @NonNull CriterionInstance newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(
@NonNull CriterionInstance oldItem, @NonNull CriterionInstance newItem) {
return oldItem.equals(newItem);
}
}

@ -10,6 +10,7 @@ import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.api.CustomFilterCriterion; import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.MultipleSelectCriterion; import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.TextInputCriterion; import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.helper.UUIDHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -22,6 +23,27 @@ public class CriterionInstance {
public static final int TYPE_SUBTRACT = 1; public static final int TYPE_SUBTRACT = 1;
public static final int TYPE_INTERSECT = 2; public static final int TYPE_INTERSECT = 2;
public static final int TYPE_UNIVERSE = 3; public static final int TYPE_UNIVERSE = 3;
public CustomFilterCriterion criterion;
public int selectedIndex = -1;
public String selectedText = null;
public int type = TYPE_INTERSECT;
public int end;
public int start;
public int max;
private String id = UUIDHelper.newUUID();
public CriterionInstance() {}
public CriterionInstance(CriterionInstance other) {
id = other.id;
criterion = other.criterion;
selectedIndex = other.selectedIndex;
selectedText = other.selectedText;
type = other.type;
end = other.end;
start = other.start;
max = other.max;
}
public static List<CriterionInstance> fromString( public static List<CriterionInstance> fromString(
FilterCriteriaProvider provider, String criterion) { FilterCriteriaProvider provider, String criterion) {
@ -30,7 +52,6 @@ public class CriterionInstance {
} }
List<CriterionInstance> entries = new ArrayList<>(); List<CriterionInstance> entries = new ArrayList<>();
for (String row : criterion.split("\n")) { for (String row : criterion.split("\n")) {
CriterionInstance entry = new CriterionInstance();
List<String> split = List<String> split =
transform( transform(
Splitter.on(AndroidUtilities.SERIALIZATION_SEPARATOR).splitToList(row), Splitter.on(AndroidUtilities.SERIALIZATION_SEPARATOR).splitToList(row),
@ -40,6 +61,7 @@ public class CriterionInstance {
return Collections.emptyList(); return Collections.emptyList();
} }
CriterionInstance entry = new CriterionInstance();
entry.criterion = provider.getFilterCriteria(split.get(0)); entry.criterion = provider.getFilterCriteria(split.get(0));
String value = split.get(1); String value = split.get(1);
if (entry.criterion instanceof TextInputCriterion) { if (entry.criterion instanceof TextInputCriterion) {
@ -60,23 +82,25 @@ public class CriterionInstance {
return entries; return entries;
} }
/** criteria for this instance */ private static String escape(String item) {
public CustomFilterCriterion criterion; if (item == null) {
return ""; // $NON-NLS-1$
/** which of the entries is selected (MultipleSelect) */ }
public int selectedIndex = -1; return item.replace(
AndroidUtilities.SERIALIZATION_SEPARATOR, AndroidUtilities.SEPARATOR_ESCAPE);
/** text of selection (TextInput) */ }
public String selectedText = null;
/** type of join */
public int type = TYPE_INTERSECT;
public int end; private static String unescape(String item) {
/** statistics for filter count */ if (Strings.isNullOrEmpty(item)) {
public int start; return "";
}
return item.replace(
AndroidUtilities.SEPARATOR_ESCAPE, AndroidUtilities.SERIALIZATION_SEPARATOR);
}
public int max; public String getId() {
return id;
}
public String getTitleFromCriterion() { public String getTitleFromCriterion() {
if (criterion instanceof MultipleSelectCriterion) { if (criterion instanceof MultipleSelectCriterion) {
@ -113,6 +137,56 @@ public class CriterionInstance {
throw new UnsupportedOperationException("Unknown criterion type"); // $NON-NLS-1$ throw new UnsupportedOperationException("Unknown criterion type"); // $NON-NLS-1$
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CriterionInstance)) {
return false;
}
CriterionInstance that = (CriterionInstance) o;
if (selectedIndex != that.selectedIndex) {
return false;
}
if (type != that.type) {
return false;
}
if (end != that.end) {
return false;
}
if (start != that.start) {
return false;
}
if (max != that.max) {
return false;
}
if (id != null ? !id.equals(that.id) : that.id != null) {
return false;
}
if (criterion != null ? !criterion.equals(that.criterion) : that.criterion != null) {
return false;
}
return selectedText != null
? selectedText.equals(that.selectedText)
: that.selectedText == null;
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (criterion != null ? criterion.hashCode() : 0);
result = 31 * result + selectedIndex;
result = 31 * result + (selectedText != null ? selectedText.hashCode() : 0);
result = 31 * result + type;
result = 31 * result + end;
result = 31 * result + start;
result = 31 * result + max;
return result;
}
@Override @Override
public String toString() { public String toString() {
return "CriterionInstance{" return "CriterionInstance{"
@ -134,7 +208,7 @@ public class CriterionInstance {
+ '}'; + '}';
} }
String serialize() { public String serialize() {
// criterion|entry|text|type|sql // criterion|entry|text|type|sql
return Joiner.on(AndroidUtilities.SERIALIZATION_SEPARATOR) return Joiner.on(AndroidUtilities.SERIALIZATION_SEPARATOR)
.join( .join(
@ -145,20 +219,4 @@ public class CriterionInstance {
type, type,
criterion.sql == null ? "" : criterion.sql)); criterion.sql == null ? "" : criterion.sql));
} }
}
private static String escape(String item) {
if (item == null) {
return ""; // $NON-NLS-1$
}
return item.replace(
AndroidUtilities.SERIALIZATION_SEPARATOR, AndroidUtilities.SEPARATOR_ESCAPE);
}
private static String unescape(String item) {
if (Strings.isNullOrEmpty(item)) {
return "";
}
return item.replace(
AndroidUtilities.SEPARATOR_ESCAPE, AndroidUtilities.SERIALIZATION_SEPARATOR);
}
}

@ -11,7 +11,7 @@ import org.tasks.Callback
import org.tasks.R import org.tasks.R
import org.tasks.locale.Locale import org.tasks.locale.Locale
class CriterionViewHolder(itemView: View, private val locale: Locale, private val onClick: Callback<CriterionInstance>) : RecyclerView.ViewHolder(itemView) { class CriterionViewHolder(itemView: View, private val locale: Locale, private val onClick: Callback<String>) : RecyclerView.ViewHolder(itemView) {
@BindView(R.id.divider) @BindView(R.id.divider)
lateinit var divider: View lateinit var divider: View
@ -66,5 +66,5 @@ class CriterionViewHolder(itemView: View, private val locale: Locale, private va
} }
@OnClick(R.id.row) @OnClick(R.id.row)
fun onClick() = this.onClick.call(criterion) fun onClick() = this.onClick.call(criterion.id)
} }

@ -1,135 +1,56 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.core; package com.todoroo.astrid.core;
import static com.google.common.collect.Lists.transform; import static com.google.common.collect.Lists.transform;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.tasks.Callback; import org.tasks.Callback;
import org.tasks.R; import org.tasks.R;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
public class CustomFilterAdapter extends RecyclerView.Adapter<CriterionViewHolder> { public class CustomFilterAdapter extends RecyclerView.Adapter<CriterionViewHolder> {
private final Callback<CriterionInstance> onClick; private final Callback<String> onClick;
private final List<CriterionInstance> objects;
private final Locale locale; private final Locale locale;
private final AsyncListDiffer<CriterionInstance> differ;
public CustomFilterAdapter( public CustomFilterAdapter(
List<CriterionInstance> objects, List<CriterionInstance> objects, Locale locale, Callback<String> onClick) {
Locale locale,
Callback<CriterionInstance> onClick) {
this.objects = objects;
this.locale = locale; this.locale = locale;
this.onClick = onClick; this.onClick = onClick;
differ = new AsyncListDiffer<>(this, new CriterionDiffCallback());
submitList(objects);
} }
@NonNull @NonNull
@Override @Override
public CriterionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public CriterionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view =
return new CriterionViewHolder( LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_filter_row, parent, false);
inflater.inflate(R.layout.custom_filter_row, parent, false), locale, onClick); return new CriterionViewHolder(view, locale, onClick);
} }
@Override @Override
public void onBindViewHolder(@NonNull CriterionViewHolder holder, int position) { public void onBindViewHolder(@NonNull CriterionViewHolder holder, int position) {
holder.bind(getItem(position)); holder.bind(getItems().get(position));
}
public List<CriterionInstance> getItems() {
return objects;
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return objects.size(); return getItems().size();
}
private String getValue(CriterionInstance instance) {
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
return value;
}
public String getSql() {
StringBuilder sql = new StringBuilder(" WHERE ");
for (CriterionInstance instance : objects) {
String value = getValue(instance);
switch (instance.type) {
case CriterionInstance.TYPE_ADD:
sql.append("OR ");
break;
case CriterionInstance.TYPE_SUBTRACT:
sql.append("AND NOT ");
break;
case CriterionInstance.TYPE_INTERSECT:
sql.append("AND ");
break;
}
// special code for all tasks universe
if (instance.type == CriterionInstance.TYPE_UNIVERSE || instance.criterion.sql == null) {
sql.append(TaskCriteria.activeAndVisible()).append(' ');
} else {
String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
}
return sql.toString();
}
public Map<String, Object> getValues() {
Map<String, Object> values = new HashMap<>();
for (CriterionInstance instance : objects) {
String value = getValue(instance);
if (instance.criterion.valuesForNewTasks != null
&& instance.type == CriterionInstance.TYPE_INTERSECT) {
for (Entry<String, Object> entry : instance.criterion.valuesForNewTasks.entrySet()) {
values.put(
entry.getKey().replace("?", value), entry.getValue().toString().replace("?", value));
}
}
}
return values;
}
public String getCriterion() {
return Joiner.on("\n").join(transform(objects, CriterionInstance::serialize));
}
public CriterionInstance getItem(int position) {
return objects.get(position);
}
public void remove(CriterionInstance criterionInstance) {
objects.remove(criterionInstance);
} }
public void replace(CriterionInstance replace, CriterionInstance instance) { public void submitList(List<CriterionInstance> criteria) {
objects.set(objects.indexOf(replace), instance); differ.submitList(ImmutableList.copyOf(transform(criteria, CriterionInstance::new)));
} }
public void add(CriterionInstance instance) { private List<CriterionInstance> getItems() {
objects.add(instance); return differ.getCurrentList();
} }
} }

@ -7,6 +7,7 @@
package org.tasks.activities; package org.tasks.activities;
import static android.text.TextUtils.isEmpty; import static android.text.TextUtils.isEmpty;
import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Lists.transform; import static com.google.common.collect.Lists.transform;
import android.content.Context; import android.content.Context;
@ -29,6 +30,7 @@ import com.google.android.material.button.MaterialButtonToggleGroup;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Joiner;
import com.todoroo.andlib.data.Property.CountProperty; import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.UnaryCriterion; import com.todoroo.andlib.sql.UnaryCriterion;
@ -46,8 +48,10 @@ import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.FilterDao; import org.tasks.data.FilterDao;
@ -64,6 +68,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
@Inject Locale locale; @Inject Locale locale;
@Inject Database database; @Inject Database database;
@Inject FilterCriteriaProvider filterCriteriaProvider; @Inject FilterCriteriaProvider filterCriteriaProvider;
List<CriterionInstance> criteria;
@BindView(R.id.name) @BindView(R.id.name)
TextInputEditText name; TextInputEditText name;
@ -97,7 +102,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
name.setText(filter.listingTitle); name.setText(filter.listingTitle);
} }
List<CriterionInstance> criteria = criteria =
new ArrayList<>( new ArrayList<>(
CriterionInstance.fromString( CriterionInstance.fromString(
filterCriteriaProvider, filterCriteriaProvider,
@ -114,14 +119,16 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
fab.setExtended(adapter.getItemCount() <= 1); fab.setExtended(isNew() || adapter.getItemCount() <= 1);
updateList(); updateList();
updateTheme(); updateTheme();
} }
private void onClick(CriterionInstance criterionInstance) { private void onClick(String replaceId) {
CriterionInstance criterionInstance = find(criteria, c -> c.getId().equals(replaceId));
View view = View view =
getLayoutInflater().inflate(R.layout.dialog_custom_filter_row_edit, recyclerView, false); getLayoutInflater().inflate(R.layout.dialog_custom_filter_row_edit, recyclerView, false);
MaterialButtonToggleGroup group = view.findViewById(R.id.button_toggle); MaterialButtonToggleGroup group = view.findViewById(R.id.button_toggle);
@ -140,7 +147,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
.show(); .show();
view.findViewById(R.id.delete).setOnClickListener(v -> { view.findViewById(R.id.delete).setOnClickListener(v -> {
d.dismiss(); d.dismiss();
adapter.remove(criterionInstance); criteria.remove(criterionInstance);
updateList(); updateList();
}); });
view.findViewById(R.id.reconfigure).setOnClickListener(v -> { view.findViewById(R.id.reconfigure).setOnClickListener(v -> {
@ -192,9 +199,9 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
instance.criterion = all.get(which); instance.criterion = all.get(which);
showOptionsFor(instance, () -> { showOptionsFor(instance, () -> {
if (replace == null) { if (replace == null) {
adapter.add(instance); criteria.add(instance);
} else { } else {
adapter.replace(replace, instance); criteria.set(criteria.indexOf(replace), instance);
} }
updateList(); updateList();
}); });
@ -248,7 +255,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(EXTRA_CRITERIA, adapter.getCriterion()); outState.putString(EXTRA_CRITERIA, getCriterion());
} }
@Override @Override
@ -284,12 +291,12 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
filter.listingTitle = newName; filter.listingTitle = newName;
filter.tint = selectedColor; filter.tint = selectedColor;
filter.icon = selectedIcon; filter.icon = selectedIcon;
filter.sqlQuery = adapter.getSql(); filter.sqlQuery = getSql();
filter.valuesForNewTasks.clear(); filter.valuesForNewTasks.clear();
for (Map.Entry<String, Object> entry : adapter.getValues().entrySet()) { for (Map.Entry<String, Object> entry : getValues().entrySet()) {
filter.valuesForNewTasks.put(entry.getKey(), entry.getValue()); filter.valuesForNewTasks.put(entry.getKey(), entry.getValue());
} }
filter.setCriterion(adapter.getCriterion()); filter.setCriterion(getCriterion());
filter.setId(filterDao.insertOrUpdate(filter.toStoreObject())); filter.setId(filterDao.insertOrUpdate(filter.toStoreObject()));
setResult( setResult(
RESULT_OK, RESULT_OK,
@ -308,9 +315,9 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
return !(getNewName().equals(filter.listingTitle) return !(getNewName().equals(filter.listingTitle)
&& selectedColor == filter.tint && selectedColor == filter.tint
&& selectedIcon == filter.icon && selectedIcon == filter.icon
&& adapter.getSql().equals(filter.sqlQuery) && getSql().equals(filter.sqlQuery)
&& adapter.getValues().equals(filter.valuesForNewTasks) && getValues().equals(filter.valuesForNewTasks)
&& adapter.getCriterion().equals(filter.getCriterion())); && getCriterion().equals(filter.getCriterion()));
} }
@Override @Override
@ -340,7 +347,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString()) new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString())
.append(" WHERE "); .append(" WHERE ");
for (CriterionInstance instance : adapter.getItems()) { for (CriterionInstance instance : criteria) {
String value = instance.getValueFromCriterion(); String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) { if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = ""; value = "";
@ -376,10 +383,66 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
} }
} }
for (CriterionInstance instance : adapter.getItems()) { for (CriterionInstance instance : criteria) {
instance.max = max; instance.max = max;
} }
adapter.notifyDataSetChanged(); adapter.submitList(criteria);
}
private String getValue(CriterionInstance instance) {
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
}
return value;
}
public String getSql() {
StringBuilder sql = new StringBuilder(" WHERE ");
for (CriterionInstance instance : criteria) {
String value = getValue(instance);
switch (instance.type) {
case CriterionInstance.TYPE_ADD:
sql.append("OR ");
break;
case CriterionInstance.TYPE_SUBTRACT:
sql.append("AND NOT ");
break;
case CriterionInstance.TYPE_INTERSECT:
sql.append("AND ");
break;
}
// special code for all tasks universe
if (instance.type == CriterionInstance.TYPE_UNIVERSE || instance.criterion.sql == null) {
sql.append(TaskCriteria.activeAndVisible()).append(' ');
} else {
String subSql = instance.criterion.sql.replace("?", UnaryCriterion.sanitize(value));
sql.append(Task.ID).append(" IN (").append(subSql).append(") ");
}
}
return sql.toString();
}
public String getCriterion() {
return Joiner.on("\n").join(transform(criteria, CriterionInstance::serialize));
}
public Map<String, Object> getValues() {
Map<String, Object> values = new HashMap<>();
for (CriterionInstance instance : criteria) {
String value = getValue(instance);
if (instance.criterion.valuesForNewTasks != null
&& instance.type == CriterionInstance.TYPE_INTERSECT) {
for (Entry<String, Object> entry : instance.criterion.valuesForNewTasks.entrySet()) {
values.put(
entry.getKey().replace("?", value), entry.getValue().toString().replace("?", value));
}
}
}
return values;
} }
} }

Loading…
Cancel
Save