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.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 {
/**
@ -54,7 +49,6 @@ public abstract class CustomFilterCriterion implements Parcelable {
// --- parcelable utilities
public String getName() {
return name;
}
@ -76,4 +70,42 @@ public abstract class CustomFilterCriterion implements Parcelable {
source.readMap(valuesForNewTasks, getClass().getClassLoader());
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.MultipleSelectCriterion;
import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.helper.UUIDHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -22,6 +23,27 @@ public class CriterionInstance {
public static final int TYPE_SUBTRACT = 1;
public static final int TYPE_INTERSECT = 2;
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(
FilterCriteriaProvider provider, String criterion) {
@ -30,7 +52,6 @@ public class CriterionInstance {
}
List<CriterionInstance> entries = new ArrayList<>();
for (String row : criterion.split("\n")) {
CriterionInstance entry = new CriterionInstance();
List<String> split =
transform(
Splitter.on(AndroidUtilities.SERIALIZATION_SEPARATOR).splitToList(row),
@ -40,6 +61,7 @@ public class CriterionInstance {
return Collections.emptyList();
}
CriterionInstance entry = new CriterionInstance();
entry.criterion = provider.getFilterCriteria(split.get(0));
String value = split.get(1);
if (entry.criterion instanceof TextInputCriterion) {
@ -60,23 +82,25 @@ public class CriterionInstance {
return entries;
}
/** criteria for this instance */
public CustomFilterCriterion criterion;
/** which of the entries is selected (MultipleSelect) */
public int selectedIndex = -1;
/** text of selection (TextInput) */
public String selectedText = null;
/** type of join */
public int type = TYPE_INTERSECT;
private static String escape(String item) {
if (item == null) {
return ""; // $NON-NLS-1$
}
return item.replace(
AndroidUtilities.SERIALIZATION_SEPARATOR, AndroidUtilities.SEPARATOR_ESCAPE);
}
public int end;
/** statistics for filter count */
public int start;
private static String unescape(String item) {
if (Strings.isNullOrEmpty(item)) {
return "";
}
return item.replace(
AndroidUtilities.SEPARATOR_ESCAPE, AndroidUtilities.SERIALIZATION_SEPARATOR);
}
public int max;
public String getId() {
return id;
}
public String getTitleFromCriterion() {
if (criterion instanceof MultipleSelectCriterion) {
@ -113,6 +137,56 @@ public class CriterionInstance {
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
public String toString() {
return "CriterionInstance{"
@ -134,7 +208,7 @@ public class CriterionInstance {
+ '}';
}
String serialize() {
public String serialize() {
// criterion|entry|text|type|sql
return Joiner.on(AndroidUtilities.SERIALIZATION_SEPARATOR)
.join(
@ -145,20 +219,4 @@ public class CriterionInstance {
type,
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.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)
lateinit var divider: View
@ -66,5 +66,5 @@ class CriterionViewHolder(itemView: View, private val locale: Locale, private va
}
@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;
import static com.google.common.collect.Lists.transform;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.base.Joiner;
import com.todoroo.andlib.sql.UnaryCriterion;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import java.util.HashMap;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.tasks.Callback;
import org.tasks.R;
import org.tasks.locale.Locale;
public class CustomFilterAdapter extends RecyclerView.Adapter<CriterionViewHolder> {
private final Callback<CriterionInstance> onClick;
private final List<CriterionInstance> objects;
private final Callback<String> onClick;
private final Locale locale;
private final AsyncListDiffer<CriterionInstance> differ;
public CustomFilterAdapter(
List<CriterionInstance> objects,
Locale locale,
Callback<CriterionInstance> onClick) {
this.objects = objects;
List<CriterionInstance> objects, Locale locale, Callback<String> onClick) {
this.locale = locale;
this.onClick = onClick;
differ = new AsyncListDiffer<>(this, new CriterionDiffCallback());
submitList(objects);
}
@NonNull
@Override
public CriterionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
return new CriterionViewHolder(
inflater.inflate(R.layout.custom_filter_row, parent, false), locale, onClick);
View view =
LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_filter_row, parent, false);
return new CriterionViewHolder(view, locale, onClick);
}
@Override
public void onBindViewHolder(@NonNull CriterionViewHolder holder, int position) {
holder.bind(getItem(position));
}
public List<CriterionInstance> getItems() {
return objects;
holder.bind(getItems().get(position));
}
@Override
public int getItemCount() {
return objects.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);
return getItems().size();
}
public void replace(CriterionInstance replace, CriterionInstance instance) {
objects.set(objects.indexOf(replace), instance);
public void submitList(List<CriterionInstance> criteria) {
differ.submitList(ImmutableList.copyOf(transform(criteria, CriterionInstance::new)));
}
public void add(CriterionInstance instance) {
objects.add(instance);
private List<CriterionInstance> getItems() {
return differ.getCurrentList();
}
}

@ -7,6 +7,7 @@
package org.tasks.activities;
import static android.text.TextUtils.isEmpty;
import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Lists.transform;
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.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Joiner;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.sql.Query;
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.data.Task;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.FilterDao;
@ -64,6 +68,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
@Inject Locale locale;
@Inject Database database;
@Inject FilterCriteriaProvider filterCriteriaProvider;
List<CriterionInstance> criteria;
@BindView(R.id.name)
TextInputEditText name;
@ -97,7 +102,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
name.setText(filter.listingTitle);
}
List<CriterionInstance> criteria =
criteria =
new ArrayList<>(
CriterionInstance.fromString(
filterCriteriaProvider,
@ -114,14 +119,16 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
fab.setExtended(adapter.getItemCount() <= 1);
fab.setExtended(isNew() || adapter.getItemCount() <= 1);
updateList();
updateTheme();
}
private void onClick(CriterionInstance criterionInstance) {
private void onClick(String replaceId) {
CriterionInstance criterionInstance = find(criteria, c -> c.getId().equals(replaceId));
View view =
getLayoutInflater().inflate(R.layout.dialog_custom_filter_row_edit, recyclerView, false);
MaterialButtonToggleGroup group = view.findViewById(R.id.button_toggle);
@ -140,7 +147,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
.show();
view.findViewById(R.id.delete).setOnClickListener(v -> {
d.dismiss();
adapter.remove(criterionInstance);
criteria.remove(criterionInstance);
updateList();
});
view.findViewById(R.id.reconfigure).setOnClickListener(v -> {
@ -192,9 +199,9 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
instance.criterion = all.get(which);
showOptionsFor(instance, () -> {
if (replace == null) {
adapter.add(instance);
criteria.add(instance);
} else {
adapter.replace(replace, instance);
criteria.set(criteria.indexOf(replace), instance);
}
updateList();
});
@ -248,7 +255,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_CRITERIA, adapter.getCriterion());
outState.putString(EXTRA_CRITERIA, getCriterion());
}
@Override
@ -284,12 +291,12 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
filter.listingTitle = newName;
filter.tint = selectedColor;
filter.icon = selectedIcon;
filter.sqlQuery = adapter.getSql();
filter.sqlQuery = getSql();
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.setCriterion(adapter.getCriterion());
filter.setCriterion(getCriterion());
filter.setId(filterDao.insertOrUpdate(filter.toStoreObject()));
setResult(
RESULT_OK,
@ -308,9 +315,9 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
return !(getNewName().equals(filter.listingTitle)
&& selectedColor == filter.tint
&& selectedIcon == filter.icon
&& adapter.getSql().equals(filter.sqlQuery)
&& adapter.getValues().equals(filter.valuesForNewTasks)
&& adapter.getCriterion().equals(filter.getCriterion()));
&& getSql().equals(filter.sqlQuery)
&& getValues().equals(filter.valuesForNewTasks)
&& getCriterion().equals(filter.getCriterion()));
}
@Override
@ -340,7 +347,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
new StringBuilder(Query.select(new CountProperty()).from(Task.TABLE).toString())
.append(" WHERE ");
for (CriterionInstance instance : adapter.getItems()) {
for (CriterionInstance instance : criteria) {
String value = instance.getValueFromCriterion();
if (value == null && instance.criterion.sql != null && instance.criterion.sql.contains("?")) {
value = "";
@ -376,10 +383,66 @@ public class FilterSettingsActivity extends BaseListSettingsActivity {
}
}
for (CriterionInstance instance : adapter.getItems()) {
for (CriterionInstance instance : criteria) {
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