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

huangapple go评论145阅读模式
英文:

RecyclerView's Items Position On The Screen Changes On The Run

问题

Sure, here's the translated content:

<br>我有一个`RecyclerView`,其中任何项目都可以点击打开,在打开时会展开(图1是关闭的项目,图2是打开的项目)

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

        /**
        * 处理展开功能。
        */
        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);
        }

        /**
         * 处理收缩功能。
         */
        public void shrink() {
            toggleExpandShrinkIcon(false);
            expandedContainer.setVisibility(View.GONE);
            shrunkProgressBar.setVisibility(View.VISIBLE);
            shrink = false;
        }

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

<br>最近,我添加了拖动悬停功能(使用[此库][1]),以便我可以将任何项目拖到任何其他项目上,当一个项目悬停在另一个项目上时,较低的项目会被打开。
<br>当项目被打开时,它会将其下方的所有其他项目都推开,以便能够展开而不隐藏其下方的项目(就像第一个视频中的效果)。
<br>当从悬停在一个项目上切换到悬停在另一个项目上时,例如从第二个项目切换到第三个项目时,在悬停在第二个项目上时,它会被打开,第三个项目会被推下,而当切换到第三个项目时,第二个项目会被关闭,但第三个项目不会回弹。
然后在悬停在第三个项目上时,它会打开第四个项目(请参阅第二个视频以更好地理解)。

<be>以下是处理悬停动作的类中的代码:

    public class HoveringCallback extends ItemTouchHelper.SimpleCallback {
        // 用于选择交换目标的重用列表
        private List<RecyclerView.ViewHolder> swapTargets = new ArrayList<>();
        // 用于对交换目标进行排序的重用列表
        private List<Integer> distances = new ArrayList<>();
        private float selectedStartX;
        private float selectedStartY;
    
        public interface OnDroppedListener {
            void onDroppedOn(ActiveGoalsAdapter.ActiveGoalsViewHolder viewHolder, ActiveGoalsAdapter.ActiveGoalsViewHolder target);
        }
    
        // ...(接下来的代码被省略,以保持简洁)
    }

