Repo created
This commit is contained in:
parent
61d62cabdf
commit
ad3df69bdb
872 changed files with 65976 additions and 2 deletions
15
cross-tab-drag-and-drop/build.gradle
Normal file
15
cross-tab-drag-and-drop/build.gradle
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
namespace 'it.niedermann.android.crosstabdnd'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 22
|
||||
targetSdk 35
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.google.android.material:material:$rootProject.materialVersion"
|
||||
}
|
||||
1
cross-tab-drag-and-drop/src/main/AndroidManifest.xml
Normal file
1
cross-tab-drag-and-drop/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<manifest />
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.util.Log;
|
||||
import android.view.DragEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public class CrossTabDragAndDrop<
|
||||
TabFragment extends Fragment & DragAndDropTab<ItemAdapter>,
|
||||
ItemAdapter extends RecyclerView.Adapter<?> & DragAndDropAdapter<ItemModel>,
|
||||
ItemModel extends DragAndDropModel> {
|
||||
|
||||
private static final String TAG = CrossTabDragAndDrop.class.getCanonicalName();
|
||||
private static final ScrollHelper SCROLL_HELPER = new ScrollHelper();
|
||||
|
||||
private final boolean isLayoutLtr;
|
||||
private final float pxToReact;
|
||||
private final float pxToReactTopBottom;
|
||||
private final int dragAndDropMsToReact;
|
||||
private final int dragAndDropMsToReactTopBottom;
|
||||
private final int displayX;
|
||||
private long lastSwap = 0;
|
||||
private long lastMove = 0;
|
||||
|
||||
private final Set<ItemMovedByDragListener<ItemModel>> moveListenerList = new HashSet<>(1);
|
||||
|
||||
public CrossTabDragAndDrop(@NonNull Resources resources, boolean isLayoutLtr) {
|
||||
this.displayX = resources.getDisplayMetrics().widthPixels;
|
||||
final float density = resources.getDisplayMetrics().density;
|
||||
this.pxToReact = resources.getInteger(R.integer.drag_n_drop_dp_to_react) * density;
|
||||
this.dragAndDropMsToReactTopBottom = resources.getInteger(R.integer.drag_n_drop_dp_to_react_top_bottom);
|
||||
this.pxToReactTopBottom = dragAndDropMsToReactTopBottom * density;
|
||||
this.dragAndDropMsToReact = resources.getInteger(R.integer.drag_n_drop_ms_to_react);
|
||||
this.isLayoutLtr = isLayoutLtr;
|
||||
}
|
||||
|
||||
public void register(final ViewPager2 viewPager, TabLayout stackLayout, FragmentManager fm) {
|
||||
viewPager.setOnDragListener((View v, DragEvent dragEvent) -> {
|
||||
//noinspection unchecked
|
||||
final DraggedItemLocalState<TabFragment, ItemAdapter, ItemModel> draggedItemLocalState = (DraggedItemLocalState<TabFragment, ItemAdapter, ItemModel>) dragEvent.getLocalState();
|
||||
// https://github.com/stefan-niedermann/nextcloud-deck/issues/1025
|
||||
if (draggedItemLocalState == null) {
|
||||
Log.v(TAG, "dragEvent has no localState → Cancelling DragListener.");
|
||||
return false;
|
||||
}
|
||||
final View draggedView = draggedItemLocalState.getDraggedView();
|
||||
switch (dragEvent.getAction()) {
|
||||
case DragEvent.ACTION_DRAG_STARTED: {
|
||||
draggedView.setVisibility(View.INVISIBLE);
|
||||
draggedItemLocalState.onDragStart(viewPager, fm);
|
||||
break;
|
||||
}
|
||||
case DragEvent.ACTION_DRAG_LOCATION: {
|
||||
final RecyclerView currentRecyclerView = draggedItemLocalState.getRecyclerView();
|
||||
final ItemAdapter itemAdapter = draggedItemLocalState.getItemAdapter();
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
if (lastSwap + dragAndDropMsToReact < now) { // don't change Tabs so fast!
|
||||
final int oldTabPosition = viewPager.getCurrentItem();
|
||||
|
||||
boolean shouldSwitchTab = true;
|
||||
|
||||
int newTabPosition = -1;
|
||||
// change tab? if yes, which direction?
|
||||
if (dragEvent.getX() <= pxToReact) {
|
||||
newTabPosition = isLayoutLtr ? oldTabPosition - 1 : oldTabPosition + 1;
|
||||
} else if (dragEvent.getX() >= displayX - pxToReact) {
|
||||
newTabPosition = isLayoutLtr ? oldTabPosition + 1 : oldTabPosition - 1;
|
||||
} else {
|
||||
shouldSwitchTab = false;
|
||||
}
|
||||
|
||||
if (shouldSwitchTab && isMovePossible(viewPager, newTabPosition)) {
|
||||
removeItem(currentRecyclerView, draggedView, itemAdapter);
|
||||
detectAndKillDuplicatesInNeighbourTab(viewPager, draggedItemLocalState.getDraggedItemModel(), fm, oldTabPosition, newTabPosition);
|
||||
switchTab(dragEvent, viewPager, stackLayout, fm, draggedItemLocalState, now, newTabPosition);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//scroll if needed
|
||||
if (dragEvent.getY() <= pxToReactTopBottom) {
|
||||
SCROLL_HELPER.startScroll(currentRecyclerView, ScrollHelper.ScrollDirection.UP);
|
||||
} else if (dragEvent.getY() >= currentRecyclerView.getBottom() - pxToReactTopBottom) {
|
||||
SCROLL_HELPER.startScroll(currentRecyclerView, ScrollHelper.ScrollDirection.DOWN);
|
||||
} else {
|
||||
SCROLL_HELPER.stopScroll();
|
||||
}
|
||||
|
||||
if (lastMove + dragAndDropMsToReactTopBottom < now) {
|
||||
//push around the other items
|
||||
pushAroundItems(draggedView, currentRecyclerView, dragEvent, itemAdapter, draggedItemLocalState, now);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DragEvent.ACTION_DRAG_ENDED: {
|
||||
draggedItemLocalState.getRecyclerView().removeOnChildAttachStateChangeListener(draggedItemLocalState.getInsertedListener());
|
||||
SCROLL_HELPER.stopScroll();
|
||||
draggedView.setVisibility(View.VISIBLE);
|
||||
// Clean up the original dragged view, so the next onBindViewHolder() will not display the view at the position of the original dragged view as View.INVISIBLE
|
||||
draggedItemLocalState.getOriginalDraggedView().setVisibility(View.VISIBLE);
|
||||
notifyListeners(draggedItemLocalState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void switchTab(DragEvent dragEvent, ViewPager2 viewPager, TabLayout stackLayout, FragmentManager fm, final DraggedItemLocalState<TabFragment, ItemAdapter, ItemModel> draggedItemLocalState, long now, int newPosition) {
|
||||
viewPager.setCurrentItem(newPosition);
|
||||
draggedItemLocalState.onTabChanged(viewPager, fm);
|
||||
Objects.requireNonNull(stackLayout.getTabAt(newPosition)).select();
|
||||
|
||||
final RecyclerView recyclerView = draggedItemLocalState.getRecyclerView();
|
||||
final ItemAdapter itemAdapter = draggedItemLocalState.getItemAdapter();
|
||||
|
||||
RecyclerView.OnChildAttachStateChangeListener onChildAttachStateChangeListener = new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
recyclerView.removeOnChildAttachStateChangeListener(this);
|
||||
draggedItemLocalState.setInsertedListener(null);
|
||||
view.setVisibility(View.INVISIBLE);
|
||||
draggedItemLocalState.setDraggedView(view);
|
||||
pushAroundItems(view, recyclerView, dragEvent, itemAdapter, draggedItemLocalState, now);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {/* do nothing */}
|
||||
};
|
||||
draggedItemLocalState.setInsertedListener(onChildAttachStateChangeListener);
|
||||
recyclerView.addOnChildAttachStateChangeListener(onChildAttachStateChangeListener);
|
||||
|
||||
//insert item in new tab
|
||||
@Nullable final View firstVisibleView = recyclerView.getChildAt(0);
|
||||
int positionToInsert = firstVisibleView == null ? 0 : recyclerView.getChildAdapterPosition(firstVisibleView) + 1;
|
||||
itemAdapter.insertItem(draggedItemLocalState.getDraggedItemModel(), positionToInsert);
|
||||
|
||||
lastSwap = now;
|
||||
}
|
||||
|
||||
private void pushAroundItems(@NonNull View view, RecyclerView recyclerView, DragEvent dragEvent, ItemAdapter itemAdapter, DraggedItemLocalState<TabFragment, ItemAdapter, ItemModel> draggedItemLocalState, long now) {
|
||||
@Nullable final View viewUnder = recyclerView.findChildViewUnder(dragEvent.getX(), dragEvent.getY());
|
||||
|
||||
if (viewUnder != null) {
|
||||
final int toPositon = recyclerView.getChildAdapterPosition(viewUnder);
|
||||
if (toPositon != -1) {
|
||||
final int fromPosition = recyclerView.getChildAdapterPosition(view);
|
||||
if (fromPosition != -1 && fromPosition != toPositon) {
|
||||
recyclerView.post(() -> {
|
||||
itemAdapter.moveItem(fromPosition, toPositon);
|
||||
draggedItemLocalState.setPositionInItemAdapter(toPositon);
|
||||
});
|
||||
lastMove = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMovePossible(ViewPager2 viewPager, int newPosition) {
|
||||
return newPosition < Objects.requireNonNull(viewPager.getAdapter()).getItemCount() && newPosition >= 0;
|
||||
}
|
||||
|
||||
private void detectAndKillDuplicatesInNeighbourTab(ViewPager2 viewPager, ItemModel itemToFind, FragmentManager fm, int oldTabPosition, int newTabPosition) {
|
||||
final int tabPositionToCheck = newTabPosition > oldTabPosition ? newTabPosition + 1 : newTabPosition - 1;
|
||||
|
||||
if (isMovePossible(viewPager, tabPositionToCheck)) {
|
||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
viewPager.unregisterOnPageChangeCallback(this);
|
||||
final ItemAdapter itemAdapter = DragAndDropUtil.<TabFragment>getTabFragment(fm, Objects.requireNonNull(viewPager.getAdapter()).getItemId(tabPositionToCheck)).getAdapter();
|
||||
itemAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
itemAdapter.unregisterAdapterDataObserver(this);
|
||||
final List<ItemModel> itemList = itemAdapter.getItemList();
|
||||
for (int i = 0; i < itemList.size(); i++) {
|
||||
final ItemModel c = itemList.get(i);
|
||||
if (itemToFind.getComparableId().equals(c.getComparableId())) {
|
||||
itemAdapter.removeItem(i);
|
||||
itemAdapter.notifyItemRemoved(i);
|
||||
Log.v(TAG, "DnD removed dupe at tab " + tabPositionToCheck + ": " + c.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void removeItem(RecyclerView currentRecyclerView, View view, ItemAdapter itemAdapter) {
|
||||
final int oldItemPosition = currentRecyclerView.getChildAdapterPosition(view);
|
||||
|
||||
if (oldItemPosition != -1) {
|
||||
itemAdapter.removeItem(oldItemPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListeners(DraggedItemLocalState<TabFragment, ItemAdapter, ItemModel> draggedItemLocalState) {
|
||||
for (ItemMovedByDragListener<ItemModel> listener : moveListenerList) {
|
||||
listener.onItemMoved(draggedItemLocalState.getDraggedItemModel(), draggedItemLocalState.getCurrentTabId(), draggedItemLocalState.getPositionInItemAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
public void addItemMovedByDragListener(ItemMovedByDragListener<ItemModel> listener) {
|
||||
moveListenerList.add(listener);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DragAndDropAdapter<Model> {
|
||||
|
||||
void removeItem(int position);
|
||||
|
||||
void moveItem(int fromPosition, int toPosition);
|
||||
|
||||
void insertItem(Model item, int position);
|
||||
|
||||
@NonNull
|
||||
List<Model> getItemList();
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface DragAndDropModel {
|
||||
@NonNull
|
||||
Long getComparableId();
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public interface DragAndDropTab<ItemAdapter extends RecyclerView.Adapter<?> & DragAndDropAdapter<?>> {
|
||||
|
||||
ItemAdapter getAdapter();
|
||||
|
||||
RecyclerView getRecyclerView();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
// Public util
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class DragAndDropUtil {
|
||||
|
||||
private DragAndDropUtil() {
|
||||
// Util class
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static <T> T getTabFragment(@NonNull FragmentManager fm, @Nullable Long currentStackId) throws IllegalArgumentException {
|
||||
return (T) fm.findFragmentByTag("f" + currentStackId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class DraggedItemLocalState<
|
||||
TabFragment extends Fragment & DragAndDropTab<ItemAdapter>,
|
||||
ItemAdapter extends RecyclerView.Adapter<?> & DragAndDropAdapter<ItemModel>,
|
||||
ItemModel> {
|
||||
private final ItemModel draggedCard;
|
||||
/** The original dragged view */
|
||||
private final View originalDraggedView;
|
||||
/** The currently dragged view (can change when the tab changes */
|
||||
private View draggedView;
|
||||
private ItemAdapter itemAdapter;
|
||||
private int positionInCardAdapter;
|
||||
private RecyclerView.OnChildAttachStateChangeListener insertedListener = null;
|
||||
private RecyclerView recyclerView = null;
|
||||
private long currentTabId;
|
||||
|
||||
public DraggedItemLocalState(ItemModel draggedCard, View draggedView, ItemAdapter itemAdapter, int positionInCardAdapter) {
|
||||
this.draggedCard = draggedCard;
|
||||
this.draggedView = draggedView;
|
||||
this.originalDraggedView = draggedView;
|
||||
this.itemAdapter = itemAdapter;
|
||||
this.positionInCardAdapter = positionInCardAdapter;
|
||||
}
|
||||
|
||||
protected void onDragStart(@NonNull ViewPager2 viewPager, @NonNull FragmentManager fm) {
|
||||
this.currentTabId = Objects.requireNonNull(viewPager.getAdapter()).getItemId(viewPager.getCurrentItem());
|
||||
this.recyclerView = DragAndDropUtil.<TabFragment>getTabFragment(fm, currentTabId).getRecyclerView();
|
||||
}
|
||||
|
||||
protected void onTabChanged(@NonNull ViewPager2 viewPager, @NonNull FragmentManager fm) {
|
||||
if (insertedListener != null) {
|
||||
this.recyclerView.removeOnChildAttachStateChangeListener(insertedListener);
|
||||
this.insertedListener = null;
|
||||
}
|
||||
this.currentTabId = Objects.requireNonNull(viewPager.getAdapter()).getItemId(viewPager.getCurrentItem());
|
||||
this.recyclerView = DragAndDropUtil.<TabFragment>getTabFragment(fm, currentTabId).getRecyclerView();
|
||||
//noinspection unchecked
|
||||
this.itemAdapter = (ItemAdapter) recyclerView.getAdapter();
|
||||
}
|
||||
|
||||
protected long getCurrentTabId() {
|
||||
return currentTabId;
|
||||
}
|
||||
|
||||
protected ItemModel getDraggedItemModel() {
|
||||
return draggedCard;
|
||||
}
|
||||
|
||||
protected View getOriginalDraggedView() {
|
||||
return originalDraggedView;
|
||||
}
|
||||
|
||||
protected View getDraggedView() {
|
||||
return draggedView;
|
||||
}
|
||||
|
||||
protected void setDraggedView(View draggedView) {
|
||||
this.draggedView = draggedView;
|
||||
}
|
||||
|
||||
protected ItemAdapter getItemAdapter() {
|
||||
return itemAdapter;
|
||||
}
|
||||
|
||||
protected int getPositionInItemAdapter() {
|
||||
return positionInCardAdapter;
|
||||
}
|
||||
|
||||
protected void setPositionInItemAdapter(int positionInCardAdapter) {
|
||||
this.positionInCardAdapter = positionInCardAdapter;
|
||||
}
|
||||
|
||||
protected void setInsertedListener(RecyclerView.OnChildAttachStateChangeListener insertedListener) {
|
||||
this.insertedListener = insertedListener;
|
||||
}
|
||||
|
||||
public RecyclerView.OnChildAttachStateChangeListener getInsertedListener() {
|
||||
return insertedListener;
|
||||
}
|
||||
|
||||
protected RecyclerView getRecyclerView() {
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
public interface ItemMovedByDragListener<ItemModel> {
|
||||
void onItemMoved(ItemModel movedItem, long tabId, int position);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package it.niedermann.android.crosstabdnd;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ScrollHelper implements Runnable {
|
||||
|
||||
private static final int SCROLL_SPEED = 200;
|
||||
|
||||
public enum ScrollDirection {
|
||||
UP,
|
||||
DOWN
|
||||
}
|
||||
|
||||
private boolean shouldScroll = false;
|
||||
private ScrollDirection scrollDirection;
|
||||
private RecyclerView currentRecyclerView;
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
public void startScroll(@NonNull RecyclerView recyclerView, @Nullable ScrollDirection scrollDirection) {
|
||||
this.scrollDirection = scrollDirection;
|
||||
this.currentRecyclerView = recyclerView;
|
||||
if (!shouldScroll) {
|
||||
this.shouldScroll = true;
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopScroll() {
|
||||
this.shouldScroll = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (scrollDirection == ScrollDirection.UP) {
|
||||
currentRecyclerView.smoothScrollBy(0, SCROLL_SPEED * -1);
|
||||
} else {
|
||||
currentRecyclerView.smoothScrollBy(0, SCROLL_SPEED);
|
||||
}
|
||||
if (shouldScroll) {
|
||||
handler.postDelayed(this, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
cross-tab-drag-and-drop/src/main/res/values/integers.xml
Normal file
9
cross-tab-drag-and-drop/src/main/res/values/integers.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- How many microseconds should be waited before switching to another tab when dragging cards -->
|
||||
<integer name="drag_n_drop_ms_to_react">500</integer>
|
||||
<!-- How many dp from border should one be away before switching to another tab when dragging cards -->
|
||||
<integer name="drag_n_drop_dp_to_react">30</integer>
|
||||
<!-- How many dp from top / bottom border should one be away before scrolling top / down when dragging cards -->
|
||||
<integer name="drag_n_drop_dp_to_react_top_bottom">100</integer>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue