RecyclerView 项目在屏幕上的位置在运行时更改

Posted

技术标签:

【中文标题】RecyclerView 项目在屏幕上的位置在运行时更改【英文标题】:RecyclerView's Items Position On The Screen Changes On The Run 【发布时间】:2021-01-12 14:44:06 【问题描述】:

我有这个RecyclerView,其中任何项目都可以点击打开,当它打开时,它会增长(图1是关闭的项目,图2是打开的项目)

项目的布局文件包含两种状态 - 关闭卡片和打开卡片。要在它们之间切换,我只更改任何状态的可见性。以下是控制项目打开(扩展)或关闭(收缩)的方法:

    /**
    * handles the expanding functionality.
    */
    public void expand() 
        shrink = true;
        if (expandedItem != -1) 
            notifyItemChanged(expandedItem);
        
        expandedItem = getLayoutPosition();

        toggleExpandShrinkIcon(true);
        if (!openedFromParent[1]) 
            openedFromParent[1] = true;
         else 
            openedFromParent[0] = false;
        
        expandedContainer.setVisibility(View.VISIBLE);
        shrunkProgressBar.setVisibility(View.INVISIBLE);
    

    /**
     * handles the shrinking functionality.
     */
    public void shrink() 
        toggleExpandShrinkIcon(false);
        expandedContainer.setVisibility(View.GONE);
        shrunkProgressBar.setVisibility(View.VISIBLE);
        shrink = false;
    

这些方法位于RecyclerView 的适配器中,在ViewHolder 的类内部,它们是公共的,因此我也可以在RecyclerView 的适配器类之外使用它们(不仅通过点击),就像我在一个项目悬停在另一个项目时所做的那样。

最近我添加了拖动悬停功能(使用this library),这样我就可以将任何项目拖到任何其他项目的顶部,当一个项目悬停在另一个项目上时,下方的项目就会打开。 当一个项目被打开时,它会推动其下方的所有其他项目,以便能够在不隐藏其下方的项目的情况下展开(如第一个视频中所示)。 当从悬停一个项目移动到另一个项目时,比如说从第二个项目到第三个项目,当悬停第二个项目时它被打开并且第三个项目被向下推,当移动到第三个项目时第二个项目被关闭,但第三项不会备份。 然后当悬停第三个项目时,它会在第四个项目上打开(请参阅第二个视频以更好地理解)。

这是处理悬停动作的类中的代码:

public class HoveringCallback extends ItemTouchHelper.SimpleCallback 
    //re-used list for selecting a swap target
    private List<RecyclerView.ViewHolder> swapTargets = new ArrayList<>();
    //re used for for sorting swap targets
    private List<Integer> distances = new ArrayList<>();
    private float selectedStartX;
    private float selectedStartY;

    public interface OnDroppedListener 
        void onDroppedOn(ActiveGoalsAdapter.ActiveGoalsViewHolder viewHolder, ActiveGoalsAdapter.ActiveGoalsViewHolder target);
    

    private List<OnDroppedListener> onDroppedListeners = new ArrayList<>();
    @Nullable
    private RecyclerView recyclerView;
    @Nullable
    ActiveGoalsAdapter.ActiveGoalsViewHolder selected;
    @Nullable
    private ActiveGoalsAdapter.ActiveGoalsViewHolder hovered;

    ItemBackgroundCallback backgroundCallback;

    public HoveringCallback() 
        super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
    

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) 
        this.recyclerView = recyclerView;
    

    public void addOnDropListener(OnDroppedListener listener) 
        onDroppedListeners.add(listener);
    

    public void removeOnDropListener(OnDroppedListener listener) 
        onDroppedListeners.remove(listener);
    

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) 
        super.onSelectedChanged(viewHolder, actionState);
        if (viewHolder == null) 
            if (hovered != null) 
                notifyDroppedOnListeners(hovered);
            
         else 
            selectedStartX = viewHolder.itemView.getLeft();
            selectedStartY = viewHolder.itemView.getTop();
        
        this.selected = (ActiveGoalsAdapter.ActiveGoalsViewHolder) viewHolder;
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder != null) 
            viewHolder.itemView.setBackgroundColor(backgroundCallback.getDraggingBackgroundColor(viewHolder));
        
    

    private void notifyDroppedOnListeners(ActiveGoalsAdapter.ActiveGoalsViewHolder holder) 
        for (OnDroppedListener listener : onDroppedListeners) 
            listener.onDroppedOn(selected, (ActiveGoalsAdapter.ActiveGoalsViewHolder) holder);
        
    

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) 
        super.clearView(recyclerView, viewHolder);
        viewHolder.itemView.setBackgroundColor(backgroundCallback.getDefaultBackgroundColor(viewHolder));
    

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) 
        return false;
    

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) 
    

    @Override
    public void onChildDraw(Canvas canvas, RecyclerView parent, RecyclerView.ViewHolder viewHolder,
                            float dX, float dY, int actionState, boolean isCurrentlyActive) 
        super.onChildDraw(canvas, parent, viewHolder, dX, dY, actionState, isCurrentlyActive);

        if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) 
            return;
        

        if (recyclerView == null || selected == null) 
            return;
        

        final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
        final int childCount = lm.getChildCount();
        List<RecyclerView.ViewHolder> swapTargets = findSwapTargets((ActiveGoalsAdapter.ActiveGoalsViewHolder) viewHolder, dX, dY);
        final int x = (int) (selectedStartX + dX);
        final int y = (int) (selectedStartY + dY);

        hovered = (ActiveGoalsAdapter.ActiveGoalsViewHolder) chooseDropTarget((ActiveGoalsAdapter.ActiveGoalsViewHolder) viewHolder, swapTargets, x, y);
        if (hovered == null) 
            this.swapTargets.clear();
            this.distances.clear();
        
        for (int i = 0; i < childCount; i++) 
            final View child = lm.getChildAt(i);

            if (viewHolder.itemView == child) 
                continue;
            

            ActiveGoalsAdapter.ActiveGoalsViewHolder childViewHolder = (ActiveGoalsAdapter.ActiveGoalsViewHolder) parent.findContainingViewHolder(child);

            if (childViewHolder == null || childViewHolder.getAdapterPosition() == RecyclerView.NO_POSITION) 
                continue;
            

            final int count = canvas.save();
            if (childViewHolder == hovered) 
                new Handler().postDelayed(new Runnable() 
                    @Override
                    public void run() 
                        childViewHolder.expand();
                    
                , 500);
             else 
                if(!childViewHolder.isShrunk()) 
                    childViewHolder.shrink();
                    if(canvas.getSaveCount() != count) 
                        canvas.restoreToCount(count);
                    
                
            
        
    

    private List<RecyclerView.ViewHolder> findSwapTargets(ActiveGoalsAdapter.ActiveGoalsViewHolder viewHolder, float dX, float dY) 
        swapTargets.clear();
        distances.clear();
        final int margin = getBoundingBoxMargin();
        final int left = Math.round(selectedStartX + dX) - margin;
        final int top = Math.round(selectedStartY + dY) - margin;
        final int right = left + viewHolder.itemView.getWidth() + 2 * margin;
        final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin;
        final int centerX = (left + right) / 2;
        final int centerY = (top + bottom) / 2;
        final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
        final int childCount = lm.getChildCount();
        for (int i = 0; i < childCount; i++) 
            View other = lm.getChildAt(i);
            if (other == viewHolder.itemView) 
                continue; //myself!
            
            if (other.getBottom() < top || other.getTop() > bottom
                    || other.getRight() < left || other.getLeft() > right) 
                continue;
            
            final ActiveGoalsAdapter.ActiveGoalsViewHolder otherVh = (ActiveGoalsAdapter.ActiveGoalsViewHolder) recyclerView.getChildViewHolder(other);
            if (canDropOver(recyclerView, selected, otherVh)) 
                // find the index to add
                final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2);
                final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2);
                final int dist = dx * dx + dy * dy;

                int pos = 0;
                final int cnt = swapTargets.size();
                for (int j = 0; j < cnt; j++) 
                    if (dist > distances.get(j)) 
                        pos++;
                     else 
                        break;
                    
                
                swapTargets.add(pos, otherVh);
                distances.add(pos, dist);
            
        
        return swapTargets;
    

    @Override
    public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) 
        return 0.05f;
    

    @Override
    public RecyclerView.ViewHolder chooseDropTarget(RecyclerView.ViewHolder selected,
                                                    List<RecyclerView.ViewHolder> dropTargets,
                                                    int curX, int curY) 
        int right = curX + selected.itemView.getWidth();
        int bottom = curY + selected.itemView.getHeight();
        ActiveGoalsAdapter.ActiveGoalsViewHolder winner = null;
        int winnerScore = -1;
        final int dx = curX - selected.itemView.getLeft();
        final int dy = curY - selected.itemView.getTop();
        final int targetsSize = dropTargets.size();
        for (int i = 0; i < targetsSize; i++) 
            final ActiveGoalsAdapter.ActiveGoalsViewHolder target = (ActiveGoalsAdapter.ActiveGoalsViewHolder) dropTargets.get(i);
            if (dx > 0) 
                int diff = target.itemView.getRight() - right;
                if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) 
                    final int score = Math.abs(diff);
                    if (score > winnerScore) 
                        winnerScore = score;
                        winner = target;
                    
                
            
            if (dx < 0) 
                int diff = target.itemView.getLeft() - curX;
                if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) 
                    final int score = Math.abs(diff);
                    if (score > winnerScore) 
                        winnerScore = score;
                        winner = target;
                    
                
            
            if (dy < 0) 
                int diff = target.itemView.getTop() - curY;
                if (target.itemView.getTop() < selected.itemView.getTop()) 
                    final int score = Math.abs(diff);
                    if (score > winnerScore) 
                        winnerScore = score;
                        winner = target;
                    
                
            

            if (dy > 0) 
                int diff = target.itemView.getBottom() - bottom;
                if (target.itemView.getBottom() > selected.itemView.getBottom()) 
                    final int score = Math.abs(diff);
                    if (score > winnerScore) 
                        winnerScore = score;
                        winner = target;
                    
                
            
        
        return winner;
    

