diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/MonsterListRecyclerViewAdapter.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/MonsterListRecyclerViewAdapter.java new file mode 100644 index 0000000..e42ecef --- /dev/null +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/MonsterListRecyclerViewAdapter.java @@ -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 { + 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 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> 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); + } + } +} diff --git a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java index eb19e35..10b31a9 100644 --- a/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java +++ b/Android/app/src/main/java/com/majinnaibu/monstercards/ui/library/LibraryFragment.java @@ -1,35 +1,159 @@ 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.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProviders; +import androidx.core.content.ContextCompat; +import androidx.navigation.NavDirections; +import androidx.navigation.Navigation; +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.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; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - libraryViewModel = - ViewModelProviders.of(this).get(LibraryViewModel.class); 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() { - @Override - public void onChanged(@Nullable String s) { - textView.setText(s); - } + + FloatingActionButton fab = root.findViewById(R.id.fab); + fab.setOnClickListener(view -> { + Monster monster = new Monster(); + 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; } + + 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); + } + } } diff --git a/Android/app/src/main/res/layout/monster_list_content.xml b/Android/app/src/main/res/layout/monster_list_content.xml index b45ad3b..395ec3e 100644 --- a/Android/app/src/main/res/layout/monster_list_content.xml +++ b/Android/app/src/main/res/layout/monster_list_content.xml @@ -1,14 +1,20 @@ - - + + + \ No newline at end of file diff --git a/Android/app/src/main/res/values/colors.xml b/Android/app/src/main/res/values/colors.xml index 4faecfa..b91e898 100644 --- a/Android/app/src/main/res/values/colors.xml +++ b/Android/app/src/main/res/values/colors.xml @@ -1,6 +1,7 @@ - #6200EE - #3700B3 - #03DAC5 + #9B2818 + #661A10 + + #995500 \ No newline at end of file diff --git a/Android/app/src/main/res/values/dimens.xml b/Android/app/src/main/res/values/dimens.xml index e00c2dd..bcff9f6 100644 --- a/Android/app/src/main/res/values/dimens.xml +++ b/Android/app/src/main/res/values/dimens.xml @@ -2,4 +2,7 @@ 16dp 16dp + 16dp + 16dp + \ No newline at end of file