在 Recyclerview 中拖动项目时自动滚动滚动条
Auto-scroll Scrollbar While Drag item In Recyclerview
滚动视图中的自动滚动有问题。
在我的例子中,有两个 Recyclerview。第一个 Recyclerview,可水平滚动,第二个可垂直滚动。第一个 RecyclerView 仅用于拖动,第二个 RecyclerView 仅用于放置。两个 recyclerviews 都在 ScrollView 内,所以我在第二个 Recyclerview 中禁用了垂直滚动。
我在第二个 Recyclerview 的项目上添加了 DragListener。每个项目都有拖动侦听器,所以基于我 add/replace 项目,同时放下项目。
所以我的主要问题是滚动,所以当我拖动项目时滚动无法正常工作。
目前我使用下面的代码在拖动时滚动。
case DragEvent.ACTION_DRAG_LOCATION:
RecyclerView recyclerView = (RecyclerView) viewSource.getParent();
MyAdapter adapter = (MyAdapter) recyclerView.getAdapter();
int y = Math.round(dragEvent.getY());
Timber.d("onDrag(): " + y);
int translatedY = y - adapter.getScrollDistance();
Timber.d("onDrag(): translated : " + translatedY + " getScrollDistance : " + adapter.getScrollDistance());
int threshold = 50;
// make a scrolling up due the y has passed the threshold
if (translatedY < threshold) {
// make a scroll up by 30 px
mScrollView.smoothScrollBy(0, -30);
} else
// make a autoscrolling down due y has passed the 500 px border
if (translatedY + threshold > 500) {
// make a scroll down by 30 px
mScrollView.smoothScrollBy(0, 30);
}
break;
但是如果 recyclerview 只有一个项目而不是它的工作正常,上面的代码对多个项目不能正常工作。但是当 recyclerview 有多个项目时,scrollview 会上下滚动,而 itemX 在两个项目之间。
已编辑问题:
现在,我将上面的代码放在 Second Recycler 视图的 OnDragListener 上。现在拖动监听器的问题,所以我想如果用户拖动第一个 Recyclerview 的项目 below/above 而不是 second Recyclerview 的拖动监听器 需要工作其他 Second Recyclerview 的项目的拖动监听器 需要工作。
我通过在 Second RecyclerView 的项目的拖动侦听器的 ACTION_DRAG_LOCATION 事件 中 returning false 解决了这个问题。我禁用了 ACTION_DRAG_LOCATION 事件,因此该事件未被 Second RecyclerView 的 Item 的 DragListener 跟踪。那个时候它的 Parent(Second RecyclerView) 的 Draglistener 工作。在代码下方,我设置了 Second RecyclerView 的 DragListener
case DragEvent.ACTION_DRAG_LOCATION:
RecyclerView recyclerView = (RecyclerView) viewSource.getParent();
MyAdapter adapter = (MyAdapter) recyclerView.getAdapter();
int y = Math.round(dragEvent.getY());
int translatedY = y - adapter.getScrollDistance();
int threshold = 50;
// make a scrolling up due the y has passed the threshold
if (translatedY < threshold) {
// make a scroll up by 30 px
mScrollView.smoothScrollBy(0, -30);
} else
// make a autoscrolling down due y has passed the 500 px border
if (translatedY + threshold > 500) {
// make a scroll down by 30 px
mScrollView.smoothScrollBy(0, 30);
}
break;
要禁用 Second RecyclerView 的项目的 DragListener 的 ACTION_DRAG_LOCATION 事件,请使用以下代码:
@Override
public boolean onDrag(View view, DragEvent dragEvent) {
int action = dragEvent.getAction();
View viewSource = (View) dragEvent.getLocalState();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
break;
case DragEvent.ACTION_DRAG_ENTERED:
break;
case DragEvent.ACTION_DRAG_LOCATION:
return false;
case DragEvent.ACTION_DRAG_EXITED:
break;
case DragEvent.ACTION_DROP:
break;
default:
break;
}
return true;
}
因此,无论您需要处理来自 Draglistener 的什么事件,您只需要 return true 否则 return false。
我制作了一个助手 class 可以更轻松地在 RecyclerViews 中拖放添加滚动。
它是用 C# 为 Xamarin 编写的,但将其更改为 Java 或 Kotlin 应该很简单。
RecyclerViewDragScrollHelper On GitHub
using System;
using System.Threading.Tasks;
using Android.Graphics;
using Android.OS;
using Android.Support.V7.Widget;
using Android.Views;
namespace SharedLibrary.Droid.Helpers {
public class RecyclerViewDragScrollHelper {
DragEvent dragEvent = null; //For passing the drag event to newly bound views
Handler handler = new Handler(); //to pass the run things in the ui thread
ScrollDirection currentScrollDirection = ScrollDirection.None; //the current direction being scrolled
/// <summary>
/// If this is not set then it will try to find a recycler view
/// in the hovered over view's view tree
/// </summary>
public RecyclerView RecyclerViewToScroll { get; set; }
public RecyclerViewDragScrollHelper() {
}
public RecyclerViewDragScrollHelper(RecyclerView recyclerViewToScroll) {
RecyclerViewToScroll = recyclerViewToScroll;
}
public enum UnitType {
Pixels,
Percent
}
enum ScrollDirection {
None,
Foward,
Back
}
/// <summary>
/// Threshold value can be a percentage or pixels
/// </summary>
public UnitType ThresholdUnits { get; set; } = UnitType.Percent;
/// <summary>
/// Threshold of how many pixels or percentage from the edge it has to be before it
/// starts scrolling
/// </summary>
public float Threshold { get; set; } = 0.20f;
/// <summary>
/// Speed of the scroll
/// </summary>
public int ScrollInterval { get; set; } = 5;
/// <summary>
/// Place this method in the method for event handler for View.Drag
/// Or if useing View.SetOnDragListener(), in the implimentation of
/// View.IOnDragListener
/// </summary>
/// <param name="view"></param>
/// <param name="dragEvent"></param>
/// <returns></returns>
public void HandleDrag(View view, DragEvent dragEvent) {
var action = dragEvent.Action;
switch (action) {
case DragAction.Started:
DragStarted(dragEvent);
break;
case DragAction.Ended:
DragEnded();
break;
case DragAction.Location:
var dragpoint = new Point(Convert.ToInt32(dragEvent.GetX()), Convert.ToInt32(dragEvent.GetY()));
HandleLocation(view, dragpoint);
break;
case DragAction.Exited:
currentScrollDirection = ScrollDirection.None;
break;
}
}
/// <summary>
/// Run on DragAction.Location.
/// If it's within the threshold bounds it should scroll the recycler view.
/// Should work for both vertical and horizontal linear layouts.
/// Wont currently work for other layouts
/// </summary>
/// <param name="viewHoveredOver">This is the view that is being dragged over.
/// It can be a view in a recyclerview, or the recycler view it's self</param>
/// <param name="position"></param>
/// <returns></returns>
private void HandleLocation(View viewHoveredOver, Point position) {
var recyclerView = RecyclerViewToScroll;
var viewHoveredOverIsChildOfRecyclerView = false;
//if RecyclerViewToScroll has not been set then
//see if there is a recycler view in the viewHoveredOver view tree
if (recyclerView == null) {
recyclerView = FindRecyclerViewInTree(viewHoveredOver);
viewHoveredOverIsChildOfRecyclerView = true;
} else {
viewHoveredOverIsChildOfRecyclerView = IsViewChildOfView(viewHoveredOver, recyclerView);
}
if (recyclerView != null &&
viewHoveredOverIsChildOfRecyclerView
&& recyclerView.GetLayoutManager() is LinearLayoutManager layoutManager) {
//get the drag position in the view, and work out where it is
//relative to the recycler view. (Both viewHoveredOver and recyclerView
//might be the same view, this doesn't matter)
var pointInRecyclerView = GetPointRelativeTo(viewHoveredOver, recyclerView, position);
//work out the properties for the scrolling based on if it's
//verticle or horizontal
float offSet = 0; //the x or y coord for the drag depending on the orientaion
float upperThreshold = 0; //threshold postion at the end of the recyclerview
int scrollX = 0; //the absolute amount to scroll horizontally
int scrollY = 0; //the absolute amount to scroll vertically
var layoutDirection = layoutManager.Orientation;
//If the threshold units are pixels then just take the threshold value.
//If it's percentage times the threshold by the height to get the pixels
var thresholdPixels = ThresholdUnits == UnitType.Pixels ? Threshold : Threshold * recyclerView.Height;
//If the layout direction is vertical scroll Y coord and check Y offset
//If the layout direction is horizontal scroll X coord and check X offset
if (layoutDirection == LinearLayoutManager.Vertical) {
offSet = pointInRecyclerView.Y;
upperThreshold = recyclerView.Height - thresholdPixels;
scrollY = ScrollInterval;
} else if (layoutDirection == LinearLayoutManager.Horizontal) {
offSet = pointInRecyclerView.X;
upperThreshold = recyclerView.Width - thresholdPixels;
scrollX = ScrollInterval;
}
//if the drag position is less than the lower threshold then scroll backwards
//if it's greater than the uper threshold then scroll forward
//if it's in neither, the set the scroll direction to none
var newScrollDirection = offSet < thresholdPixels
? ScrollDirection.Back
: offSet > upperThreshold
? ScrollDirection.Foward
: ScrollDirection.None;
InScrollPostion(newScrollDirection, recyclerView, scrollX, scrollY);
}
}
Java.Util.Timer timer;
/// <summary>
/// Scroll continiously in the set direction while the position is in the bounds
/// </summary>
/// <param name="newDirection"></param>
/// <param name="recyclerView"></param>
/// <param name="scrollX"></param>
/// <param name="scrollY"></param>
/// <returns></returns>
private void InScrollPostion(
ScrollDirection newDirection,
RecyclerView recyclerView,
int scrollX,
int scrollY) {
//if it's not already scrolling
if (currentScrollDirection != newDirection && newDirection != ScrollDirection.None) {
currentScrollDirection = newDirection;
//1 for forward -1 for backwards
var scrollDirectionInt = newDirection == ScrollDirection.Foward ? 1 : -1;
var canScrollDown = recyclerView.CanScrollVertically(scrollDirectionInt);
var canScrollRight = recyclerView.CanScrollHorizontally(scrollDirectionInt);
scrollX *= scrollDirectionInt;
scrollY *= scrollDirectionInt;
timer?.Cancel(); //cancel the previous timer so they don't stack
timer?.Dispose();
timer = new Java.Util.Timer(); //make a new timer
//Action to be performed every timer tick
Action action = () => {
if (currentScrollDirection == newDirection
&& (recyclerView.CanScrollVertically(scrollDirectionInt)
|| recyclerView.CanScrollHorizontally(scrollDirectionInt))) {
handler.Post(() => recyclerView.ScrollBy(scrollX, scrollY));
} else {
currentScrollDirection = ScrollDirection.None;
timer?.Cancel();
timer?.Dispose();
timer = null;
}
};
//start the timer to do the action every 25ms
timer.ScheduleAtFixedRate(new TimerTask(action), 0, 25);
//the C# async way of doing it
//while (currentScrollDirection == newDirection
// && (recyclerView.CanScrollVertically(scrollDirectionInt)
// || recyclerView.CanScrollHorizontally(scrollDirectionInt))) {
// //Not sure how this would be done in a native android way
// recyclerView.ScrollBy(scrollX, scrollY);
// await Task.Delay(25); //Go back to the ui thread for 25ms before scrolling another increment
//}
//currentScrollDirection = ScrollDirection.None;
} else if (newDirection == ScrollDirection.None) {
currentScrollDirection = ScrollDirection.None;
timer?.Cancel();
timer?.Dispose();
timer = null;
}
}
private class TimerTask : Java.Util.TimerTask {
Action action;
public TimerTask(Action action) {
this.action = action;
}
public override void Run() {
action?.Invoke(); ;
}
}
/// <summary>
/// Run this on DragAction.Ended
/// </summary>
private void DragEnded() {
currentScrollDirection = ScrollDirection.None;
dragEvent = null;
}
/// <summary>
/// Run on DragAction.Started
/// </summary>
/// <param name="dragEvent"></param>
private void DragStarted(DragEvent dragEvent) {
this.dragEvent = dragEvent;
}
/// <summary>
/// This allows a newly bound cell that was previously off screen when
/// the drag started to trigger drag events.
/// Add this to OnBindViewHolder in override of RecyclerView.Adapter
/// </summary>
/// <param name="view"></param>
public void PrepareCellView(View view) {
handler.Post(() => {
if (dragEvent != null && view.Parent is ViewGroup parent) {
parent.DispatchDragEvent(dragEvent);
}
});
}
/// <summary>
/// This allows a newly bound cell that was previously off screen when
/// the drag started to trigger drag events.
/// Add this to OnBindViewHolder in override of RecyclerView.Adapter
/// </summary>
/// <param name="view"></param>
public void PrepareCellView(RecyclerView.ViewHolder holder) {
handler.Post(() => {
if (dragEvent != null && holder.ItemView.Parent is ViewGroup parent) {
parent.DispatchDragEvent(dragEvent);
}
});
}
/// <summary>
/// Find a recycler view in the views view tree
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
private static RecyclerView FindRecyclerViewInTree(View view) {
if (view == null) {
return null;
} else if(view is RecyclerView recyclerView) {
return recyclerView;
} else {
return FindRecyclerViewInTree(view.Parent as View);
}
}
private static bool IsViewChildOfView(View child, View parent) {
if (child == null || parent == null) {
return false;
} else if(child.Parent is View childsParent && childsParent == parent) {
return true;
} else {
return IsViewChildOfView(child.Parent as View, parent);
}
}
/// <summary>
/// Get's a point in a view relative to another view
/// </summary>
/// <param name="fromView"></param>
/// <param name="toView"></param>
/// <param name="fromPoint"></param>
/// <returns></returns>
public static Point GetPointRelativeTo(View fromView, View toView, Point fromPoint) {
var toViewRect = GetRectOnScreen(toView);
var fromViewRect = GetRectOnScreen(fromView);
var pointInToView = new Point(fromViewRect.Left - toViewRect.Left + fromPoint.X,
fromViewRect.Top - toViewRect.Top + fromPoint.Y);
return pointInToView;
}
/// <summary>
/// Gets the views Rect relative to the screen
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
public static Rect GetRectOnScreen(View view) {
int[] l = new int[2];
view.GetLocationOnScreen(l);
int x = l[0];
int y = l[1];
Rect rect = new Rect(x, y, view.Width + x, view.Height + y);
return rect;
}
}
}
将 RecyclerViewDragScrollHelper 实例添加到您的 class,并将 recyclerDragScroller.HandleDrag() 添加到您对 onDrag() 的覆盖。
@Override
public boolean onDrag(View view, DragEvent dragEvent) {
recyclerDragScroller.HandleDrag(view, dragEvent);
return true;
}
如果在拖动开始时未绑定视图持有者,它不会与拖动交互,因此将 recyclerDragScroller.PrepareCellView() 添加到适配器中的 OnBindViewHolder() 覆盖以传递拖动事件。
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
recyclerDragScroller.PrepareCellView(viewHolder);
}
滚动视图中的自动滚动有问题。
在我的例子中,有两个 Recyclerview。第一个 Recyclerview,可水平滚动,第二个可垂直滚动。第一个 RecyclerView 仅用于拖动,第二个 RecyclerView 仅用于放置。两个 recyclerviews 都在 ScrollView 内,所以我在第二个 Recyclerview 中禁用了垂直滚动。 我在第二个 Recyclerview 的项目上添加了 DragListener。每个项目都有拖动侦听器,所以基于我 add/replace 项目,同时放下项目。
所以我的主要问题是滚动,所以当我拖动项目时滚动无法正常工作。 目前我使用下面的代码在拖动时滚动。
case DragEvent.ACTION_DRAG_LOCATION:
RecyclerView recyclerView = (RecyclerView) viewSource.getParent();
MyAdapter adapter = (MyAdapter) recyclerView.getAdapter();
int y = Math.round(dragEvent.getY());
Timber.d("onDrag(): " + y);
int translatedY = y - adapter.getScrollDistance();
Timber.d("onDrag(): translated : " + translatedY + " getScrollDistance : " + adapter.getScrollDistance());
int threshold = 50;
// make a scrolling up due the y has passed the threshold
if (translatedY < threshold) {
// make a scroll up by 30 px
mScrollView.smoothScrollBy(0, -30);
} else
// make a autoscrolling down due y has passed the 500 px border
if (translatedY + threshold > 500) {
// make a scroll down by 30 px
mScrollView.smoothScrollBy(0, 30);
}
break;
但是如果 recyclerview 只有一个项目而不是它的工作正常,上面的代码对多个项目不能正常工作。但是当 recyclerview 有多个项目时,scrollview 会上下滚动,而 itemX 在两个项目之间。
已编辑问题: 现在,我将上面的代码放在 Second Recycler 视图的 OnDragListener 上。现在拖动监听器的问题,所以我想如果用户拖动第一个 Recyclerview 的项目 below/above 而不是 second Recyclerview 的拖动监听器 需要工作其他 Second Recyclerview 的项目的拖动监听器 需要工作。
我通过在 Second RecyclerView 的项目的拖动侦听器的 ACTION_DRAG_LOCATION 事件 中 returning false 解决了这个问题。我禁用了 ACTION_DRAG_LOCATION 事件,因此该事件未被 Second RecyclerView 的 Item 的 DragListener 跟踪。那个时候它的 Parent(Second RecyclerView) 的 Draglistener 工作。在代码下方,我设置了 Second RecyclerView 的 DragListener
case DragEvent.ACTION_DRAG_LOCATION:
RecyclerView recyclerView = (RecyclerView) viewSource.getParent();
MyAdapter adapter = (MyAdapter) recyclerView.getAdapter();
int y = Math.round(dragEvent.getY());
int translatedY = y - adapter.getScrollDistance();
int threshold = 50;
// make a scrolling up due the y has passed the threshold
if (translatedY < threshold) {
// make a scroll up by 30 px
mScrollView.smoothScrollBy(0, -30);
} else
// make a autoscrolling down due y has passed the 500 px border
if (translatedY + threshold > 500) {
// make a scroll down by 30 px
mScrollView.smoothScrollBy(0, 30);
}
break;
要禁用 Second RecyclerView 的项目的 DragListener 的 ACTION_DRAG_LOCATION 事件,请使用以下代码:
@Override
public boolean onDrag(View view, DragEvent dragEvent) {
int action = dragEvent.getAction();
View viewSource = (View) dragEvent.getLocalState();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
break;
case DragEvent.ACTION_DRAG_ENTERED:
break;
case DragEvent.ACTION_DRAG_LOCATION:
return false;
case DragEvent.ACTION_DRAG_EXITED:
break;
case DragEvent.ACTION_DROP:
break;
default:
break;
}
return true;
}
因此,无论您需要处理来自 Draglistener 的什么事件,您只需要 return true 否则 return false。
我制作了一个助手 class 可以更轻松地在 RecyclerViews 中拖放添加滚动。
它是用 C# 为 Xamarin 编写的,但将其更改为 Java 或 Kotlin 应该很简单。
RecyclerViewDragScrollHelper On GitHub
using System;
using System.Threading.Tasks;
using Android.Graphics;
using Android.OS;
using Android.Support.V7.Widget;
using Android.Views;
namespace SharedLibrary.Droid.Helpers {
public class RecyclerViewDragScrollHelper {
DragEvent dragEvent = null; //For passing the drag event to newly bound views
Handler handler = new Handler(); //to pass the run things in the ui thread
ScrollDirection currentScrollDirection = ScrollDirection.None; //the current direction being scrolled
/// <summary>
/// If this is not set then it will try to find a recycler view
/// in the hovered over view's view tree
/// </summary>
public RecyclerView RecyclerViewToScroll { get; set; }
public RecyclerViewDragScrollHelper() {
}
public RecyclerViewDragScrollHelper(RecyclerView recyclerViewToScroll) {
RecyclerViewToScroll = recyclerViewToScroll;
}
public enum UnitType {
Pixels,
Percent
}
enum ScrollDirection {
None,
Foward,
Back
}
/// <summary>
/// Threshold value can be a percentage or pixels
/// </summary>
public UnitType ThresholdUnits { get; set; } = UnitType.Percent;
/// <summary>
/// Threshold of how many pixels or percentage from the edge it has to be before it
/// starts scrolling
/// </summary>
public float Threshold { get; set; } = 0.20f;
/// <summary>
/// Speed of the scroll
/// </summary>
public int ScrollInterval { get; set; } = 5;
/// <summary>
/// Place this method in the method for event handler for View.Drag
/// Or if useing View.SetOnDragListener(), in the implimentation of
/// View.IOnDragListener
/// </summary>
/// <param name="view"></param>
/// <param name="dragEvent"></param>
/// <returns></returns>
public void HandleDrag(View view, DragEvent dragEvent) {
var action = dragEvent.Action;
switch (action) {
case DragAction.Started:
DragStarted(dragEvent);
break;
case DragAction.Ended:
DragEnded();
break;
case DragAction.Location:
var dragpoint = new Point(Convert.ToInt32(dragEvent.GetX()), Convert.ToInt32(dragEvent.GetY()));
HandleLocation(view, dragpoint);
break;
case DragAction.Exited:
currentScrollDirection = ScrollDirection.None;
break;
}
}
/// <summary>
/// Run on DragAction.Location.
/// If it's within the threshold bounds it should scroll the recycler view.
/// Should work for both vertical and horizontal linear layouts.
/// Wont currently work for other layouts
/// </summary>
/// <param name="viewHoveredOver">This is the view that is being dragged over.
/// It can be a view in a recyclerview, or the recycler view it's self</param>
/// <param name="position"></param>
/// <returns></returns>
private void HandleLocation(View viewHoveredOver, Point position) {
var recyclerView = RecyclerViewToScroll;
var viewHoveredOverIsChildOfRecyclerView = false;
//if RecyclerViewToScroll has not been set then
//see if there is a recycler view in the viewHoveredOver view tree
if (recyclerView == null) {
recyclerView = FindRecyclerViewInTree(viewHoveredOver);
viewHoveredOverIsChildOfRecyclerView = true;
} else {
viewHoveredOverIsChildOfRecyclerView = IsViewChildOfView(viewHoveredOver, recyclerView);
}
if (recyclerView != null &&
viewHoveredOverIsChildOfRecyclerView
&& recyclerView.GetLayoutManager() is LinearLayoutManager layoutManager) {
//get the drag position in the view, and work out where it is
//relative to the recycler view. (Both viewHoveredOver and recyclerView
//might be the same view, this doesn't matter)
var pointInRecyclerView = GetPointRelativeTo(viewHoveredOver, recyclerView, position);
//work out the properties for the scrolling based on if it's
//verticle or horizontal
float offSet = 0; //the x or y coord for the drag depending on the orientaion
float upperThreshold = 0; //threshold postion at the end of the recyclerview
int scrollX = 0; //the absolute amount to scroll horizontally
int scrollY = 0; //the absolute amount to scroll vertically
var layoutDirection = layoutManager.Orientation;
//If the threshold units are pixels then just take the threshold value.
//If it's percentage times the threshold by the height to get the pixels
var thresholdPixels = ThresholdUnits == UnitType.Pixels ? Threshold : Threshold * recyclerView.Height;
//If the layout direction is vertical scroll Y coord and check Y offset
//If the layout direction is horizontal scroll X coord and check X offset
if (layoutDirection == LinearLayoutManager.Vertical) {
offSet = pointInRecyclerView.Y;
upperThreshold = recyclerView.Height - thresholdPixels;
scrollY = ScrollInterval;
} else if (layoutDirection == LinearLayoutManager.Horizontal) {
offSet = pointInRecyclerView.X;
upperThreshold = recyclerView.Width - thresholdPixels;
scrollX = ScrollInterval;
}
//if the drag position is less than the lower threshold then scroll backwards
//if it's greater than the uper threshold then scroll forward
//if it's in neither, the set the scroll direction to none
var newScrollDirection = offSet < thresholdPixels
? ScrollDirection.Back
: offSet > upperThreshold
? ScrollDirection.Foward
: ScrollDirection.None;
InScrollPostion(newScrollDirection, recyclerView, scrollX, scrollY);
}
}
Java.Util.Timer timer;
/// <summary>
/// Scroll continiously in the set direction while the position is in the bounds
/// </summary>
/// <param name="newDirection"></param>
/// <param name="recyclerView"></param>
/// <param name="scrollX"></param>
/// <param name="scrollY"></param>
/// <returns></returns>
private void InScrollPostion(
ScrollDirection newDirection,
RecyclerView recyclerView,
int scrollX,
int scrollY) {
//if it's not already scrolling
if (currentScrollDirection != newDirection && newDirection != ScrollDirection.None) {
currentScrollDirection = newDirection;
//1 for forward -1 for backwards
var scrollDirectionInt = newDirection == ScrollDirection.Foward ? 1 : -1;
var canScrollDown = recyclerView.CanScrollVertically(scrollDirectionInt);
var canScrollRight = recyclerView.CanScrollHorizontally(scrollDirectionInt);
scrollX *= scrollDirectionInt;
scrollY *= scrollDirectionInt;
timer?.Cancel(); //cancel the previous timer so they don't stack
timer?.Dispose();
timer = new Java.Util.Timer(); //make a new timer
//Action to be performed every timer tick
Action action = () => {
if (currentScrollDirection == newDirection
&& (recyclerView.CanScrollVertically(scrollDirectionInt)
|| recyclerView.CanScrollHorizontally(scrollDirectionInt))) {
handler.Post(() => recyclerView.ScrollBy(scrollX, scrollY));
} else {
currentScrollDirection = ScrollDirection.None;
timer?.Cancel();
timer?.Dispose();
timer = null;
}
};
//start the timer to do the action every 25ms
timer.ScheduleAtFixedRate(new TimerTask(action), 0, 25);
//the C# async way of doing it
//while (currentScrollDirection == newDirection
// && (recyclerView.CanScrollVertically(scrollDirectionInt)
// || recyclerView.CanScrollHorizontally(scrollDirectionInt))) {
// //Not sure how this would be done in a native android way
// recyclerView.ScrollBy(scrollX, scrollY);
// await Task.Delay(25); //Go back to the ui thread for 25ms before scrolling another increment
//}
//currentScrollDirection = ScrollDirection.None;
} else if (newDirection == ScrollDirection.None) {
currentScrollDirection = ScrollDirection.None;
timer?.Cancel();
timer?.Dispose();
timer = null;
}
}
private class TimerTask : Java.Util.TimerTask {
Action action;
public TimerTask(Action action) {
this.action = action;
}
public override void Run() {
action?.Invoke(); ;
}
}
/// <summary>
/// Run this on DragAction.Ended
/// </summary>
private void DragEnded() {
currentScrollDirection = ScrollDirection.None;
dragEvent = null;
}
/// <summary>
/// Run on DragAction.Started
/// </summary>
/// <param name="dragEvent"></param>
private void DragStarted(DragEvent dragEvent) {
this.dragEvent = dragEvent;
}
/// <summary>
/// This allows a newly bound cell that was previously off screen when
/// the drag started to trigger drag events.
/// Add this to OnBindViewHolder in override of RecyclerView.Adapter
/// </summary>
/// <param name="view"></param>
public void PrepareCellView(View view) {
handler.Post(() => {
if (dragEvent != null && view.Parent is ViewGroup parent) {
parent.DispatchDragEvent(dragEvent);
}
});
}
/// <summary>
/// This allows a newly bound cell that was previously off screen when
/// the drag started to trigger drag events.
/// Add this to OnBindViewHolder in override of RecyclerView.Adapter
/// </summary>
/// <param name="view"></param>
public void PrepareCellView(RecyclerView.ViewHolder holder) {
handler.Post(() => {
if (dragEvent != null && holder.ItemView.Parent is ViewGroup parent) {
parent.DispatchDragEvent(dragEvent);
}
});
}
/// <summary>
/// Find a recycler view in the views view tree
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
private static RecyclerView FindRecyclerViewInTree(View view) {
if (view == null) {
return null;
} else if(view is RecyclerView recyclerView) {
return recyclerView;
} else {
return FindRecyclerViewInTree(view.Parent as View);
}
}
private static bool IsViewChildOfView(View child, View parent) {
if (child == null || parent == null) {
return false;
} else if(child.Parent is View childsParent && childsParent == parent) {
return true;
} else {
return IsViewChildOfView(child.Parent as View, parent);
}
}
/// <summary>
/// Get's a point in a view relative to another view
/// </summary>
/// <param name="fromView"></param>
/// <param name="toView"></param>
/// <param name="fromPoint"></param>
/// <returns></returns>
public static Point GetPointRelativeTo(View fromView, View toView, Point fromPoint) {
var toViewRect = GetRectOnScreen(toView);
var fromViewRect = GetRectOnScreen(fromView);
var pointInToView = new Point(fromViewRect.Left - toViewRect.Left + fromPoint.X,
fromViewRect.Top - toViewRect.Top + fromPoint.Y);
return pointInToView;
}
/// <summary>
/// Gets the views Rect relative to the screen
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
public static Rect GetRectOnScreen(View view) {
int[] l = new int[2];
view.GetLocationOnScreen(l);
int x = l[0];
int y = l[1];
Rect rect = new Rect(x, y, view.Width + x, view.Height + y);
return rect;
}
}
}
将 RecyclerViewDragScrollHelper 实例添加到您的 class,并将 recyclerDragScroller.HandleDrag() 添加到您对 onDrag() 的覆盖。
@Override
public boolean onDrag(View view, DragEvent dragEvent) {
recyclerDragScroller.HandleDrag(view, dragEvent);
return true;
}
如果在拖动开始时未绑定视图持有者,它不会与拖动交互,因此将 recyclerDragScroller.PrepareCellView() 添加到适配器中的 OnBindViewHolder() 覆盖以传递拖动事件。
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
recyclerDragScroller.PrepareCellView(viewHolder);
}