Adds swipe to delete monsters on the library screen.
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user