Use material chips on task list

pull/757/head
Alex Baker 7 years ago
parent 57ef0f1088
commit 1169e7fd5e

@ -1,219 +0,0 @@
package org.tasks.tasklist;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.transform;
import android.content.Context;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.util.TypedValue;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
import com.todoroo.astrid.tags.TagService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.data.TagData;
import org.tasks.injection.ForApplication;
import org.tasks.themes.ThemeCache;
import org.tasks.themes.ThemeColor;
public class TagFormatter {
private static final char SPACE = '\u0020';
private static final char HAIR_SPACE = '\u200a';
private static final int MAX_TAGS = 4;
private final Map<String, ColoredString> tagMap = new HashMap<>();
private final Map<String, ColoredString> googleTaskLists = new HashMap<>();
private final Map<String, ColoredString> caldavCalendars = new HashMap<>();
private final TagService tagService;
private final ThemeCache themeCache;
private final GoogleTaskListDao googleTaskListDao;
private final CaldavDao caldavDao;
private final float tagCharacters;
private final Ordering<ColoredString> orderByName =
new Ordering<ColoredString>() {
@Override
public int compare(ColoredString left, ColoredString right) {
return left.name.compareTo(right.name);
}
};
private final Ordering<ColoredString> orderByLength =
new Ordering<ColoredString>() {
@Override
public int compare(ColoredString left, ColoredString right) {
int leftLength = left.name.length();
int rightLength = right.name.length();
if (leftLength < rightLength) {
return -1;
} else if (rightLength < leftLength) {
return 1;
} else {
return 0;
}
}
};
@Inject
public TagFormatter(
@ForApplication Context context,
TagService tagService,
ThemeCache themeCache,
GoogleTaskListDao googleTaskListDao,
CaldavDao caldavDao) {
this.tagService = tagService;
this.themeCache = themeCache;
this.googleTaskListDao = googleTaskListDao;
this.caldavDao = caldavDao;
TypedValue typedValue = new TypedValue();
context.getResources().getValue(R.dimen.tag_characters, typedValue, true);
tagCharacters = typedValue.getFloat();
for (TagData tagData : tagService.getTagList()) {
tagMap.put(tagData.getRemoteId(), new ColoredString(tagData));
}
for (CaldavCalendar calendar : caldavDao.getCalendars()) {
caldavCalendars.put(calendar.getUuid(), new ColoredString(calendar));
}
for (GoogleTaskList list : googleTaskListDao.getAllLists()) {
googleTaskLists.put(list.getRemoteId(), new ColoredString(list));
}
}
CharSequence getTagString(String caldav, String googleTask, List<String> tagUuids) {
List<ColoredString> strings = new ArrayList<>();
if (!Strings.isNullOrEmpty(googleTask)) {
ColoredString googleTaskList = getGoogleTaskList(googleTask);
if (googleTaskList != null) {
strings.add(googleTaskList);
}
} else if (!Strings.isNullOrEmpty(caldav)) {
ColoredString caldavCalendar = getCaldavCalendar(caldav);
if (caldavCalendar != null) {
strings.add(caldavCalendar);
}
}
Iterable<ColoredString> tags = filter(transform(tagUuids, this::getTag), Predicates.notNull());
strings.addAll(0, orderByName.leastOf(tags, MAX_TAGS - strings.size()));
int numTags = strings.size();
if (numTags == 0) {
return null;
}
List<ColoredString> firstFourByNameLength = orderByLength.sortedCopy(strings);
float maxLength = tagCharacters / numTags;
for (int i = 0; i < numTags - 1; i++) {
ColoredString tagData = firstFourByNameLength.get(i);
String name = tagData.name;
if (name.length() >= maxLength) {
break;
}
float excess = maxLength - name.length();
int beneficiaries = numTags - i - 1;
float additional = excess / beneficiaries;
maxLength += additional;
}
List<SpannableString> tagStrings = transform(strings, tagToString(maxLength));
SpannableStringBuilder builder = new SpannableStringBuilder();
for (SpannableString tagString : tagStrings) {
if (builder.length() > 0) {
builder.append(HAIR_SPACE);
}
builder.append(tagString);
}
return builder;
}
private Function<ColoredString, SpannableString> tagToString(final float maxLength) {
return tagData -> {
String tagName = tagData.name;
tagName = tagName.substring(0, Math.min(tagName.length(), (int) maxLength));
SpannableString string = new SpannableString(SPACE + tagName + SPACE);
int themeIndex = tagData.color;
ThemeColor color =
themeIndex >= 0 ? themeCache.getThemeColor(themeIndex) : themeCache.getUntaggedColor();
string.setSpan(
new BackgroundColorSpan(color.getPrimaryColor()),
0,
string.length(),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
string.setSpan(
new ForegroundColorSpan(color.getActionBarTint()),
0,
string.length(),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return string;
};
}
private ColoredString getGoogleTaskList(String remoteId) {
ColoredString googleTaskList = googleTaskLists.get(remoteId);
if (googleTaskList == null) {
GoogleTaskList byRemoteId = googleTaskListDao.getByRemoteId(remoteId);
if (byRemoteId != null) {
googleTaskList = new ColoredString(byRemoteId);
}
googleTaskLists.put(remoteId, googleTaskList);
}
return googleTaskList;
}
private ColoredString getCaldavCalendar(String uuid) {
ColoredString calendar = caldavCalendars.get(uuid);
if (calendar == null) {
CaldavCalendar byUuid = caldavDao.getCalendar(uuid);
if (byUuid != null) {
calendar = new ColoredString(byUuid);
}
caldavCalendars.put(uuid, calendar);
}
return calendar;
}
private ColoredString getTag(String uuid) {
ColoredString tagData = tagMap.get(uuid);
if (tagData == null) {
TagData tagByUuid = tagService.getTagByUuid(uuid);
if (tagByUuid != null) {
tagData = new ColoredString(tagByUuid);
}
tagMap.put(uuid, tagData);
}
return tagData;
}
private class ColoredString {
final String name;
final int color;
ColoredString(TagData tagData) {
name = tagData.getName();
color = tagData.getColor();
}
ColoredString(GoogleTaskList googleTaskList) {
name = googleTaskList.getTitle();
color = googleTaskList.getColor();
}
ColoredString(CaldavCalendar caldavCalendar) {
name = caldavCalendar.getName();
color = caldavCalendar.getColor();
}
}
}

@ -10,8 +10,10 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.ViewGroup;
import com.google.common.primitives.Longs;
import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.utility.Flags;
import java.util.List;
@ -108,6 +110,14 @@ public class TaskListRecyclerAdapter extends RecyclerView.Adapter<ViewHolder>
}
}
@Override
public void onClick(Filter filter) {
if (mode == null) {
MainActivity activity = (MainActivity) taskList.getActivity();
activity.onFilterItemClicked(filter);
}
}
@Override
public boolean onLongPress(ViewHolder viewHolder) {
if (!adapter.isManuallySorted()) {

@ -10,15 +10,21 @@ import android.graphics.Paint;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnLongClick;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.collect.Lists;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.ui.CheckableImageView;
@ -26,13 +32,13 @@ import java.util.List;
import org.tasks.R;
import org.tasks.preferences.Preferences;
import org.tasks.ui.CheckBoxes;
import org.tasks.ui.ChipProvider;
class ViewHolder extends RecyclerView.ViewHolder {
private final Context context;
private final Preferences preferences;
private final CheckBoxes checkBoxes;
private final TagFormatter tagFormatter;
private final int textColorSecondary;
private final TaskDao taskDao;
private final ViewHolderCallbacks callback;
@ -40,6 +46,8 @@ class ViewHolder extends RecyclerView.ViewHolder {
private final int background;
private final int selectedColor;
private final int textColorOverdue;
private final ChipProvider chipProvider;
private final int fontSizeDetails;
@BindView(R.id.row)
public ViewGroup row;
@ -61,8 +69,11 @@ class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.completeBox)
CheckableImageView completeBox;
@BindView(R.id.tag_block)
TextView tagBlock;
@BindView(R.id.chip_scroll)
HorizontalScrollView chipScroll;
@BindView(R.id.chip_group)
ChipGroup chipGroup;
private int indent;
private boolean selected;
@ -74,7 +85,7 @@ class ViewHolder extends RecyclerView.ViewHolder {
Preferences preferences,
int fontSize,
CheckBoxes checkBoxes,
TagFormatter tagFormatter,
ChipProvider chipProvider,
int textColorOverdue,
int textColorSecondary,
TaskDao taskDao,
@ -87,7 +98,7 @@ class ViewHolder extends RecyclerView.ViewHolder {
this.context = context;
this.preferences = preferences;
this.checkBoxes = checkBoxes;
this.tagFormatter = tagFormatter;
this.chipProvider = chipProvider;
this.textColorOverdue = textColorOverdue;
this.textColorSecondary = textColorSecondary;
this.taskDao = taskDao;
@ -119,9 +130,9 @@ class ViewHolder extends RecyclerView.ViewHolder {
}
nameView.setTextSize(fontSize);
int fontSizeDetails = Math.max(10, fontSize - 2);
description.setTextSize(fontSize);
fontSizeDetails = Math.max(10, fontSize - 2);
dueDate.setTextSize(fontSizeDetails);
tagBlock.setTextSize(fontSizeDetails);
view.setTag(this);
for (int i = 0; i < view.getChildCount(); i++) {
@ -248,19 +259,20 @@ class ViewHolder extends RecyclerView.ViewHolder {
dueDateView.setVisibility(View.GONE);
}
if (task.isCompleted()) {
tagBlock.setVisibility(View.GONE);
String tags = task.getTagsString();
List<String> tagUuids = tags != null ? newArrayList(tags.split(",")) : Lists.newArrayList();
List<Chip> chips = chipProvider.getChips(task.getCaldav(), task.getGoogleTaskList(), tagUuids);
if (chips.isEmpty()) {
chipScroll.setVisibility(View.GONE);
} else {
String tags = task.getTagsString();
List<String> tagUuids = tags != null ? newArrayList(tags.split(",")) : Lists.newArrayList();
CharSequence tagString =
tagFormatter.getTagString(task.getCaldav(), task.getGoogleTaskList(), tagUuids);
if (TextUtils.isEmpty(tagString)) {
tagBlock.setVisibility(View.GONE);
} else {
tagBlock.setText(tagString);
tagBlock.setVisibility(View.VISIBLE);
chipGroup.removeAllViews();
for (Chip chip : chips) {
chip.setTextSize(fontSizeDetails);
chip.setOnClickListener(view -> callback.onClick((Filter) view.getTag()));
chipGroup.addView(chip);
}
chipScroll.setVisibility(View.VISIBLE);
}
}
@ -297,6 +309,8 @@ class ViewHolder extends RecyclerView.ViewHolder {
void onClick(ViewHolder viewHolder);
void onClick(Filter filter);
boolean onLongPress(ViewHolder viewHolder);
}
}

@ -15,6 +15,7 @@ import org.tasks.R;
import org.tasks.injection.ForActivity;
import org.tasks.preferences.Preferences;
import org.tasks.ui.CheckBoxes;
import org.tasks.ui.ChipProvider;
public class ViewHolderFactory {
@ -22,7 +23,7 @@ public class ViewHolderFactory {
private final int textColorOverdue;
private final Context context;
private final CheckBoxes checkBoxes;
private final TagFormatter tagFormatter;
private final ChipProvider chipProvider;
private final int fontSize;
private final TaskDao taskDao;
private final DisplayMetrics metrics;
@ -36,11 +37,11 @@ public class ViewHolderFactory {
@ForActivity Context context,
Preferences preferences,
CheckBoxes checkBoxes,
TagFormatter tagFormatter,
ChipProvider chipProvider,
TaskDao taskDao) {
this.context = context;
this.checkBoxes = checkBoxes;
this.tagFormatter = tagFormatter;
this.chipProvider = chipProvider;
this.taskDao = taskDao;
this.preferences = preferences;
textColorSecondary = getData(context, android.R.attr.textColorSecondary);
@ -60,7 +61,7 @@ public class ViewHolderFactory {
preferences,
fontSize,
checkBoxes,
tagFormatter,
chipProvider,
textColorOverdue,
textColorSecondary,
taskDao,

@ -1,16 +1,32 @@
package org.tasks.ui;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static org.tasks.preferences.ResourceResolver.getDimen;
import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.view.LayoutInflater;
import com.google.android.material.chip.Chip;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
import com.todoroo.astrid.api.CaldavFilter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.api.TagFilter;
import com.todoroo.astrid.tags.TagService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.data.TagData;
import org.tasks.injection.ForActivity;
import org.tasks.themes.ThemeCache;
@ -18,14 +34,35 @@ import org.tasks.themes.ThemeColor;
public class ChipProvider {
private final Map<String, GtasksFilter> googleTaskLists = new HashMap<>();
private final Map<String, CaldavFilter> caldavCalendars = new HashMap<>();
private final Map<String, TagFilter> tagDatas = new HashMap<>();
private final Context context;
private final ThemeCache themeCache;
private final int iconAlpha;
private final GoogleTaskListDao googleTaskListDao;
private final CaldavDao caldavDao;
private final TagService tagService;
private final Ordering<TagFilter> orderByName =
new Ordering<TagFilter>() {
@Override
public int compare(TagFilter left, TagFilter right) {
return left.listingTitle.compareTo(right.listingTitle);
}
};
@Inject
public ChipProvider(@ForActivity Context context, ThemeCache themeCache) {
public ChipProvider(
@ForActivity Context context,
ThemeCache themeCache,
GoogleTaskListDao googleTaskListDao,
CaldavDao caldavDao,
TagService tagService) {
this.context = context;
this.themeCache = themeCache;
this.googleTaskListDao = googleTaskListDao;
this.caldavDao = caldavDao;
this.tagService = tagService;
iconAlpha = (int) (255 * getDimen(context, R.dimen.alpha_secondary));
}
@ -36,10 +73,39 @@ public class ChipProvider {
return chip;
}
public List<Chip> getChips(String caldav, String googleTask, Iterable<String> tagUuids) {
List<Chip> chips = new ArrayList<>();
if (!Strings.isNullOrEmpty(googleTask)) {
GtasksFilter googleTaskFilter = getGoogleTaskList(googleTask);
if (googleTaskFilter != null) {
chips.add(newChip(googleTaskFilter));
}
} else if (!Strings.isNullOrEmpty(caldav)) {
CaldavFilter caldavFilter = getCaldavCalendar(caldav);
if (caldavFilter != null) {
chips.add(newChip(caldavFilter));
}
}
Iterable<TagFilter> tagFilters = filter(transform(tagUuids, this::getTag), Predicates.notNull());
for (TagFilter tagFilter : orderByName.sortedCopy(tagFilters)) {
chips.add(newChip(tagFilter));
}
return chips;
}
public void apply(Chip chip, Filter filter) {
apply(chip, filter.listingTitle, filter.tint);
}
private Chip newChip(Filter filter) {
LayoutInflater layoutInflater = ((Activity) context).getLayoutInflater();
Chip chip = (Chip) layoutInflater.inflate(R.layout.chip_task_list, null);
chip.setTag(filter);
apply(chip, filter.listingTitle, filter.tint);
return chip;
}
private void apply(Chip chip, String name, int theme) {
ThemeColor color = theme >= 0 ? themeCache.getThemeColor(theme) : themeCache.getUntaggedColor();
chip.setText(name);
@ -54,4 +120,40 @@ public class ChipProvider {
},
new int[] {color.getPrimaryColor(), color.getPrimaryColor()}));
}
private GtasksFilter getGoogleTaskList(String remoteId) {
GtasksFilter gtasksFilter = googleTaskLists.get(remoteId);
if (gtasksFilter == null) {
GoogleTaskList googleTaskList = googleTaskListDao.getByRemoteId(remoteId);
if (googleTaskList != null) {
gtasksFilter = new GtasksFilter(googleTaskList);
googleTaskLists.put(remoteId, gtasksFilter);
}
}
return gtasksFilter;
}
private CaldavFilter getCaldavCalendar(String uuid) {
CaldavFilter caldavFilter = caldavCalendars.get(uuid);
if (caldavFilter == null) {
CaldavCalendar calendar = caldavDao.getCalendar(uuid);
if (calendar != null) {
caldavFilter = new CaldavFilter(calendar);
caldavCalendars.put(uuid, caldavFilter);
}
}
return caldavFilter;
}
private TagFilter getTag(String uuid) {
TagFilter tagFilter = tagDatas.get(uuid);
if (tagFilter == null) {
TagData tagData = tagService.getTagByUuid(uuid);
if (tagData != null) {
tagFilter = new TagFilter(tagData);
tagDatas.put(uuid, tagFilter);
}
}
return tagFilter;
}
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/TaskListChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:closeIconEnabled="false"/>

@ -74,29 +74,30 @@
android:maxLines="2"
android:textColor="@color/text_secondary"/>
<RelativeLayout
android:layout_width="match_parent"
<HorizontalScrollView
android:id="@+id/chip_scroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/keyline_content_inset"
android:paddingEnd="0dp"
android:paddingLeft="@dimen/keyline_content_inset"
android:paddingRight="0dp"
android:orientation="horizontal">
android:layout_gravity="end"
android:clipChildren="false"
android:scrollbarThumbHorizontal="@android:color/transparent">
<TextView
android:id="@+id/tag_block"
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first"
android:paddingLeft="0dp"
android:paddingRight="@dimen/keyline_first"
android:ellipsize="none"
android:singleLine="true"
android:textSize="14sp"/>
</RelativeLayout>
android:layout_marginStart="0dp"
android:layout_marginEnd="@dimen/keyline_first"
android:layout_marginLeft="0dp"
android:layout_marginRight="@dimen/keyline_first"
android:layout_gravity="end"
android:paddingStart="@dimen/keyline_content_inset"
android:paddingLeft="@dimen/keyline_content_inset"
android:clipToPadding="false"
app:singleLine="true">
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</LinearLayout>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item format="float" name="tag_characters" type="dimen">16</item>
</resources>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item format="float" name="tag_characters" type="dimen">20</item>
</resources>

@ -12,8 +12,6 @@
<dimen name="task_edit_drawable_padding_top_bottom">17dp</dimen>
<dimen name="task_edit_text_size">16sp</dimen>
<item format="float" name="tag_characters" type="dimen">12</item>
<item format="float" name="alpha_secondary" type="dimen">0.54</item>
<item format="float" name="alpha_disabled" type="dimen">0.38</item>

@ -16,6 +16,17 @@
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="TaskListChip" parent="Base.Widget.MaterialComponents.Chip">
<item name="closeIconVisible">false</item>
<item name="chipMinHeight">28dp</item>
<item name="chipCornerRadius">14dp</item>
<item name="chipStartPadding">4dp</item>
<item name="textStartPadding">6dp</item>
<item name="textEndPadding">6dp</item>
<item name="chipEndPadding">4dp</item>
<item name="enforceTextAppearance">false</item>
</style>
<style name="TextBoxText">
<item name="android:textColor">?attr/asTextColor</item>
</style>

Loading…
Cancel
Save