Browse Source

Allow to reorder and rename categories

inorichi 9 years ago
parent
commit
3f1f9ea9f2
20 changed files with 280 additions and 20 deletions
  1. 7 0
      app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java
  2. 6 0
      app/src/main/java/eu/kanade/mangafeed/data/database/models/Category.java
  3. 9 1
      app/src/main/java/eu/kanade/mangafeed/data/database/tables/CategoryTable.java
  4. 57 0
      app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/ItemTouchHelperAdapter.java
  5. 13 0
      app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/OnStartDragListener.java
  6. 43 0
      app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/SimpleItemTouchHelperCallback.java
  7. 29 9
      app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryAdapter.java
  8. 35 4
      app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryFragment.java
  9. 14 1
      app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryHolder.java
  10. 16 0
      app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryItemTouchHelper.java
  11. 30 1
      app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryPresenter.java
  12. BIN
      app/src/main/res/drawable-hdpi/ic_reorder_grey_600_24dp.png
  13. BIN
      app/src/main/res/drawable-ldpi/ic_reorder_grey_600_24dp.png
  14. BIN
      app/src/main/res/drawable-mdpi/ic_reorder_grey_600_24dp.png
  15. BIN
      app/src/main/res/drawable-xhdpi/ic_reorder_grey_600_24dp.png
  16. BIN
      app/src/main/res/drawable-xxhdpi/ic_reorder_grey_600_24dp.png
  17. BIN
      app/src/main/res/drawable-xxxhdpi/ic_reorder_grey_600_24dp.png
  18. 14 2
      app/src/main/res/layout/item_edit_categories.xml
  19. 6 2
      app/src/main/res/menu/category_selection.xml
  20. 1 0
      app/src/main/res/values/strings.xml

+ 7 - 0
app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java

@@ -351,6 +351,7 @@ public class DatabaseHelper {
                 .listOfObjects(Category.class)
                 .withQuery(Query.builder()
                         .table(CategoryTable.TABLE)
+                        .orderBy(CategoryTable.COLUMN_ORDER)
                         .build())
                 .prepare();
     }
@@ -361,6 +362,12 @@ public class DatabaseHelper {
                 .prepare();
     }
 
