Add recycler view item long press with read state actions (set read/unread)

This commit is contained in:
Shinokuni 2019-04-15 18:45:23 +02:00
parent da13d487e4
commit 1e8195b906
11 changed files with 253 additions and 44 deletions

View File

@ -6,6 +6,7 @@ import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
@ -14,6 +15,7 @@ import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -46,6 +48,7 @@ import org.apache.commons.collections4.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -87,6 +90,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
private boolean showReadItems;
private ListSortType sortType;
private ActionMode actionMode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -221,31 +226,105 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider();
adapter = new MainItemListAdapter(GlideApp.with(this), preloadSizeProvider);
adapter.setOnItemClickListener((itemWithFeed, position) -> {
Intent intent = new Intent(this, ItemActivity.class);
adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() {
@Override
public void onItemClick(ItemWithFeed itemWithFeed, int position) {
if (actionMode == null) {
Intent intent = new Intent(getApplicationContext(), ItemActivity.class);
intent.putExtra(ItemActivity.ITEM_ID, itemWithFeed.getItem().getId());
intent.putExtra(ItemActivity.IMAGE_URL, itemWithFeed.getItem().getImageLink());
startActivityForResult(intent, ITEM_REQUEST);
intent.putExtra(ItemActivity.ITEM_ID, itemWithFeed.getItem().getId());
intent.putExtra(ItemActivity.IMAGE_URL, itemWithFeed.getItem().getImageLink());
startActivityForResult(intent, ITEM_REQUEST);
viewModel.setItemRead(itemWithFeed.getItem().getId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
viewModel.setItemReadState(itemWithFeed.getItem().getId(), true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableCompletableObserver() {
@Override
public void onComplete() {
}
@Override
public void onError(Throwable e) {
}
});
itemWithFeed.getItem().setRead(true);
adapter.notifyItemChanged(position, itemWithFeed);
updateDrawerFeeds();
} else {
adapter.toggleSelection(position);
if (adapter.getSelection().isEmpty())
actionMode.finish();
}
}
@Override
public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
if (actionMode != null)
return;
adapter.toggleSelection(position);
actionMode = startActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
refreshLayout.setEnabled(false);
actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
menu.findItem(R.id.item_mark_read).setVisible(!itemWithFeed.getItem().isRead());
menu.findItem(R.id.item_mark_unread).setVisible(itemWithFeed.getItem().isRead());
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.item_mark_read:
viewModel.setItemsReadState(getIdsFromPositions(adapter.getSelection()), true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
adapter.updateSelection(true);
break;
case R.id.item_mark_unread:
viewModel.setItemsReadState(getIdsFromPositions(adapter.getSelection()), false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
adapter.updateSelection(false);
break;
}
@Override
public void onError(Throwable e) {
actionMode.finish();
return true;
}
}
});
@Override
public void onDestroyActionMode(ActionMode mode) {
mode.finish();
actionMode = null;
itemWithFeed.getItem().setRead(true);
adapter.notifyItemChanged(position, itemWithFeed);
updateDrawerFeeds();
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
refreshLayout.setEnabled(true);
adapter.clearSelection();
}
});
}
});
RecyclerViewPreloader<String> preloader = new RecyclerViewPreloader<String>(Glide.with(this), adapter, preloadSizeProvider, 10);
@ -421,7 +500,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
showReadItems = true;
SharedPreferencesManager.writeValue(this,
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true);
}
filterItems(filterFeedId);
@ -457,8 +535,20 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
.show();
}
private List<Integer> getIdsFromPositions(LinkedHashSet<Integer> positions) {
List<Integer> ids = new ArrayList<>();
for (int position : positions) {
ids.add((int)adapter.getItemId(position));
}
return ids;
}
public enum ListSortType {
NEWEST_TO_OLDEST,
OLDEST_TO_NEWEST
}
}

View File

@ -32,8 +32,13 @@ public interface ItemDao {
@Insert
void insertAll(List<Item> items);
@Query("Update Item set read = 1 Where id = :itemId")
void setRead(int itemId);
/**
* Set an item read or unread
* @param itemId if of the item to update
* @param readState 1 for read, 0 for unread
*/
@Query("Update Item set read = :readState Where id = :itemId")
void setReadState(int itemId, int readState);
@Query("Select count(*) From Item Where feed_id = :feedId And read = 0")
int getUnreadCount(int feedId);

View File

@ -10,9 +10,8 @@ import com.readrops.app.database.entities.Feed;
import com.readrops.app.database.entities.Folder;
import com.readrops.app.database.pojo.ItemWithFeed;
import com.readrops.app.repositories.LocalFeedRepository;
import com.readrops.app.utils.ParsingResult;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@ -67,10 +66,20 @@ public class MainViewModel extends AndroidViewModel {
});
}
public Completable setItemRead(int itemId) {
public Completable setItemReadState(int itemId, boolean read) {
return Completable.create(emitter -> {
db.itemDao().setRead(itemId);
db.itemDao().setReadState(itemId, read ? 1 : 0);
emitter.onComplete();
});
}
public Completable setItemsReadState(List<Integer> ids, boolean read) {
List<Completable> completableList = new ArrayList<>();
for (int id : ids) {
completableList.add(setItemReadState(id, read));
}
return Completable.concat(completableList);
}
}

View File

@ -1,13 +1,19 @@
package com.readrops.app.views;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.recyclerview.extensions.ListAdapter;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -32,6 +38,7 @@ import com.readrops.app.utils.GlideRequests;
import com.readrops.app.utils.Utils;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import static com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions.withCrossFade;
@ -42,11 +49,14 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
private OnItemClickListener listener;
private ViewPreloadSizeProvider preloadSizeProvider;
private LinkedHashSet<Integer> selection;
public MainItemListAdapter(GlideRequests glideRequests, ViewPreloadSizeProvider preloadSizeProvider) {
super(DIFF_CALLBACK);
this.glideRequests = glideRequests;
this.preloadSizeProvider = preloadSizeProvider;
selection = new LinkedHashSet<>();
}
private static final DiffUtil.ItemCallback<ItemWithFeed> DIFF_CALLBACK = new DiffUtil.ItemCallback<ItemWithFeed>() {
@ -87,8 +97,8 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
if (payloads.size() > 0) {
ItemWithFeed itemWithFeed = (ItemWithFeed) payloads.get(0);
float alpha = itemWithFeed.getItem().isRead() ? 0.5f : 1.0f;
holder.itemView.setAlpha(alpha);
holder.setReadState(itemWithFeed.getItem().isRead());
holder.setSelected(selection.contains(position));
} else
onBindViewHolder(holder, position);
}
@ -98,17 +108,6 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
ItemWithFeed itemWithFeed = getItem(i);
viewHolder.bind(itemWithFeed);
View[] alphaViews = new View[] {
viewHolder.dateLayout,
viewHolder.itemFolderName,
viewHolder.feedIcon,
viewHolder.feedName,
viewHolder.itemDescription,
viewHolder.itemTitle,
viewHolder.itemImage,
viewHolder.itemReadTime,
};
if (itemWithFeed.getItem().hasImage()) {
viewHolder.itemImage.setVisibility(View.VISIBLE);
@ -156,18 +155,47 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
else
viewHolder.itemFolderName.setText(resources.getString(R.string.no_folder));
float alpha = itemWithFeed.getItem().isRead() ? 0.5f : 1.0f;
for (View view : alphaViews) {
view.setAlpha(alpha);
}
viewHolder.setReadState(itemWithFeed.getItem().isRead());
viewHolder.setSelected(selection.contains(viewHolder.getAdapterPosition()));
}
@Override
public long getItemId(int position) {
return getItem(position).getItem().getId();
}
public void toggleSelection(int position) {
if (selection.contains(position))
selection.remove(position);
else
selection.add(position);
notifyItemChanged(position);
}
public void clearSelection() {
selection.clear();
notifyDataSetChanged();
}
public LinkedHashSet<Integer> getSelection() {
return selection;
}
public void updateSelection(boolean read) {
for (int position : selection) {
ItemWithFeed itemWithFeed = getItem(position);
itemWithFeed.getItem().setRead(read);
notifyItemChanged(position, itemWithFeed);
}
}
public ItemWithFeed getItemWithFeed(int i) {
return getItem(i);
}
@NonNull
@Override
public List<String> getPreloadItems(int position) {
@ -177,7 +205,6 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
} else {
return Collections.emptyList();
}
}
@Nullable
@ -192,6 +219,7 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
public interface OnItemClickListener {
void onItemClick(ItemWithFeed itemWithFeed, int position);
void onItemLongClick(ItemWithFeed itemWithFeed, int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
@ -210,6 +238,8 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
private TextView itemFolderName;
private RelativeLayout dateLayout;
View[] alphaViews;
ItemViewHolder(@NonNull View itemView) {
super(itemView);
@ -220,6 +250,15 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
listener.onItemClick(getItem(position), position);
}));
itemView.setOnLongClickListener(v -> {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION)
listener.onItemLongClick(getItem(position), position);
return true;
});
itemTitle = itemView.findViewById(R.id.item_title);
date = itemView.findViewById(R.id.item_date);
feedName = itemView.findViewById(R.id.item_feed_title);
@ -229,6 +268,17 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
itemReadTime = itemView.findViewById(R.id.item_readtime);
itemFolderName = itemView.findViewById(R.id.item_folder_name);
dateLayout = itemView.findViewById(R.id.item_date_layout);
alphaViews = new View[] {
dateLayout,
itemFolderName,
feedIcon,
feedName,
itemDescription,
itemTitle,
itemImage,
itemReadTime
};
}
private void bind(ItemWithFeed itemWithFeed) {
@ -245,6 +295,27 @@ public class MainItemListAdapter extends ListAdapter<ItemWithFeed, MainItemListA
itemDescription.setVisibility(View.GONE);
}
private void setReadState(boolean isRead) {
float alpha = isRead ? 0.5f : 1.0f;
for (View view : alphaViews) {
view.setAlpha(alpha);
}
}
private void setSelected(boolean selected) {
Context context = itemView.getContext();
if (selected)
itemView.setBackground(new ColorDrawable(ContextCompat.getColor(context, R.color.selected_background)));
else {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(
android.R.attr.selectableItemBackground, outValue, true);
itemView.setBackgroundResource(outValue.resourceId);
}
}
public ImageView getItemImage() {
return itemImage;
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_mark_unread"
android:title="@string/unread"
android:icon="@drawable/ic_unread"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/item_mark_read"
android:title="@string/read"
android:icon="@drawable/ic_read"
app:showAsAction="ifRoom" />
</menu>

View File

@ -47,5 +47,7 @@
<item>Du plus récent au plus ancien</item>
<item>Du plus ancien au plus récent</item>
</string-array>
<string name="unread">Marquer comme non lu</string>
<string name="read">Marquer comme lu</string>
</resources>

View File

@ -6,4 +6,6 @@
<color name="colorControlNormal">#d7d7d7</color>
<color name="colorBackground">#fafafa</color>
<color name="textColorPrimary">#000000</color>
<color name="selected_background">#E0E0E0</color>
</resources>

View File

@ -49,4 +49,6 @@
<item>Newest to oldest</item>
<item>Oldest to newsest</item>
</string-array>
<string name="unread">Mark as non read</string>
<string name="read">Mark as read</string>
</resources>

View File

@ -14,6 +14,7 @@
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="TextAppearance.Design.CollapsingToolbar.Expanded.Custom" parent="TextAppearance.Design.CollapsingToolbar.Expanded">