mirror of https://github.com/tasks/tasks
New tag picker activity
parent
1e33549d44
commit
4e29043390
@ -0,0 +1,19 @@
|
||||
package org.tasks.tags;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import com.google.common.base.Objects;
|
||||
import org.tasks.data.TagData;
|
||||
|
||||
public class TagDiffCallback extends DiffUtil.ItemCallback<TagData> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull TagData oldItem, @NonNull TagData newItem) {
|
||||
return Objects.equal(oldItem.getId(), newItem.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull TagData oldItem, @NonNull TagData newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package org.tasks.tags;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.EditText;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnTextChanged;
|
||||
import com.google.common.base.Strings;
|
||||
import java.util.ArrayList;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.R;
|
||||
import org.tasks.billing.Inventory;
|
||||
import org.tasks.data.TagData;
|
||||
import org.tasks.injection.ActivityComponent;
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity;
|
||||
import org.tasks.themes.Theme;
|
||||
import org.tasks.themes.ThemeCache;
|
||||
import org.tasks.themes.ThemeColor;
|
||||
import org.tasks.ui.MenuColorizer;
|
||||
|
||||
public class TagPickerActivity extends ThemedInjectingAppCompatActivity {
|
||||
|
||||
public static final String EXTRA_TAGS = "extra_tags";
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
|
||||
@BindView(R.id.recycler_view)
|
||||
RecyclerView recyclerView;
|
||||
|
||||
@BindView(R.id.search_input)
|
||||
EditText editText;
|
||||
|
||||
@Inject Theme theme;
|
||||
@Inject ThemeCache themeCache;
|
||||
@Inject Inventory inventory;
|
||||
|
||||
private TagPickerViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
ArrayList<TagData> tags = getIntent().getParcelableArrayListExtra(EXTRA_TAGS);
|
||||
viewModel.setTags(tags);
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_tag_picker);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px);
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
MenuColorizer.colorToolbar(this, toolbar);
|
||||
ThemeColor themeColor = theme.getThemeColor();
|
||||
themeColor.applyToStatusBarIcons(this);
|
||||
themeColor.applyToNavigationBar(this);
|
||||
|
||||
TagRecyclerAdapter recyclerAdapter =
|
||||
new TagRecyclerAdapter(this, viewModel, themeCache, inventory, this::onToggle);
|
||||
|
||||
recyclerView.setAdapter(recyclerAdapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
viewModel.observe(this, recyclerAdapter::submitList);
|
||||
editText.setText(viewModel.getText());
|
||||
}
|
||||
|
||||
private Void onToggle(TagData tagData, Boolean checked) {
|
||||
boolean newTag = tagData.getId() == null;
|
||||
|
||||
viewModel.toggle(tagData, checked);
|
||||
|
||||
if (newTag) {
|
||||
clear();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@OnTextChanged(R.id.search_input)
|
||||
void onSearch(CharSequence text) {
|
||||
viewModel.search(text.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (Strings.isNullOrEmpty(viewModel.getText())) {
|
||||
Intent data = new Intent();
|
||||
data.putParcelableArrayListExtra(EXTRA_TAGS, viewModel.getTags());
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
editText.setText("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = ViewModelProviders.of(this).get(TagPickerViewModel.class);
|
||||
component.inject(viewModel);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
package org.tasks.tags;
|
||||
|
||||
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnCheckedChanged;
|
||||
import butterknife.OnClick;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import org.tasks.R;
|
||||
import org.tasks.data.TagData;
|
||||
|
||||
public class TagPickerViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Context context;
|
||||
private final Function2<TagData, Boolean, Void> callback;
|
||||
|
||||
@BindView(R.id.text)
|
||||
TextView text;
|
||||
|
||||
@BindView(R.id.checkbox)
|
||||
CheckBox checkBox;
|
||||
|
||||
private TagData tagData;
|
||||
|
||||
TagPickerViewHolder(
|
||||
Context context, @NonNull View view, Function2<TagData, Boolean, Void> callback) {
|
||||
super(view);
|
||||
this.callback = callback;
|
||||
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@OnClick(R.id.tag_row)
|
||||
void onClickRow() {
|
||||
if (tagData.getId() == null) {
|
||||
callback.invoke(tagData, true);
|
||||
} else {
|
||||
checkBox.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
@OnCheckedChanged(R.id.checkbox)
|
||||
void onCheckedChanged() {
|
||||
callback.invoke(tagData, checkBox.isChecked());
|
||||
}
|
||||
|
||||
public void bind(@NonNull TagData tagData, int color, @Nullable Integer icon, boolean checked) {
|
||||
this.tagData = tagData;
|
||||
if (tagData.getId() == null) {
|
||||
text.setText(context.getString(R.string.create_new_tag, tagData.getName()));
|
||||
icon = R.drawable.ic_outline_add_24px;
|
||||
checkBox.setVisibility(View.GONE);
|
||||
} else {
|
||||
text.setText(tagData.getName());
|
||||
checkBox.setChecked(checked);
|
||||
checkBox.setVisibility(View.VISIBLE);
|
||||
if (icon == null) {
|
||||
icon = R.drawable.ic_outline_label_24px;
|
||||
}
|
||||
}
|
||||
Drawable original = ContextCompat.getDrawable(context, icon);
|
||||
Drawable wrapped = DrawableCompat.wrap(original.mutate());
|
||||
DrawableCompat.setTint(wrapped, color);
|
||||
if (atLeastJellybeanMR1()) {
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(wrapped, null, null, null);
|
||||
} else {
|
||||
text.setCompoundDrawablesWithIntrinsicBounds(wrapped, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package org.tasks.tags;
|
||||
|
||||
import static com.google.common.collect.Iterables.any;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import com.google.common.base.Strings;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.data.TagData;
|
||||
import org.tasks.data.TagDataDao;
|
||||
|
||||
public class TagPickerViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<List<TagData>> tags = new MutableLiveData<>();
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
@Inject TagDataDao tagDataDao;
|
||||
|
||||
private String text;
|
||||
private Set<TagData> selected = new HashSet<>();
|
||||
|
||||
public void observe(LifecycleOwner owner, Observer<List<TagData>> observer) {
|
||||
tags.observe(owner, observer);
|
||||
}
|
||||
|
||||
public void setTags(List<TagData> tags) {
|
||||
selected.addAll(tags);
|
||||
}
|
||||
|
||||
public ArrayList<TagData> getTags() {
|
||||
return newArrayList(selected);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
search("");
|
||||
}
|
||||
|
||||
public void search(String text) {
|
||||
if (!text.equalsIgnoreCase(this.text)) {
|
||||
disposables.add(
|
||||
Single.fromCallable(() -> tagDataDao.searchTags(text))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onUpdate));
|
||||
}
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
private void onUpdate(List<TagData> results) {
|
||||
if (!Strings.isNullOrEmpty(text) && !any(results, t -> text.equalsIgnoreCase(t.getName()))) {
|
||||
results.add(0, new TagData(text));
|
||||
}
|
||||
tags.setValue(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
disposables.dispose();
|
||||
}
|
||||
|
||||
public boolean isChecked(TagData tagData) {
|
||||
return selected.contains(tagData);
|
||||
}
|
||||
|
||||
void toggle(TagData tagData, boolean checked) {
|
||||
if (tagData.getId() == null) {
|
||||
tagData = new TagData(tagData.getName());
|
||||
tagDataDao.createNew(tagData);
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
selected.add(tagData);
|
||||
} else {
|
||||
selected.remove(tagData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package org.tasks.tags;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.AsyncListDiffer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import java.util.List;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import org.tasks.R;
|
||||
import org.tasks.billing.Inventory;
|
||||
import org.tasks.data.TagData;
|
||||
import org.tasks.themes.CustomIcons;
|
||||
import org.tasks.themes.ThemeCache;
|
||||
|
||||
public class TagRecyclerAdapter extends RecyclerView.Adapter<TagPickerViewHolder> {
|
||||
|
||||
private final AsyncListDiffer<TagData> differ;
|
||||
private final Context context;
|
||||
private final TagPickerViewModel viewModel;
|
||||
private final ThemeCache themeCache;
|
||||
private final Inventory inventory;
|
||||
private final Function2<TagData, Boolean, Void> callback;
|
||||
|
||||
TagRecyclerAdapter(
|
||||
Context context,
|
||||
TagPickerViewModel viewModel,
|
||||
ThemeCache themeCache,
|
||||
Inventory inventory,
|
||||
Function2<TagData, Boolean, Void> callback) {
|
||||
this.context = context;
|
||||
this.viewModel = viewModel;
|
||||
this.themeCache = themeCache;
|
||||
this.inventory = inventory;
|
||||
this.callback = callback;
|
||||
differ = new AsyncListDiffer<>(this, new TagDiffCallback());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TagPickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.row_tag_picker, parent, false);
|
||||
return new TagPickerViewHolder(context, view, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TagPickerViewHolder holder, int position) {
|
||||
TagData tagData = differ.getCurrentList().get(position);
|
||||
boolean checked = viewModel.isChecked(tagData);
|
||||
holder.bind(tagData, getColor(tagData), getIcon(tagData), checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return differ.getCurrentList().size();
|
||||
}
|
||||
|
||||
private int getColor(TagData tagData) {
|
||||
return themeCache
|
||||
.getThemeColor(tagData.getColor() >= 0 ? tagData.getColor() : 19)
|
||||
.getPrimaryColor();
|
||||
}
|
||||
|
||||
private @Nullable Integer getIcon(TagData tagData) {
|
||||
return tagData.getIcon() < 1000 || inventory.hasPro()
|
||||
? CustomIcons.getIconResId(tagData.getIcon())
|
||||
: null;
|
||||
}
|
||||
|
||||
public void submitList(List<TagData> tagData) {
|
||||
differ.submitList(tagData);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="@dimen/elevation_toolbar"
|
||||
android:theme="?attr/overlay_theme"
|
||||
app:popupTheme="@style/popup_overlay"
|
||||
app:titleTextColor="?attr/colorOnPrimary"
|
||||
app:toolbarStyle="@style/Widget.MaterialComponents.Toolbar"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/search_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:hint="@string/enter_tag_name"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
android:textColorHint="?attr/colorOnPrimaryHint" />
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:elevation="@dimen/elevation_task_list" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
** Copyright (c) 2012 Todoroo Inc
|
||||
**
|
||||
** See the file "LICENSE" for the full license governing this code.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/newTags"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/keyline_first"
|
||||
android:paddingEnd="@dimen/keyline_first"
|
||||
android:paddingLeft="@dimen/keyline_first"
|
||||
android:paddingRight="@dimen/keyline_first"
|
||||
android:orientation="vertical"/>
|
||||
|
||||
<View
|
||||
style="@style/horizontal_divider"
|
||||
android:padding="5dip"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/existingTags"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="100"
|
||||
android:choiceMode="multipleChoice"
|
||||
android:divider="@null"
|
||||
android:dividerHeight="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/tag_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_toStartOf="@id/checkbox"
|
||||
android:layout_toLeftOf="@id/checkbox"
|
||||
android:drawablePadding="@dimen/keyline_second"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_centerVertical="true"
|
||||
android:paddingStart="@dimen/keyline_first"
|
||||
android:paddingLeft="@dimen/keyline_first"
|
||||
android:paddingEnd="@dimen/keyline_first"
|
||||
android:paddingRight="@dimen/keyline_first"
|
||||
android:textSize="@dimen/task_edit_text_size" />
|
||||
</RelativeLayout>
|
||||
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
** Copyright (c) 2012 Todoroo Inc
|
||||
**
|
||||
** See the file "LICENSE" for the full license governing this code.
|
||||
-->
|
||||
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||
android:paddingStart="@dimen/keyline_first"
|
||||
android:paddingEnd="@dimen/keyline_first"
|
||||
android:paddingLeft="@dimen/keyline_first"
|
||||
android:paddingRight="@dimen/keyline_first"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:drawableLeft="@drawable/ic_outline_label_24px"
|
||||
android:drawablePadding="@dimen/keyline_second"
|
||||
android:drawableStart="@drawable/ic_outline_label_24px"
|
||||
android:gravity="center_vertical"/>
|
||||
@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/text1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:hint="@string/new_tag"
|
||||
android:inputType="textCapSentences"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/remove"
|
||||
android:src="@drawable/ic_outline_clear_24px"
|
||||
android:tint="@color/icon_tint"/>
|
||||
</LinearLayout>
|
||||
Loading…
Reference in New Issue