+    public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
+        return db.put()
+                .objects(categories)
+                .prepare();
+    }
+
     public PreparedDeleteObject<Category> deleteCategory(Category category) {
         return db.delete()
                 .object(category)

+ 6 - 0
app/src/main/java/eu/kanade/mangafeed/data/database/models/Category.java

@@ -16,6 +16,12 @@ public class Category implements Serializable {
     @StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
     public String name;
 
+    @StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
+    public int order;
+
+    @StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
+    public int flags;
+
     public Category() {}
 
     public static Category create(String name) {

+ 9 - 1
app/src/main/java/eu/kanade/mangafeed/data/database/tables/CategoryTable.java

@@ -13,6 +13,12 @@ public class CategoryTable {
     @NonNull
     public static final String COLUMN_NAME = "name";
 
+    @NonNull
+    public static final String COLUMN_ORDER = "sort";
+
+    @NonNull
+    public static final String COLUMN_FLAGS = "flags";
+
     // This is just class with Meta Data, we don't need instances
     private CategoryTable() {
         throw new IllegalStateException("No instances please");
@@ -24,7 +30,9 @@ public class CategoryTable {
     public static String getCreateTableQuery() {
         return "CREATE TABLE " + TABLE + "("
                 + COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
-                + COLUMN_NAME + " TEXT NOT NULL"
+                + COLUMN_NAME + " TEXT NOT NULL, "
+                + COLUMN_ORDER + " INTEGER NOT NULL, "
+                + COLUMN_FLAGS + " INTEGER NOT NULL"
                 + ");";
 
     }

+ 57 - 0
app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/ItemTouchHelperAdapter.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 Paul Burke
+ *
+ * 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.
+ */
+
+package eu.kanade.mangafeed.ui.base.adapter;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+/**
+ * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
+ *
+ * @author Paul Burke (ipaulpro)
+ */
+public interface ItemTouchHelperAdapter {
+
+    /**
+     * Called when an item has been dragged far enough to trigger a move. This is called every time
+     * an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
+     * <br/>
+     * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
+     * adjusting the underlying data to reflect this move.
+     *
+     * @param fromPosition The start position of the moved item.
+     * @param toPosition   Then resolved position of the moved item.
+     *
+     * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+     * @see RecyclerView.ViewHolder#getAdapterPosition()
+     */
+    void onItemMove(int fromPosition, int toPosition);
+
+
+    /**
+     * Called when an item has been dismissed by a swipe.<br/>
+     * <br/>
+     * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
+     * adjusting the underlying data to reflect this removal.
+     *
+     * @param position The position of the item dismissed.
+     *
+     * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
+     * @see RecyclerView.ViewHolder#getAdapterPosition()
+     */
+    void onItemDismiss(int position);
+}

+ 13 - 0
app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/OnStartDragListener.java

@@ -0,0 +1,13 @@
+package eu.kanade.mangafeed.ui.base.adapter;
+
+import android.support.v7.widget.RecyclerView;
+
+public interface OnStartDragListener {
+
+    /**
+     * Called when a view is requesting a start of a drag.
+     *
+     * @param viewHolder The holder of the view to drag.
+     */
+    void onStartDrag(RecyclerView.ViewHolder viewHolder);
+}

+ 43 - 0
app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/SimpleItemTouchHelperCallback.java

@@ -0,0 +1,43 @@
+package eu.kanade.mangafeed.ui.base.adapter;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+
+public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
+
+    private final ItemTouchHelperAdapter adapter;
+
+    public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
+        this.adapter = adapter;
+    }
+
+    @Override
+    public boolean isLongPressDragEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isItemViewSwipeEnabled() {
+        return true;
+    }
+
+    @Override
+    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
+        int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
+        return makeMovementFlags(dragFlags, swipeFlags);
+    }
+
+    @Override
+    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
+                          RecyclerView.ViewHolder target) {
+        adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
+        return true;
+    }
+
+    @Override
+    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+        adapter.onItemDismiss(viewHolder.getAdapterPosition());
+    }
+
+}

+ 29 - 9
app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryAdapter.java

@@ -6,25 +6,29 @@ import android.view.ViewGroup;
 
 import com.amulyakhare.textdrawable.util.ColorGenerator;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import eu.davidea.flexibleadapter.FlexibleAdapter;
 import eu.kanade.mangafeed.R;
 import eu.kanade.mangafeed.data.database.models.Category;
+import eu.kanade.mangafeed.ui.base.adapter.ItemTouchHelperAdapter;
 
-public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> {
-    
-    private CategoryFragment fragment;
-    private ColorGenerator generator;
+public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> implements
+        ItemTouchHelperAdapter {
+
+    private final CategoryFragment fragment;
+    private final ColorGenerator generator;
 
     public CategoryAdapter(CategoryFragment fragment) {
         this.fragment = fragment;
-        setHasStableIds(true);
         generator = ColorGenerator.DEFAULT;
+        setHasStableIds(true);
     }
 
     public void setItems(List<Category> items) {
-        mItems = items;
+        mItems = new ArrayList<>(items);
         notifyDataSetChanged();
     }
 
@@ -42,7 +46,7 @@ public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> {
     public CategoryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         LayoutInflater inflater = LayoutInflater.from(fragment.getActivity());
         View v = inflater.inflate(R.layout.item_edit_categories, parent, false);
-        return new CategoryHolder(v, this, fragment);
+        return new CategoryHolder(v, this, fragment, fragment);
     }
 
     @Override
@@ -54,7 +58,23 @@ public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> {
         holder.itemView.setActivated(isSelected(position));
     }
 
-    public ColorGenerator getColorGenerator() {
-        return generator;
+    @Override
+    public void onItemMove(int fromPosition, int toPosition) {
+        if (fromPosition < toPosition) {
+            for (int i = fromPosition; i < toPosition; i++) {
+                Collections.swap(mItems, i, i + 1);
+            }
+        } else {
+            for (int i = fromPosition; i > toPosition; i--) {
+                Collections.swap(mItems, i, i - 1);
+            }
+        }
+
+        fragment.getPresenter().reorderCategories(mItems);
+    }
+
+    @Override
+    public void onItemDismiss(int position) {
+
     }
 }

+ 35 - 4
app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryFragment.java

@@ -6,6 +6,7 @@ import android.support.v4.content.res.ResourcesCompat;
 import android.support.v7.view.ActionMode;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -22,6 +23,7 @@ import eu.kanade.mangafeed.R;
 import eu.kanade.mangafeed.data.database.models.Category;
 import eu.kanade.mangafeed.ui.base.activity.BaseActivity;
 import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder;
+import eu.kanade.mangafeed.ui.base.adapter.OnStartDragListener;
 import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment;
 import eu.kanade.mangafeed.ui.decoration.DividerItemDecoration;
 import eu.kanade.mangafeed.ui.library.LibraryCategoryAdapter;
@@ -29,14 +31,15 @@ import nucleus.factory.RequiresPresenter;
 import rx.Observable;
 
 @RequiresPresenter(CategoryPresenter.class)
-public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
-        implements ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
+public class CategoryFragment extends BaseRxFragment<CategoryPresenter> implements
+        ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
 
     @Bind(R.id.categories_list) RecyclerView recycler;
     @Bind(R.id.fab) FloatingActionButton fab;
 
     private CategoryAdapter adapter;
     private ActionMode actionMode;
+    private ItemTouchHelper touchHelper;
 
     public static CategoryFragment newInstance() {
         return new CategoryFragment();
@@ -56,12 +59,17 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
         recycler.addItemDecoration(new DividerItemDecoration(
                 ResourcesCompat.getDrawable(getResources(), R.drawable.line_divider, null)));
 
+        // Touch helper to drag and reorder categories
+        touchHelper = new ItemTouchHelper(new CategoryItemTouchHelper(adapter));
+        touchHelper.attachToRecyclerView(recycler);
+
         fab.setOnClickListener(v -> {
             new MaterialDialog.Builder(getActivity())
                     .title(R.string.action_add_category)
                     .input(R.string.name, 0, false, (dialog, input) -> {
                         getPresenter().createCategory(input.toString());
-                    }).show();
+                    })
+                    .show();
         });
 
         return view;
@@ -105,6 +113,8 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
         } else {
             setContextTitle(count);
             actionMode.invalidate();
+            MenuItem editItem = actionMode.getMenu().findItem(R.id.action_edit);
+            editItem.setVisible(count == 1);
         }
     }
 
