From 1a05278ab02e8bc16c0a53c1d48f9cc101072962 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 23 Apr 2020 09:01:02 -0500 Subject: [PATCH] Animate filter recycler view changes --- .../astrid/api/CustomFilterCriterion.java | 44 +++++- .../astrid/core/CriterionDiffCallback.java | 19 +++ .../astrid/core/CriterionInstance.java | 126 +++++++++++++----- .../astrid/core/CriterionViewHolder.kt | 4 +- .../astrid/core/CustomFilterAdapter.java | 113 +++------------- .../activities/FilterSettingsActivity.java | 95 ++++++++++--- 6 files changed, 247 insertions(+), 154 deletions(-) create mode 100644 app/src/main/java/com/todoroo/astrid/core/CriterionDiffCallback.java diff --git a/app/src/main/java/com/todoroo/astrid/api/CustomFilterCriterion.java b/app/src/main/java/com/todoroo/astrid/api/CustomFilterCriterion.java index 833b0ab7a..307e84980 100644 --- a/app/src/main/java/com/todoroo/astrid/api/CustomFilterCriterion.java +++ b/app/src/main/java/com/todoroo/astrid/api/CustomFilterCriterion.java @@ -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 - */ 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; + } } diff --git a/app/src/main/java/com/todoroo/astrid/core/CriterionDiffCallback.java b/app/src/main/java/com/todoroo/astrid/core/CriterionDiffCallback.java new file mode 100644 index 000000000..d494e69b2 --- /dev/null +++ b/app/src/main/java/com/todoroo/astrid/core/CriterionDiffCallback.java @@ -0,0 +1,19 @@ +package com.todoroo.astrid.core; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +public class CriterionDiffCallback extends DiffUtil.ItemCallback { + + @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); + } +} diff --git a/app/src/main/java/com/todoroo/astrid/core/CriterionInstance.java b/app/src/main/java/com/todoroo/astrid/core/CriterionInstance.java index ad4a62731..3fc16543f 100644 --- a/app/src/main/java/com/todoroo/astrid/core/CriterionInstance.java +++ b/app/src/main/java/com/todoroo/astrid/core/CriterionInstance.java @@ -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 fromString( FilterCriteriaProvider provider, String criterion) { @@ -30,7 +52,6 @@ public class CriterionInstance { } List entries = new ArrayList<>(); for (String row : criterion.split("\n")) { - CriterionInstance entry = new CriterionInstance(); List 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); - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/todoroo/astrid/core/CriterionViewHolder.kt b/app/src/main/java/com/todoroo/astrid/core/CriterionViewHolder.kt index aa34a1ba4..3b8351453 100644 --- a/app/src/main/java/com/todoroo/astrid/core/CriterionViewHolder.kt +++ b/app/src/main/java/com/todoroo/astrid/core/CriterionViewHolder.kt @@ -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) : RecyclerView.ViewHolder(itemView) { +class CriterionViewHolder(itemView: View, private val locale: Locale, private val onClick: Callback) : 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) } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java b/app/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java index dc80ed683..776316b38 100644 --- a/app/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/core/CustomFilterAdapter.java @@ -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 { - private final Callback onClick; - private final List objects; + private final Callback onClick; private final Locale locale; + private final AsyncListDiffer differ; public CustomFilterAdapter( - List objects, - Locale locale, - Callback onClick) { - this.objects = objects; + List objects, Locale locale, Callback 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 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 getValues() { - Map values = new HashMap<>(); - for (CriterionInstance instance : objects) { - String value = getValue(instance); - - if (instance.criterion.valuesForNewTasks != null - && instance.type == CriterionInstance.TYPE_INTERSECT) { - for (Entry 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 criteria) { + differ.submitList(ImmutableList.copyOf(transform(criteria, CriterionInstance::new))); } - public void add(CriterionInstance instance) { - objects.add(instance); + private List getItems() { + return differ.getCurrentList(); } } diff --git a/app/src/main/java/org/tasks/activities/FilterSettingsActivity.java b/app/src/main/java/org/tasks/activities/FilterSettingsActivity.java index 789ec9a16..7314779d4 100644 --- a/app/src/main/java/org/tasks/activities/FilterSettingsActivity.java +++ b/app/src/main/java/org/tasks/activities/FilterSettingsActivity.java @@ -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 criteria; @BindView(R.id.name) TextInputEditText name; @@ -97,7 +102,7 @@ public class FilterSettingsActivity extends BaseListSettingsActivity { name.setText(filter.listingTitle); } - List 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 entry : adapter.getValues().entrySet()) { + for (Map.Entry 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 getValues() { + Map values = new HashMap<>(); + for (CriterionInstance instance : criteria) { + String value = getValue(instance); + + if (instance.criterion.valuesForNewTasks != null + && instance.type == CriterionInstance.TYPE_INTERSECT) { + for (Entry entry : instance.criterion.valuesForNewTasks.entrySet()) { + values.put( + entry.getKey().replace("?", value), entry.getValue().toString().replace("?", value)); + } + } + } + return values; } }