【问题标题】:Auto-scroll Scrollbar While Drag item In Recyclerview在 Recyclerview 中拖动项目时自动滚动滚动条
【发布时间】:2017-02-27 12:46:29
【问题描述】:

我在滚动视图中遇到自动滚动问题。

就我而言,有两个 Recyclerview。第一个 Recyclerview,水平滚动,第二个垂直滚动。第一个 RecyclerView 仅用于拖动,第二个 RecyclerView 仅用于放置。两个 recyclerview 都在 ScrollView 内,所以我在第二个 Recyclerview 中禁用了垂直滚动。 我在 Second Recyclerview 的项目上添加了 DragListener。每个项目都有拖动侦听器,因此基于我在放置项目时添加/替换项目。

所以我的主要问题是滚动,所以当我拖动项目时滚动无法正常工作。 目前我在拖动时使用下面的代码进行滚动。

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 有多个项目时,滚动视图会上下滚动,而 itemX 在两个项目之间。

已编辑的问题: 现在,我将上面的代码放在 Second Recycler 视图的 OnDragListener 上。现在是拖动监听器的问题,所以我想如果用户拖动第一个 Recyclerview 的项目低于/高于 第二个 Recyclerview 的拖动监听器需要工作,否则 第二个 Recyclerview 的项目的拖动监听器需要工作.

【问题讨论】:

    标签: android android-recyclerview scrollview


    【解决方案1】:

    我通过在 Second RecyclerView 的项目的拖动侦听器的 ACTION_DRAG_LOCATION 事件中返回 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 的什么事件,你只需要返回 true,否则返回 false。

    【讨论】:

    • 什么是 adapter.getScrollDistance() ?
    【解决方案2】:

    我制作了一个帮助类,以便在 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 的实例添加到您的类中,并将 recyclerDragScroller.HandleDrag() 添加到您的 onDrag() 覆盖中。

    @Override
    public boolean onDrag(View view, DragEvent dragEvent) {
        recyclerDragScroller.HandleDrag(view, dragEvent);
        return true;
    }
    

    如果在拖动开始时未绑定 viewholder,则它不会与拖动交互,因此将 recyclerDragScroller.PrepareCellView() 添加到适配器中的 OnBindViewHolder() 覆盖以传递拖动事件。

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        recyclerDragScroller.PrepareCellView(viewHolder);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-01-15
      • 1970-01-01
      • 2020-09-12
      • 1970-01-01
      • 2016-01-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多