@@ -128,7 +138,10 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
         switch (item.getItemId()) {
             case R.id.action_delete:
-                getPresenter().deleteCategories(getSelectedCategories());
+                deleteCategories(getSelectedCategories());
+                return true;
+            case R.id.action_edit:
+                editCategory(getSelectedCategories().get(0));
                 return true;
         }
         return false;
@@ -147,4 +160,22 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
         }
     }
 
+    private void deleteCategories(List<Category> categories) {
+        getPresenter().deleteCategories(categories);
+    }
+
+    private void editCategory(Category category) {
+        new MaterialDialog.Builder(getActivity())
+                .title(R.string.action_rename_category)
+                .input(getString(R.string.name), category.name, false, (dialog, input) -> {
+                    getPresenter().renameCategory(category, input.toString());
+                })
+                .show();
+    }
+
+    @Override
+    public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
+        touchHelper.startDrag(viewHolder);
+    }
+
 }

+ 14 - 1
app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryHolder.java

@@ -1,5 +1,7 @@
 package eu.kanade.mangafeed.ui.library.category;
 
+import android.support.v4.view.MotionEventCompat;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -13,6 +15,7 @@ import butterknife.OnClick;
 import eu.kanade.mangafeed.R;
 import eu.kanade.mangafeed.data.database.models.Category;
 import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder;
