word-based tag search with respect to order
This commit is contained in:
parent
862160d1ef
commit
1802a0e022
1 changed files with 136 additions and 30 deletions
|
@ -27,34 +27,37 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import art.pegasko.yeeemp.base.Tag;
|
||||||
import art.pegasko.yeeemp.base.TagStat;
|
import art.pegasko.yeeemp.base.TagStat;
|
||||||
|
|
||||||
public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
|
public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
|
||||||
public static final String TAG = EventEditTagsAdapter.class.getSimpleName();
|
public static final String TAG = EventEditTagsAdapter.class.getSimpleName();
|
||||||
|
|
||||||
private ArrayList<TagStat> tags;
|
private ArrayList<TagStat> tags; // used by parent as container for filtered elements, changes as suer types
|
||||||
|
private ArrayList<TagStat> tagsAll; // used as container with backup of all unfiltered tags, does not change
|
||||||
private LayoutInflater inflater;
|
private LayoutInflater inflater;
|
||||||
private final int viewResourceId;
|
private final int viewResourceId;
|
||||||
private final int viewFieldId;
|
private final int viewFieldId;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList<TagStat> tags) {
|
public EventEditTagsAdapter(Context context, int viewResourceId, int viewFieldId, ArrayList<TagStat> tags) {
|
||||||
super(context, viewResourceId, tags);
|
super(context, viewResourceId, tags);
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
|
this.tagsAll = (ArrayList<TagStat>) tags.clone();
|
||||||
this.viewResourceId = viewResourceId;
|
this.viewResourceId = viewResourceId;
|
||||||
this.viewFieldId = viewFieldId;
|
this.viewFieldId = viewFieldId;
|
||||||
this.inflater = LayoutInflater.from(context);
|
this.inflater = LayoutInflater.from(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
public View getView(int position, View tagView, ViewGroup parent) {
|
||||||
if (convertView == null) convertView = inflater.inflate(this.viewResourceId, parent, false);
|
if (tagView == null)
|
||||||
|
tagView = inflater.inflate(this.viewResourceId, parent, false);
|
||||||
|
|
||||||
TextView text;
|
TextView text;
|
||||||
try {
|
try {
|
||||||
if (this.viewFieldId == 0) {
|
if (this.viewFieldId == 0) {
|
||||||
text = (TextView) convertView;
|
text = (TextView) tagView;
|
||||||
} else {
|
} else {
|
||||||
text = convertView.findViewById(this.viewFieldId);
|
text = tagView.findViewById(this.viewFieldId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
|
@ -68,7 +71,7 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
|
||||||
|
|
||||||
text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")");
|
text.setText(tags.get(position).tag.getName() + " (" + tags.get(position).count + ")");
|
||||||
|
|
||||||
return convertView;
|
return tagView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -80,47 +83,150 @@ public class EventEditTagsAdapter extends ArrayAdapter<TagStat> {
|
||||||
// TODO: Stop using mutable global
|
// TODO: Stop using mutable global
|
||||||
// Reusable filter result
|
// Reusable filter result
|
||||||
private ArrayList<TagStat> tagsSuggestions = new ArrayList<TagStat>();
|
private ArrayList<TagStat> tagsSuggestions = new ArrayList<TagStat>();
|
||||||
|
private String prevConstraint = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
public String convertResultToString(Object resultValue) {
|
public String convertResultToString(Object resultValue) {
|
||||||
String str = ((TagStat) (resultValue)).tag.getName();
|
return ((TagStat) (resultValue)).tag.getName();
|
||||||
return str;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if constraint is reducing previous.
|
||||||
|
*
|
||||||
|
* Constraint (C) reduces previous (P) if `len(C) >= len(P)` and `C.startsWith(P)`.
|
||||||
|
*
|
||||||
|
* This is means that user has typed some text to the end of input field and matching items can be filtered
|
||||||
|
* from previous filter result.
|
||||||
|
* In the opposite case, the set of matching items grows and required full rebuild from scratch.
|
||||||
|
*/
|
||||||
|
private boolean checkIfPossibleReduceToConstraint(String prevConstraint, String constraint) {
|
||||||
|
if (prevConstraint == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (constraint.length() >= prevConstraint.length()) && (constraint.startsWith(prevConstraint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeConstraintFromUserInput(CharSequence userInput) {
|
||||||
|
return userInput.toString().toLowerCase().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasChangedConstraint(String prevConstraint, String newConstraint) {
|
||||||
|
if (prevConstraint == null || newConstraint == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return !prevConstraint.equals(newConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reduceTagsSuggestions(TagMatcher matcher) {
|
||||||
|
for (int index = 0; index < tagsSuggestions.size(); ) {
|
||||||
|
if (!matcher.isMatch(tagsSuggestions.get(index).tag))
|
||||||
|
tagsSuggestions.remove(index);
|
||||||
|
else
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void filterTagsSuggestions(TagMatcher matcher) {
|
||||||
|
tagsSuggestions.clear();
|
||||||
|
for (TagStat tag : tagsAll) {
|
||||||
|
if (matcher.isMatch(tag.tag)) {
|
||||||
|
tagsSuggestions.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTagsSuggestions(CharSequence constraint) {
|
||||||
|
String newConstraint = makeConstraintFromUserInput(constraint);
|
||||||
|
TagMatcher matcher = new TagMatcher(newConstraint);
|
||||||
|
|
||||||
|
// TODO: Use better search strategy and optimize search in non-reducing mode
|
||||||
|
if (hasChangedConstraint(prevConstraint, newConstraint)) {
|
||||||
|
if (checkIfPossibleReduceToConstraint(prevConstraint, newConstraint))
|
||||||
|
reduceTagsSuggestions(matcher);
|
||||||
|
else
|
||||||
|
filterTagsSuggestions(matcher);
|
||||||
|
|
||||||
|
prevConstraint = newConstraint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetTagsSuggestions() {
|
||||||
|
prevConstraint = null;
|
||||||
|
tagsSuggestions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilterResults makeFilterResults() {
|
||||||
|
FilterResults filterResults = new FilterResults();
|
||||||
|
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
|
||||||
|
filterResults.values = tagsSuggestions;
|
||||||
|
filterResults.count = tagsSuggestions.size();
|
||||||
|
return filterResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilterResults makeEmptyFilterResults() {
|
||||||
|
return new FilterResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FilterResults performFiltering(CharSequence constraint) {
|
protected FilterResults performFiltering(CharSequence constraint) {
|
||||||
synchronized (tagsSuggestions) {
|
synchronized (this) {
|
||||||
if (constraint != null) {
|
if (constraint != null) {
|
||||||
tagsSuggestions.clear();
|
prepareTagsSuggestions(constraint);
|
||||||
for (TagStat tag : tags) {
|
return makeFilterResults();
|
||||||
if (tag.tag.getName().contains(constraint.toString().toLowerCase())) {
|
|
||||||
tagsSuggestions.add(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FilterResults filterResults = new FilterResults();
|
|
||||||
// TODO: Spank me for using global mutable instance for returning immutable result (causes concurrent modification when publishResults())
|
|
||||||
filterResults.values = tagsSuggestions;
|
|
||||||
filterResults.count = tagsSuggestions.size();
|
|
||||||
return filterResults;
|
|
||||||
} else {
|
} else {
|
||||||
return new FilterResults();
|
resetTagsSuggestions();
|
||||||
|
return makeEmptyFilterResults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
if (results.values == null)
|
synchronized (this) {
|
||||||
return;
|
if (results == null)
|
||||||
|
return;
|
||||||
|
|
||||||
synchronized (results.values) {
|
if (results.values == null) {
|
||||||
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
|
|
||||||
if (results.count > 0) {
|
|
||||||
clear();
|
clear();
|
||||||
for (TagStat c : filteredList) {
|
|
||||||
add(c);
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Do not republish results if no data changed
|
||||||
|
ArrayList<TagStat> filteredList = (ArrayList<TagStat>) results.values;
|
||||||
|
clear();
|
||||||
|
for (TagStat c : filteredList) {
|
||||||
|
add(c);
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to filter tags based on user-input constraint
|
||||||
|
*/
|
||||||
|
class TagMatcher {
|
||||||
|
private final String[] constraintParts;
|
||||||
|
|
||||||
|
public TagMatcher(String constraint) {
|
||||||
|
this.constraintParts = constraint.split(" +");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMatch(Tag tag) {
|
||||||
|
String tagName = tag.getName();
|
||||||
|
int lastIndex = 0;
|
||||||
|
for (String seq : constraintParts) {
|
||||||
|
if (lastIndex >= tagName.length())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int newIndex = tagName.indexOf(seq, lastIndex);
|
||||||
|
if (newIndex == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
lastIndex = newIndex + seq.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue