Adds swipe to delete monsters on the library screen.

This commit is contained in:
2021-05-01 00:48:17 -07:00
committed by Tom Hicks
parent eec695bfc8
commit 0cbf6022c4
5 changed files with 275 additions and 22 deletions

View File

@@ -0,0 +1,119 @@
package com.majinnaibu.monstercards.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.RecyclerView;
import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.library.LibraryFragment;
import com.majinnaibu.monstercards.ui.library.LibraryFragmentDirections;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class MonsterListRecyclerViewAdapter extends RecyclerView.Adapter<MonsterListRecyclerViewAdapter.ViewHolder> {
public interface ItemCallback {
void onItem(Monster monster);
}
// TODO: Replace SimpleItemRecyclerViewAdapter with something better like MonsterListRecyclerViewAdapter that can be reused in search
private final LibraryFragment mParentActivity;
private List<Monster> mValues;
private final boolean mTwoPane;
private final Context mContext;
private final ItemCallback mOnDelete;
private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
Monster monster = (Monster) view.getTag();
// TODO: I would like to call navigateToMonsterDetail(item.id) here
if (mTwoPane) {
// TODO: Figure out how to navigate to a MonsterDetailFragment when in two pane view.
// Bundle arguments = new Bundle();
// arguments.putString(ItemDetailFragment.ARG_ITEM_ID, monster.id.toString());
// ItemDetailFragment fragment = new ItemDetailFragment();
// fragment.setArguments(arguments);
// mParentActivity.getSupportFragmentManager().beginTransaction()
// .replace(R.id.item_detail_container, fragment)
// .commit();
} else {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monster.id.toString());
Navigation.findNavController(view).navigate(action);
}
}
};
public MonsterListRecyclerViewAdapter(LibraryFragment parent,
Flowable<List<Monster>> itemsObservable,
ItemCallback onDelete,
boolean twoPane) {
mValues = new ArrayList<>();
mParentActivity = parent;
mTwoPane = twoPane;
mContext = parent.getContext();
mOnDelete = onDelete;
itemsObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(monsters -> {
mValues = monsters;
notifyDataSetChanged();
});
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.monster_list_content, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mIdView.setText(mValues.get(position).id.toString().substring(0, 6));
holder.mContentView.setText(mValues.get(position).name);
holder.itemView.setTag(mValues.get(position));
holder.itemView.setOnClickListener(mOnClickListener);
}
@Override
public int getItemCount() {
return mValues.size();
}
public Context getContext() {
return mContext;
}
class ViewHolder extends RecyclerView.ViewHolder {
final TextView mIdView;
final TextView mContentView;
ViewHolder(View view) {
super(view);
mIdView = view.findViewById(R.id.id_text);
mContentView = view.findViewById(R.id.content);
}
}
public void deleteItem(int position) {
if (mOnDelete != null) {
Monster monster = mValues.get(position);
mOnDelete.onItem(monster);
}
}
}

View File

@@ -1,35 +1,159 @@
package com.majinnaibu.monstercards.ui.library; package com.majinnaibu.monstercards.ui.library;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.navigation.NavDirections;
import androidx.lifecycle.Observer; import androidx.navigation.Navigation;
import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.data.MonsterRepository;
import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.ui.MCFragment;
import com.majinnaibu.monstercards.ui.MonsterListRecyclerViewAdapter;
import com.majinnaibu.monstercards.utils.Logger;
public class LibraryFragment extends Fragment { import java.util.UUID;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class LibraryFragment extends MCFragment {
private LibraryViewModel libraryViewModel; private LibraryViewModel libraryViewModel;
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
libraryViewModel =
ViewModelProviders.of(this).get(LibraryViewModel.class);
View root = inflater.inflate(R.layout.fragment_library, container, false); View root = inflater.inflate(R.layout.fragment_library, container, false);
final TextView textView = root.findViewById(R.id.text_library);
libraryViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() { FloatingActionButton fab = root.findViewById(R.id.fab);
@Override fab.setOnClickListener(view -> {
public void onChanged(@Nullable String s) { Monster monster = new Monster();
textView.setText(s); monster.name = "Unnamed Monster";
} MonsterRepository repository = this.getMonsterRepository();
repository.addMonster(monster)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Snackbar.make(
getView(),
String.format("%s created", monster.name),
Snackbar.LENGTH_LONG)
.setAction("Action", (_view) -> {
navigateToMonsterDetail(monster.id);
})
.show();
}, throwable -> {
Logger.logError("Error creating monster", throwable);
Snackbar.make(getView(), "Failed to create monster", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
});
}); });
RecyclerView recyclerView = root.findViewById(R.id.monster_list);
assert recyclerView != null;
setupRecyclerView(recyclerView);
return root; return root;
} }
private void setupRecyclerView(@NonNull RecyclerView recyclerView) {
MonsterRepository repository = this.getMonsterRepository();
boolean mTwoPane = false;
MonsterListRecyclerViewAdapter adapter = new MonsterListRecyclerViewAdapter(
this,
repository.getMonsters(),
(monster) -> {
repository
.deleteMonster(monster)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Logger.logDebug("deleted");
}, Logger::logError);
},
mTwoPane);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeToDeleteMonsterCallback(adapter));
itemTouchHelper.attachToRecyclerView(recyclerView);
}
protected void navigateToMonsterDetail(UUID monsterId) {
NavDirections action = LibraryFragmentDirections.actionNavigationLibraryToNavigationMonster(monsterId.toString());
Navigation.findNavController(getView()).navigate(action);
}
public static class SwipeToDeleteMonsterCallback extends ItemTouchHelper.SimpleCallback {
private final MonsterListRecyclerViewAdapter mAdapter;
private final Drawable icon;
private final ColorDrawable background;
private final Paint clearPaint;
public SwipeToDeleteMonsterCallback(MonsterListRecyclerViewAdapter adapter) {
super(0, ItemTouchHelper.LEFT);
mAdapter = adapter;
icon = ContextCompat.getDrawable(mAdapter.getContext(), R.drawable.ic_delete_white_36);
background = new ColorDrawable(Color.RED);
clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
mAdapter.deleteItem(position);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
View itemView = viewHolder.itemView;
int itemHeight = itemView.getBottom() - itemView.getTop();
boolean isCancelled = dX == 0 && !isCurrentlyActive;
if (isCancelled) {
c.drawRect(itemView.getRight() + dX, itemView.getTop(), itemView.getRight(), itemView.getBottom(), clearPaint);
return;
}
// Draw the red delete background
background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
background.draw(c);
// Calculate position of delete icon
int iconHeight = icon.getIntrinsicHeight();
int iconWidth = icon.getIntrinsicWidth();
int iconTop = itemView.getTop() + (itemHeight - iconHeight) / 2;
int iconMargin = (itemHeight - iconHeight) / 2;
int iconLeft = itemView.getRight() - iconMargin - iconWidth;
int iconRight = itemView.getRight() - iconMargin;
int iconBottom = iconTop + iconHeight;
// Draw the icon
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom);
icon.draw(c);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
} }

View File

@@ -1,14 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<!-- // TODO: combine all of these similar list layouts into a single one -->
<TextView <TextView
android:id="@+id/content" android:id="@+id/id_text"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin" android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" /> android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#6200EE</color> <color name="colorPrimary">#9B2818</color>
<color name="colorPrimaryDark">#3700B3</color> <color name="colorPrimaryDark">#661A10</color>
<color name="colorAccent">#03DAC5</color> <!-- <color name="colorAccent">#188B9B</color>-->
<color name="colorAccent">#995500</color>
</resources> </resources>

View File

@@ -2,4 +2,7 @@
<!-- Default screen margins, per the Android Design guidelines. --> <!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources> </resources>