<br>我该如何解决这个问题?(使第三个项目返回到上方,就像在第二个视频结束时拖动被释放的情况一样)
<br>非常感谢您的帮助!(:

<br>第一张图片(关闭的项目):
<br>[![closed item][2]][2]

<br>第二张图片(打开的项目):
<br>[![opened item][3]][3]

<br>第一个视频(列表中的项目被打开并关闭):
<br>[![item gets opened & closed in the list][4]][4]

<br>第二个视频(拖动项目):
<br>[![item dragged with the bug][5]][5]

  [1]: https://github.com/shuhart/HoveringCallback
  [2]: https://i.stack.imgur.com/owyvZ.png
  [3]: https://i.stack.imgur.com/mQcAO.png
  [4]: https://i.stack.imgur.com/WIC0e.gif
  [5]: https://i.stack.imgur.com/6brOO.gif

Please note that the translation provided here might not perfectly match the original content's formatting due to limitations in the text representation. If you need the translation for specific parts to be adjusted for clarity, please let me know.

英文:

<br>I have this RecyclerView where any item can be clicked to open, and when it's opened, it grows (picture 1 is a closed item, picture 2 is an opened item)

<br>The layout file of an item holds two states - the closed card, and the opened card. To switch between them, I change only the visibility of any state. Here are the methods that control an item's opening (expanding) or closing (shrinking) :

    /**
    * 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;
    }

These methods are located in the RecyclerView's adapter, inside of the ViewHolder's class, and they are public so I could use them also out of the RecyclerView's adapter class (not only by clicking), as I did when one item hovers another.

<br> Recently I added drag-to-hover functionality (using this library) so that I can drag any item on top of any other item, and when one item hovers another item, the lower item gets opened.
<br>When an item gets opened, it pushes all the other items below it to be able to expand without hiding the items under it (like in the first video).
<br>When moving from hovering one item to another, say from the second item to the third, when hovering the second item it gets opened and the third item is pushed down, and when moving to the third item the second item gets closed, but the third item won't go back up.
Then when hovering the third item, it gets opened on the fourth item (see the second video to understand better).

<be>Here's the code in the class that handles the hovering action:

public class HoveringCallback extends ItemTouchHelper.SimpleCallback {
//re-used list for selecting a swap target
private List&lt;RecyclerView.ViewHolder&gt; swapTargets = new ArrayList&lt;&gt;();
//re used for for sorting swap targets
private List&lt;Integer&gt; distances = new ArrayList&lt;&gt;();
private float selectedStartX;
private float selectedStartY;
public interface OnDroppedListener {
void onDroppedOn(ActiveGoalsAdapter.ActiveGoalsViewHolder viewHolder, ActiveGoalsAdapter.ActiveGoalsViewHolder target);
}
private List&lt;OnDroppedListener&gt; onDroppedListeners = new ArrayList&lt;&gt;();
@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 &amp;&amp; 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&lt;RecyclerView.ViewHolder&gt; 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 &lt; 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&lt;RecyclerView.ViewHolder&gt; 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 &lt; childCount; i++) {
View other = lm.getChildAt(i);
if (other == viewHolder.itemView) {
continue; //myself!
}
if (other.getBottom() &lt; top || other.getTop() &gt; bottom
|| other.getRight() &lt; left || other.getLeft() &gt; 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 &lt; cnt; j++) {
if (dist &gt; 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&lt;RecyclerView.ViewHolder&gt; 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 &lt; targetsSize; i++) {
final ActiveGoalsAdapter.ActiveGoalsViewHolder target = (ActiveGoalsAdapter.ActiveGoalsViewHolder) dropTargets.get(i);
if (dx &gt; 0) {
int diff = target.itemView.getRight() - right;
if (diff &lt; 0 &amp;&amp; target.itemView.getRight() &gt; selected.itemView.getRight()) {
final int score = Math.abs(diff);
if (score &gt; winnerScore) {
winnerScore = score;
winner = target;
}
}
}
if (dx &lt; 0) {
int diff = target.itemView.getLeft() - curX;
if (diff &gt; 0 &amp;&amp; target.itemView.getLeft() &lt; selected.itemView.getLeft()) {
final int score = Math.abs(diff);
if (score &gt; winnerScore) {
winnerScore = score;
winner = target;
}
}
}
if (dy &lt; 0) {
int diff = target.itemView.getTop() - curY;
if (target.itemView.getTop() &lt; selected.itemView.getTop()) {
final int score = Math.abs(diff);
if (score &gt; winnerScore) {
winnerScore = score;
winner = target;
}
}
}
if (dy &gt; 0) {
int diff = target.itemView.getBottom() - bottom;
if (target.itemView.getBottom() &gt; selected.itemView.getBottom()) {
final int score = Math.abs(diff);
if (score &gt; winnerScore) {
winnerScore = score;
winner = target;
}
}
}
}
return winner;
}
}

<br>How can I solve this? (make the third item go back up, like when the drag is released, at the end of the second video)
<br>Help would be highly appreciated! (:

<br>First picture (closed item):
<br>RecyclerView的项目在运行时在屏幕上的位置更改

<br>Second picture (opened item):
<br>RecyclerView的项目在运行时在屏幕上的位置更改

<br>First Video (item gets opened & closed in the list):
<br>RecyclerView的项目在运行时在屏幕上的位置更改

<br>Second video (item dragged):
<br>RecyclerView的项目在运行时在屏幕上的位置更改

答案1

得分: 1

没有研究Library的代码,我假设位置正在被改变,因此很有可能会调用 .notifyItemChanged(position),在此之中,展开函数被调用,因为没有检查来将展开函数与未处理的事件隔离开。

一个简单的解决方法是保存状态并禁用项目的展开

当长按项目时,将isDragActive设置为true

private static isDragActive = false
@Overrides
public onCreateViewHolder(){
itemView.setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView&lt;?&gt; 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

英文:

Without studying the code of Library I assume positions are being changed so it is highly likely that .notifyItemChanged(position) is invoked, and in that, expand function invoke because there is no check to isolate the expand function from unhandled events

A simple answer can be to save the state and disable the expansion of items

Set isDragActive to true when where the item is long pressed

private static isDragActive = false
@Overrides
public onCreateViewHolder(){
itemView.setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView&lt;?&gt; 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);
}
}

You can also use the library given below instead of above solution

https://github.com/mikepenz/FastAdapter

答案2

得分: 0

我认为最好的方法是将打开每个RecyclerView的代码设置为使用ACTION_UP,以便在手指抬起时触发代码,而不是在感应到手指触摸时触发。

button.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            Toast.makeText(MainActivity.this, "Up", Toast.LENGTH_SHORT).show();
        }
        return false;
    }
});
英文:

I think the best bet is to set your code that opens each recyclerview using ACTION_UP so that the code is triggered on the lifting of the finger, not on the sensing of it.

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(); }

huangapple
  • 本文由 发表于 2020年9月26日 21:12:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/64078086.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定