+import eu.kanade.mangafeed.ui.base.adapter.OnStartDragListener;
 
 public class CategoryHolder extends FlexibleViewHolder {
 
@@ -20,11 +23,21 @@ public class CategoryHolder extends FlexibleViewHolder {
 
     @Bind(R.id.image) ImageView image;
     @Bind(R.id.title) TextView title;
+    @Bind(R.id.reorder) ImageView reorder;
 
-    public CategoryHolder(View view, CategoryAdapter adapter, OnListItemClickListener listener) {
+    public CategoryHolder(View view, CategoryAdapter adapter,
+                          OnListItemClickListener listener, OnStartDragListener dragListener) {
         super(view, adapter, listener);
         ButterKnife.bind(this, view);
         this.view = view;
+
+        reorder.setOnTouchListener((v, event) -> {
+            if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+                dragListener.onStartDrag(this);
+                return true;
+            }
+            return false;
+        });
     }
 
     public void onSetValues(Category category, ColorGenerator generator) {

+ 16 - 0
app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryItemTouchHelper.java

@@ -0,0 +1,16 @@
+package eu.kanade.mangafeed.ui.library.category;
+
+import eu.kanade.mangafeed.ui.base.adapter.ItemTouchHelperAdapter;
+import eu.kanade.mangafeed.ui.base.adapter.SimpleItemTouchHelperCallback;
+
+public class CategoryItemTouchHelper extends SimpleItemTouchHelperCallback {
+
+    public CategoryItemTouchHelper(ItemTouchHelperAdapter adapter) {
+        super(adapter);
+    }
+
+    @Override
+    public boolean isItemViewSwipeEnabled() {
+        return false;
+    }
+}

+ 30 - 1
app/src/main/java/eu/kanade/mangafeed/ui/library/category/CategoryPresenter.java

@@ -15,6 +15,8 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
 
     @Inject DatabaseHelper db;
 
+    private List<Category> categories;
+
     private static final int GET_CATEGORIES = 1;
 
     @Override
@@ -23,6 +25,7 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
 
         restartableLatestCache(GET_CATEGORIES,
                 () -> db.getCategories().createObservable()
+                        .doOnNext(categories -> this.categories = categories)
                         .observeOn(AndroidSchedulers.mainThread()),
                 CategoryFragment::setCategories);
 
@@ -30,10 +33,36 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
     }
 
     public void createCategory(String name) {
-        db.insertCategory(Category.create(name)).createObservable().subscribe();
+        Category cat = Category.create(name);
+
+        // Set the new item in the last position
+        int max = 0;
+        if (categories != null) {
+            for (Category cat2 : categories) {
+                if (cat2.order > max) {
+                    max = cat2.order + 1;
+                }
+            }
+        }
+        cat.order = max;
+
+        db.insertCategory(cat).createObservable().subscribe();
     }
 
     public void deleteCategories(List<Category> categories) {
         db.deleteCategories(categories).createObservable().subscribe();
     }
+
+    public void reorderCategories(List<Category> categories) {
+        for (int i = 0; i < categories.size(); i++) {
+            categories.get(i).order = i;
+        }
+
+        db.insertCategories(categories).createObservable().subscribe();
+    }
+
+    public void renameCategory(Category category, String name) {
+        category.name = name;
+        db.insertCategory(category).createObservable().subscribe();
+    }
 }

BIN
app/src/main/res/drawable-hdpi/ic_reorder_grey_600_24dp.png


BIN
app/src/main/res/drawable-ldpi/ic_reorder_grey_600_24dp.png


BIN
app/src/main/res/drawable-mdpi/ic_reorder_grey_600_24dp.png


BIN
app/src/main/res/drawable-xhdpi/ic_reorder_grey_600_24dp.png


BIN
app/src/main/res/drawable-xxhdpi/ic_reorder_grey_600_24dp.png


BIN
app/src/main/res/drawable-xxxhdpi/ic_reorder_grey_600_24dp.png


+ 14 - 2
app/src/main/res/layout/item_edit_categories.xml

@@ -21,14 +21,26 @@
         android:layout_marginRight="@dimen/margin_right"
         android:layout_marginEnd="@dimen/margin_right"/>
 
+    <ImageView
+        android:id="@+id/reorder"
+        android:layout_width="50dp"
+        android:layout_height="50dp"
+        android:layout_marginLeft="@dimen/margin_left"
+        android:layout_marginStart="@dimen/margin_left"
+        android:layout_marginRight="@dimen/margin_right"
+        android:layout_marginEnd="@dimen/margin_right"
+        android:scaleType="center"
+        android:layout_alignParentRight="true"
+        android:src="@drawable/ic_reorder_grey_600_24dp"/>
+
     <TextView
         android:id="@+id/title"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginRight="@dimen/margin_right"
-        android:layout_marginEnd="@dimen/margin_right"
         android:layout_toRightOf="@id/image"
         android:layout_toEndOf="@id/image"
+        android:layout_toLeftOf="@id/reorder"
+        android:layout_toStartOf="@id/reorder"
         android:layout_centerInParent="true"
         android:ellipsize="end"
         android:maxLines="1"

+ 6 - 2
app/src/main/res/menu/category_selection.xml

@@ -3,10 +3,14 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto">
 
+    <item android:id="@+id/action_edit"
+          android:title="@string/action_edit"
+          android:icon="@drawable/ic_create"
+          app:showAsAction="ifRoom"/>
+
     <item android:id="@+id/action_delete"
           android:title="@string/action_delete"
           android:icon="@drawable/ic_action_delete"
-          android:orderInCategory="1"
-          app:showAsAction="always"/>
+          app:showAsAction="ifRoom"/>
 
 </menu>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -24,6 +24,7 @@
     <string name="action_edit">Edit</string>
     <string name="action_add_category">Add category</string>
     <string name="action_edit_categories">Edit categories</string>
+    <string name="action_rename_category">Rename category</string>
     <string name="action_sort_up">Sort up</string>
     <string name="action_sort_down">Sort down</string>
     <string name="action_show_unread">Unread</string>