我该如何解决这个问题? (让第三个项目回上,就像释放拖动时一样,在第二个视频的末尾) 帮助将不胜感激! (:

第一张图片(已关闭项目):

第二张图片(打开的物品):

第一个视频(项目在列表中打开和关闭):

第二个视频(拖动的项目):

【问题讨论】:

好吧,我想问题是当你触发 notifyItemChanged(expandedItem);对于第 3 个项目,您没有通知上一个(第 2 个)项目也必须更改大小,这就是第 3 个项目认为第 2 个项目像扩展项目一样发生的原因。尝试使用 notifyItemRangeChanged();或使用其他方法来验证问题是否在这里?同样“openedFromParent”这部分很奇怪......但我们没有关于这个数组的任何信息...... @EugeneTroyanskii 嗨。首先感谢您的回复,尝试了 notifyItenRangeChanged();,但它只是让情况变得更糟......当第二个项目展开时,第三个甚至没有移动......关于“openedFromParent” - 它与这种情况,我敢肯定,它处理其他事情。你还有别的想法吗?无论如何谢谢(: 【参考方案1】:

我认为最好的办法是设置使用 ACTION_UP 打开每个回收站视图的代码,以便在抬起手指时触发代码,而不是在感应到它时触发。

button.setOnTouchListener(new View.OnTouchListener() @Override public boolean onTouch(View view, MotionEvent motionEvent) sw If(MotionEvent.ACTION_UP) Toast.makeText(MainActivity.this, "Up", Toast.LENGTH_SHORT).show();

【讨论】:

我不确定它是否相关...我不是在将另一个项目拖动到它们上方时单击这些项目以打开它们,它们只是自行打开...它与任何触摸事件无关关于开幕项目...:/无论如何谢谢(: 如果与触摸事件无关,它们如何“自行打开”。也许发布一些代码? 我已经发布了一些代码来更好地解释它。你能帮我提供这段代码吗?【参考方案2】:

在没有研究库代码的情况下,我假设位置正在更改,因此很可能调用 .notifyItemChanged(position),并在其中展开函数调用因为没有检查将 expand 函数 与未处理的事件隔离

一个简单的答案可以是保存状态并禁用项目的扩展

长按项目时将 isDragActive 设置为 true

private static isDragActive = false
  @Overrides
   public onCreateViewHolder()
     itemView.setOnItemLongClickListener(new OnItemLongClickListener() 
        @Override
        public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
                int pos, long id) 
            isDragActive = true;
            return true;
        
    ); 
 

    public void expand() 
    if(!isDragActive)
        shrink = true;
        if (expandedItem != -1) 
            notifyItemChanged(expandedItem);
        
        expandedItem = getLayoutPosition();

        toggleExpandShrinkIcon(true);
        if (!openedFromParent[1]) 
            openedFromParent[1] = true;
         else 
            openedFromParent[0] = false;
        
        expandedContainer.setVisibility(View.VISIBLE);
        shrunkProgressBar.setVisibility(View.INVISIBLE);
    
 

您也可以使用下面给出的库来代替上面的解决方案

https://github.com/mikepenz/FastAdapter

【讨论】:

嗨 Ali,感谢您的关注问题是 - 我希望拖动的项目下方的项目被消耗,但是当拖动的项目离开下方的项目时,我希望关闭下方的项目,并且如果拖动的项目现在悬停在另一个项目上,它应该展开...我希望我的解释很清楚,我没有其他词可以描述它:/ 知道了,我研究一下代码告诉你:) 好吧,如果你有这个位置,那么就从活动中用 position = draggedItemPosition + 1 展开项目 也许您希望我上传 HoveringCallback 类?它可能会有所帮助,但它是一个完整的课程,这意味着它有一些代码要阅读,我不想吓到试图帮助的人? 对不起,你能解释一下吗?我无法理解您的建议:/

以上是关于RecyclerView 项目在屏幕上的位置在运行时更改的主要内容,如果未能解决你的问题,请参考以下文章

更改 recyclerView 滚动位置参考

Android 通过OnScrollListener来监听RecyclerView的位置

单击后退按钮时,在 RecyclerView 中获得相同的项目位置

滚动在RecyclerView中插入的项目

SnapHelper 项目位置

PopupWindow的简单使用(结合RecyclerView)