From a66b3f70903392512c6ab96bef06d1e6a8c22c98 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 3 Dec 2019 21:07:31 +0100 Subject: [PATCH] allow sorting of automation tasks --- .../general/automation/AutomationFragment.kt | 19 ++++- .../general/automation/EventListAdapter.java | 76 ++++++++++++++--- .../dragHelpers/ItemTouchHelperAdapter.kt | 33 +++++++ .../dragHelpers/ItemTouchHelperViewHolder.kt | 20 +++++ .../dragHelpers/OnStartDragListener.kt | 12 +++ .../SimpleItemTouchHelperCallback.kt | 85 +++++++++++++++++++ .../res/drawable/ic_reorder_gray_24dp.xml | 5 ++ .../main/res/layout/automation_event_item.xml | 10 ++- app/src/main/res/values/strings.xml | 1 + 9 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperAdapter.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperViewHolder.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/OnStartDragListener.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/SimpleItemTouchHelperCallback.kt create mode 100644 app/src/main/res/drawable/ic_reorder_gray_24dp.xml diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt index d1135c21ed..5266c0781c 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.kt @@ -5,10 +5,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import info.nightscout.androidaps.R import info.nightscout.androidaps.plugins.bus.RxBus import info.nightscout.androidaps.plugins.general.automation.dialogs.EditEventDialog +import info.nightscout.androidaps.plugins.general.automation.dragHelpers.OnStartDragListener +import info.nightscout.androidaps.plugins.general.automation.dragHelpers.SimpleItemTouchHelperCallback import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationDataChanged import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationUpdateGui import info.nightscout.androidaps.utils.FabricPrivacy @@ -17,11 +21,14 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import kotlinx.android.synthetic.main.automation_fragment.* -class AutomationFragment : Fragment() { + +class AutomationFragment : Fragment(), OnStartDragListener { private var disposable: CompositeDisposable = CompositeDisposable() private var eventListAdapter: EventListAdapter? = null + private var itemTouchHelper: ItemTouchHelper? = null + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.automation_fragment, container, false) } @@ -29,7 +36,7 @@ class AutomationFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - eventListAdapter = EventListAdapter(AutomationPlugin.automationEvents, fragmentManager, activity) + eventListAdapter = EventListAdapter(AutomationPlugin.automationEvents, fragmentManager, activity, this) automation_eventListView.layoutManager = LinearLayoutManager(context) automation_eventListView.adapter = eventListAdapter @@ -42,6 +49,10 @@ class AutomationFragment : Fragment() { fragmentManager?.let { dialog.show(it, "EditEventDialog") } } + val callback: ItemTouchHelper.Callback = SimpleItemTouchHelperCallback(eventListAdapter!!) + itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper?.attachToRecyclerView(automation_eventListView) + } @Synchronized @@ -81,4 +92,8 @@ class AutomationFragment : Fragment() { automation_logView?.text = sb.toString() } + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { + itemTouchHelper?.startDrag(viewHolder); + } + } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/EventListAdapter.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/EventListAdapter.java index 08079a9110..87c78d02eb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/EventListAdapter.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/EventListAdapter.java @@ -1,9 +1,12 @@ package info.nightscout.androidaps.plugins.general.automation; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; +import android.graphics.Color; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; @@ -17,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -25,19 +29,26 @@ import info.nightscout.androidaps.R; import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.plugins.general.automation.actions.Action; import info.nightscout.androidaps.plugins.general.automation.dialogs.EditEventDialog; +import info.nightscout.androidaps.plugins.general.automation.dragHelpers.ItemTouchHelperAdapter; +import info.nightscout.androidaps.plugins.general.automation.dragHelpers.ItemTouchHelperViewHolder; +import info.nightscout.androidaps.plugins.general.automation.dragHelpers.OnStartDragListener; import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationDataChanged; +import info.nightscout.androidaps.plugins.general.automation.events.EventAutomationUpdateGui; import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector; import info.nightscout.androidaps.utils.OKDialog; -class EventListAdapter extends RecyclerView.Adapter { +class EventListAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { private final List eventList; private final FragmentManager fragmentManager; private final Activity activity; - EventListAdapter(List events, FragmentManager fragmentManager, Activity activity) { + private final OnStartDragListener mDragStartListener; + + EventListAdapter(List events, FragmentManager fragmentManager, Activity activity, OnStartDragListener dragStartListener) { this.eventList = events; this.fragmentManager = fragmentManager; this.activity = activity; + mDragStartListener = dragStartListener; } @NonNull @@ -54,6 +65,7 @@ class EventListAdapter extends RecyclerView.Adapter layout.addView(iv); } + @SuppressLint("ClickableViewAccessibility") @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { final AutomationEvent event = eventList.get(position); @@ -91,16 +103,10 @@ class EventListAdapter extends RecyclerView.Adapter RxBus.INSTANCE.send(new EventAutomationDataChanged()); }); - // remove event - holder.iconTrash.setOnClickListener(v -> - OKDialog.showConfirmation(activity, MainApp.gs(R.string.removerecord) + " " + event.getTitle(), () -> { - eventList.remove(event); - RxBus.INSTANCE.send(new EventAutomationDataChanged()); - }) - ); - // edit event - holder.rootLayout.setOnClickListener(v -> { + holder.rootLayout.setOnClickListener(v -> + + { //EditEventDialog dialog = EditEventDialog.Companion.newInstance(event, false); EditEventDialog dialog = new EditEventDialog(); Bundle args = new Bundle(); @@ -110,6 +116,17 @@ class EventListAdapter extends RecyclerView.Adapter if (fragmentManager != null) dialog.show(fragmentManager, "EditEventDialog"); }); + + // Start a drag whenever the handle view it touched + holder.iconSort.setOnTouchListener((v, motionEvent) -> + + { + if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { + mDragStartListener.onStartDrag(holder); + return true; + } + return v.onTouchEvent(motionEvent); + }); } @Override @@ -117,12 +134,33 @@ class EventListAdapter extends RecyclerView.Adapter return eventList.size(); } - static class ViewHolder extends RecyclerView.ViewHolder { + @Override + public boolean onItemMove(int fromPosition, int toPosition) { + Collections.swap(eventList, fromPosition, toPosition); + notifyItemMoved(fromPosition, toPosition); + RxBus.INSTANCE.send(new EventAutomationDataChanged()); + return true; + } + + @Override + public void onItemDismiss(int position) { + OKDialog.showConfirmation(activity, MainApp.gs(R.string.removerecord) + " " + eventList.get(position).getTitle(), + () -> { + eventList.remove(position); + notifyItemRemoved(position); + RxBus.INSTANCE.send(new EventAutomationDataChanged()); + RxBus.INSTANCE.send(new EventAutomationUpdateGui()); + }, () -> { + RxBus.INSTANCE.send(new EventAutomationUpdateGui()); + }); + } + + static class ViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder { final RelativeLayout rootLayout; final LinearLayout iconLayout; final TextView eventTitle; final Context context; - final ImageView iconTrash; + final ImageView iconSort; final CheckBox enabled; ViewHolder(View view, Context context) { @@ -131,8 +169,18 @@ class EventListAdapter extends RecyclerView.Adapter eventTitle = view.findViewById(R.id.viewEventTitle); rootLayout = view.findViewById(R.id.rootLayout); iconLayout = view.findViewById(R.id.iconLayout); - iconTrash = view.findViewById(R.id.iconTrash); + iconSort = view.findViewById(R.id.iconSort); enabled = view.findViewById(R.id.automation_enabled); } + + @Override + public void onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY); + } + + @Override + public void onItemClear() { + itemView.setBackgroundColor(MainApp.gc(R.color.ribbonDefault)); + } } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperAdapter.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperAdapter.kt new file mode 100644 index 0000000000..bdd68080ef --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperAdapter.kt @@ -0,0 +1,33 @@ +package info.nightscout.androidaps.plugins.general.automation.dragHelpers + + +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 **not** at the end of a "drop" event.

+ *

+ * Implementations should call [RecyclerView.Adapter.notifyItemMoved] 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. + * @return True if the item was moved to the new adapter position. + * + * @see RecyclerView.getAdapterPositionFor + * @see RecyclerView.ViewHolder.getAdapterPosition + */ + fun onItemMove(fromPosition: Int, toPosition: Int): Boolean + + /** + * Called when an item has been dismissed by a swipe.

+ *

+ * Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after + * adjusting the underlying data to reflect this removal. + * + * @param position The position of the item dismissed. + * + * @see RecyclerView.getAdapterPositionFor + * @see RecyclerView.ViewHolder.getAdapterPosition + */ + fun onItemDismiss(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperViewHolder.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperViewHolder.kt new file mode 100644 index 0000000000..a873e5253d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/ItemTouchHelperViewHolder.kt @@ -0,0 +1,20 @@ +package info.nightscout.androidaps.plugins.general.automation.dragHelpers + +/** + * Interface to notify an item ViewHolder of relevant callbacks from [ ]. + * + * @author Paul Burke (ipaulpro) + */ +interface ItemTouchHelperViewHolder { + /** + * Called when the [ItemTouchHelper] first registers an item as being moved or swiped. + * Implementations should update the item view to indicate it's active state. + */ + fun onItemSelected() + + /** + * Called when the [ItemTouchHelper] has completed the move or swipe, and the active item + * state should be cleared. + */ + fun onItemClear() +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/OnStartDragListener.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/OnStartDragListener.kt new file mode 100644 index 0000000000..9a90a360de --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/OnStartDragListener.kt @@ -0,0 +1,12 @@ +package info.nightscout.androidaps.plugins.general.automation.dragHelpers + +import androidx.recyclerview.widget.RecyclerView + +interface OnStartDragListener { + /** + * Called when a view is requesting a start of a drag. + * + * @param viewHolder The holder of the view to drag. + */ + fun onStartDrag(viewHolder: RecyclerView.ViewHolder) +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/SimpleItemTouchHelperCallback.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/SimpleItemTouchHelperCallback.kt new file mode 100644 index 0000000000..7e66046e73 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dragHelpers/SimpleItemTouchHelperCallback.kt @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.general.automation.dragHelpers + +import android.graphics.Canvas +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs + + +/** + * An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and + * swipe-to-dismiss. Drag events are automatically started by an item long-press.

+ * + * Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement + * [ItemTouchHelperViewHolder]. + * + */ +class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() { + override fun isLongPressDragEnabled(): Boolean { + return true + } + + override fun isItemViewSwipeEnabled(): Boolean { + return true + } + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { // Set movement flags based on the layout manager + return if (recyclerView.layoutManager is GridLayoutManager) { + val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + val swipeFlags = 0 + makeMovementFlags(dragFlags, swipeFlags) + } else { + val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN + val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END + makeMovementFlags(dragFlags, swipeFlags) + } + } + + override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + if (source.itemViewType != target.itemViewType) { + return false + } + // Notify the adapter of the move + mAdapter.onItemMove(source.adapterPosition, target.adapterPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, i: Int) { // Notify the adapter of the dismissal + mAdapter.onItemDismiss(viewHolder.adapterPosition) + } + + override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { // Fade out the view as it is swiped out of the parent's bounds + val alpha = ALPHA_FULL - abs(dX) / viewHolder.itemView.width.toFloat() + viewHolder.itemView.alpha = alpha + viewHolder.itemView.translationX = dX + } else { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { // We only want the active item to change + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + if (viewHolder is ItemTouchHelperViewHolder) { // Let the view holder know that this item is being moved or dragged + val itemViewHolder: ItemTouchHelperViewHolder = viewHolder + itemViewHolder.onItemSelected() + } + } + super.onSelectedChanged(viewHolder, actionState) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.alpha = ALPHA_FULL + if (viewHolder is ItemTouchHelperViewHolder) { // Tell the view holder it's time to restore the idle state + val itemViewHolder: ItemTouchHelperViewHolder = viewHolder + itemViewHolder.onItemClear() + } + } + + companion object { + const val ALPHA_FULL = 1.0f + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_reorder_gray_24dp.xml b/app/src/main/res/drawable/ic_reorder_gray_24dp.xml new file mode 100644 index 0000000000..8c12d67379 --- /dev/null +++ b/app/src/main/res/drawable/ic_reorder_gray_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/automation_event_item.xml b/app/src/main/res/layout/automation_event_item.xml index 6cbc138278..1785c8c452 100644 --- a/app/src/main/res/layout/automation_event_item.xml +++ b/app/src/main/res/layout/automation_event_item.xml @@ -8,6 +8,8 @@ android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" + android:clickable="true" + android:focusable="true" android:background="@color/ribbonDefault" android:padding="8dp"> @@ -26,21 +28,21 @@ android:layout_alignBottom="@+id/automation_enabled" android:layout_centerVertical="true" android:layout_marginTop="6dp" - android:layout_toStartOf="@+id/iconTrash" + android:layout_toStartOf="@+id/iconSort" android:layout_toEndOf="@id/automation_enabled" android:text="Title" android:textAlignment="viewStart" android:textStyle="bold" /> + android:src="@drawable/ic_reorder_gray_24dp" /> Profile name contains dots.\nThis is not supported by NS.\nProfile is not uploaded to NS. Lower value of in range area (display only) Higher value of in range area (display only) + Reorder