使用DefaultItemAnimator时,StaggeredGridLayoutManager的移动动画被破坏

Che*_*eng 11 android

我有一个非常直接的实现使用StaggeredGridLayoutManager.

我想基于实现移动动画 DefaultItemAnimator

用第4项切换第3项

我尝试在第4项中切换第3项,如下面的屏幕记录所示.它工作得很好.动画只涉及第3和第4项.

在此输入图像描述

用第2项切换第1项

但是,当我尝试用第二项切换第一项时,行为会以某种方式被破坏.

我希望动画只发生在第1项和第2项之间.但是,似乎整个列表都是动画的,即使每个项目具有相同的宽度和高度.(因此,不应执行间隙填充操作)

每次切换后,您需要滚动RecyclerView,以便切换项目可见.

在此输入图像描述


我尝试了什么

  1. 使用

    staggeredGridLayoutManager.setGapStrategy( StaggeredGridLayoutManager.GAP_HANDLING_NONE); 
    
    Run Code Online (Sandbox Code Playgroud)

    没有用.

为了知道场景背后发生了什么,我尝试使用以下内容DefaultItemAnimator进行记录.

public class DebugDefaultItemAnimator extends DefaultItemAnimator {
    @Override
    public boolean animateRemove(RecyclerView.ViewHolder holder) {
        Log.i("CHEOK", "animateRemove");
        return super.animateRemove(holder);
    }

    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        Log.i("CHEOK", "animateAdd");
        return super.animateAdd(holder);

    }

    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        Log.i("CHEOK", "animateMove (" + fromX + "," + fromY + ") to (" + toX + "," + toY + ")");
        return super.animateMove(holder, fromX, fromY, toX, toY);
    }

    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
        Log.i("CHEOK", "animateChange");
        return super.animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我们在第3和第4之间切换时,一切正常,这是日志.

animateMove (24,250) to (564,250)
animateMove (564,250) to (24,250)
Run Code Online (Sandbox Code Playgroud)

但是当我们在第一项和第二项之间切换时,事情就会被打破,这就是日志.

animateMove (24,928) to (564,702)
animateMove (564,476) to (24,476)
animateMove (24,1154) to (564,928)
animateMove (24,476) to (564,250)
animateMove (564,1154) to (24,1154)
animateMove (564,250) to (24,250)
animateMove (564,702) to (24,702)
animateMove (564,928) to (24,928)
animateRemove
animateMove (24,702) to (564,476)
animateMove (24,1380) to (564,1154)
animateMove (24,250) to (564,24)
animateAdd
animateMove (564,1380) to (24,1380)
Run Code Online (Sandbox Code Playgroud)

我的实施非常直接.

知道什么可能出错吗?我尝试StaggeredGridLayoutManagerLinearLayoutManager和替换GridLayoutManager.一切都很好,除了StaggeredGridLayoutManager.

源代码如下.(完整的源代码可以从https://github.com/yccheok/StaggeredGridLayoutManagerProblem下载)


Adapter.java

public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {

    private List<Data> datas;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public final TextView titleTextView;
        public final TextView bodyTextView;
        public ViewHolder(View view) {
            super(view);

            titleTextView = view.findViewById(R.id.title_text_view);
            bodyTextView = view.findViewById(R.id.body_text_view);
        }
    }

    public Adapter(List<Data> datas) {
        this.datas = datas;

        setHasStableIds(true);
    }

    @Override
    public long getItemId(int position) {
        return datas.get(position).id;
    }

    public Adapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        // create a new view
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item, parent, false);

        ViewHolder vh = new ViewHolder(view);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.titleTextView.setText(datas.get(position).title);
        holder.bodyTextView.setText(datas.get(position).body);

    }

    @Override
    public int getItemCount() {
        return datas.size();
    }
}
Run Code Online (Sandbox Code Playgroud)

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;

    private List<Data> datas = new ArrayList<>();
    private Adapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recycler_view);

        StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);

        //staggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);

        this.recyclerView.setLayoutManager(staggeredGridLayoutManager);

        datas.add(new Data(0, "A title", "A body"));
        datas.add(new Data(1, "B title", "B body"));
        datas.add(new Data(2, "C title", "C body"));
        datas.add(new Data(3, "D title", "D body"));
        datas.add(new Data(4, "E title", "E body"));
        datas.add(new Data(5, "F title", "F body"));
        datas.add(new Data(6, "G title", "G body"));
        datas.add(new Data(7, "H title", "H body"));
        datas.add(new Data(8, "I title", "I body"));
        datas.add(new Data(9, "J title", "J body"));
        datas.add(new Data(10, "K title", "K body"));
        datas.add(new Data(11, "L title", "L body"));
        datas.add(new Data(12, "M title", "M body"));
        datas.add(new Data(13, "N title", "N body"));
        datas.add(new Data(14, "O title", "O body"));
        adapter = new Adapter(datas);

        recyclerView.setAdapter(adapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
            case R.id.debug:

                Data data0 = datas.get(0);
                Data data1 = datas.get(1);

                datas.set(0, data1);
                datas.set(1, data0);

                adapter.notifyItemMoved(0, 1);

                return true;

            case R.id.debug2:

                Data data2 = datas.get(2);
                Data data3 = datas.get(3);

                datas.set(2, data3);
                datas.set(3, data2);

                adapter.notifyItemMoved(2, 3);

                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Data.java

public class Data {
    public final int id;
    public final String title;
    public final String body;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Data data = (Data) o;

        if (id != data.id) return false;
        if (title != null ? !title.equals(data.title) : data.title != null) return false;
        return body != null ? body.equals(data.body) : data.body == null;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (title != null ? title.hashCode() : 0);
        result = 31 * result + (body != null ? body.hashCode() : 0);
        return result;
    }

    public Data(int id, String title, String body) {

        this.id = id;
        this.title = title;
        this.body = body;
    }
}
Run Code Online (Sandbox Code Playgroud)

重要的提示

请不要建议使用notifyItemRangeChangednotifyItemChanged.因为,这是一个"移动"操作,而不是"改变"操作.

如果您使用DiffUtil上述情况,notifyItemMoved肯定会被解雇.

我已经更新了GitHub中的代码,以证明DiffUtil会解雇notifyItemMoved.因此,DiffUtil就动画问题而言也会失败 - https://github.com/yccheok/StaggeredGridLayoutManagerProblem/commit/cfa2bc9659f11e52dcee97ce8e78dcfcb6ad5e8c

我更感兴趣的是知道为什么notifyItemMoved不适用于上述情况,以及如何使其工作.


结论

我在https://issuetracker.google.com/issues/78373192上提交了错误报告.如果你想看到要解决的话,请为它加注星标.

Che*_*amp 7

这看起来像是一个bug StaggeredGridLayoutManager.我已经使用了您的应用程序并对其进行了一些修改,以用于以下演示.考虑以下因素:

在此输入图像描述

正如您所看到的,最初一切都按预期工作:项目移动,动画就像屏幕未满时一样.但是,当我们添加项目"M"时,一切都会像你所呈现的那样变得混乱.我的结论是,这是一个错误.它看起来像是与差距管理相关的边界问题,但这只是我的猜测.

我进一步确认这是一个布局问题,并且可以证明虽然孩子的顺序RecyclerView是正确的,但是StaggeredGridLayout 不能以相同的顺序布置孩子.滚动时,您强制布局管理器再次查看布局,然后尽管适配器没有更改,但它仍然正确.

我将补充一点,这不是项目动画师的问题.如果将动画设置为off(recyclerView.setItemAnimator(null);),则问题仍然存在.

一种解决方法是自定义ListUpdateCallback以捕获问题情况并执行不同的操作.在下面的代码中,查找位置0的移动onMoved()方法MyListUpdateCallbacknotifyItemChanged从位置调用的方法; 否则,处理正常进行.

这是应用了修复程序的应用程序:

在此输入图像描述

虽然此解决方案解决了您提供的MCVE问题,但它可能无法完全按照书面形式解决真实应用中的问题.如果没有,我相信这种方法可以适应工作.

MainActivity.java

这是演示的更新代码.将布尔值设置mDoTheFix为true以应用修复.如果mDoTheFix为false,则应用程序将显示损坏的行为.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private RecyclerView recyclerView;

    private List<Data> datas = new ArrayList<>();
    private Adapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.recycler_view);

        StaggeredGridLayoutManager staggeredGridLayoutManager =
            new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) {
                @Override
                public void onLayoutCompleted(RecyclerView.State state) {
//                    super.onLayoutCompleted(state);
                }
            };
        //        recyclerView.setItemAnimator(null);
//        staggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
        this.recyclerView.setLayoutManager(staggeredGridLayoutManager);
        datas.add(new Data(0, "A title", "A body"));
        datas.add(new Data(1, "B title", "B body"));
        datas.add(new Data(2, "C title", "C body"));
        datas.add(new Data(3, "D title", "D body"));
        datas.add(new Data(4, "E title", "E body"));
        datas.add(new Data(5, "F title", "F body"));
        datas.add(new Data(6, "G title", "G body"));
        datas.add(new Data(7, "H title", "H body"));
        datas.add(new Data(8, "I title", "I body"));
        datas.add(new Data(9, "J title", "J body"));
        datas.add(new Data(10, "K title", "K body"));
        datas.add(new Data(11, "L title", "L body"));
        datas.add(new Data(12, "M title", "M body"));
//        datas.add(new Data(13, "N title", "N body"));
//        datas.add(new Data(14, "O title", "O body"));
//        datas.add(new Data(11, "P title", "P body"));
//        datas.add(new Data(12, "Q title", "Q body"));
//        datas.add(new Data(13, "R title", "R body"));
//        datas.add(new Data(14, "S title", "S body"));
        adapter = new Adapter(datas, this);

        recyclerView.setAdapter(adapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
            case R.id.debug: {
                String s = (String) ((TextView) (((LinearLayout) ((FrameLayout) recyclerView.getChildAt(0)).getChildAt(0)).getChildAt(0)))
                    .getText();
                Data data0 = datas.get(0);
                Data data1 = datas.get(1);

                datas.set(0, data1);
                datas.set(1, data0);

                new MyListUpdateCallback().onMoved(1, 0);

                return true;
            }

            case R.id.debug2:

                Data data2 = datas.get(2);
                Data data3 = datas.get(3);

                datas.set(2, data3);
                datas.set(3, data2);

                adapter.notifyItemMoved(2, 3);

                return true;

            case R.id.debug3: {
                List<Data> oldDatas = new ArrayList<>(datas);

                Data data0 = datas.get(0);
                Data data1 = datas.get(1);

                datas.set(0, data1);
                datas.set(1, data0);

                MyNoteDiffUtilCallback noteDiffUtilCallback = new MyNoteDiffUtilCallback(datas, oldDatas);
                DiffUtil.calculateDiff(noteDiffUtilCallback).dispatchUpdatesTo(new MyListUpdateCallback());

                return true;
            }

            case R.id.debug4:
                String ABC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
                int i = datas.size();
                if (i < 26) {
                    String ch = ABC.substring(i, i + 1);
                    datas.add(new Data(12, ch + " title", ch + " body"));
                    adapter.notifyItemInserted(i - 1);
                }
                return true;

            case R.id.debug5:
                int toRemove = datas.size() - 1;
                datas.remove(toRemove);
                adapter.notifyItemRemoved(toRemove);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onClick(View v) {
        int oldPos = recyclerView.getLayoutManager().getPosition(v);
        int newPos = oldPos - 1;
        if (newPos < 0) {
            return;
        }
        Data data0 = datas.get(oldPos);
        Data data1 = datas.get(newPos);

        datas.set(oldPos, data1);
        datas.set(newPos, data0);
        new MyListUpdateCallback().onMoved(oldPos, newPos);
    }

    public class MyNoteDiffUtilCallback extends DiffUtil.Callback {
        private List<Data> newsDatas;
        private List<Data> oldDatas;


        public MyNoteDiffUtilCallback(List<Data> newsDatas, List<Data> oldDatas) {
            this.newsDatas = newsDatas;
            this.oldDatas = oldDatas;
        }

        @Override
        public int getOldListSize() {
            return oldDatas.size();
        }

        @Override
        public int getNewListSize() {
            return newsDatas.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return oldDatas.get(oldItemPosition).id == newsDatas.get(newItemPosition).id;
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return oldDatas.get(oldItemPosition).equals(newsDatas.get(newItemPosition));
        }
    }

    boolean mDoTheFix = true;

    private class MyListUpdateCallback implements ListUpdateCallback {

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            if (mDoTheFix && toPosition == 0) {
                adapter.notifyItemChanged(fromPosition);
                adapter.notifyItemChanged(toPosition);
            } else {
                adapter.notifyItemMoved(fromPosition, toPosition);
            }
        }

        public void onInserted(int position, int count) {
            adapter.notifyItemRangeInserted(position, count);
        }

        @Override
        public void onRemoved(int position, int count) {
            adapter.notifyItemRangeRemoved(position, count);
        }

        @Override
        public void onChanged(int position, int count, Object payload) {
            adapter.notifyItemRangeChanged(position, count, payload);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

menu.xml文件

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myApp="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/debug"
        android:title="0->1"
        myApp:showAsAction="always" />

    <item android:id="@+id/debug2"
        android:title="2->3"
        myApp:showAsAction="always" />

    <item android:id="@+id/debug3"
        android:title="DiffUtil"
        myApp:showAsAction="always" />

    <item android:id="@+id/debug4"
        android:title="Add"
        myApp:showAsAction="always" />

    <item android:id="@+id/debug5"
        android:title="Del"
        myApp:showAsAction="always" />
</menu>
Run Code Online (Sandbox Code Playgroud)


Sag*_*gar 6

StaggeredGridLayoutManager通过扫描grep代码做了一些分析.当itemMoved处于时,看起来存在计算问题index 0.您可以参考这个方法,当notifyItemMoved()RecyclerView的适配器隐式调用该方法时将调用该方法.

它适用于其他布局,因为其他布局只是调用invalidateSpanCache()以在下一个布局中执行正确的重新计算notifyItemMoved(),因为它是一个结构更改事件.

所以我找到的解决方案/解决方法如下,其中只关注处理位置移动index 0:

选项1:调用notifyDataSetChanged

对于您的情况,为了强制StaggeredGridLayoutManager对移动的项目做出正确的反应index 0,您需要强制执行StaggeredGridLayoutManager完全重新绑定并重新布局所有可见视图.这可以通过notifyDataSetChanged()如下调用来实现:

case R.id.debug: {

     Data data0 = datas.get(0);
     Data data1 = datas.get(1);

     datas.set(0, data1);
     datas.set(1, data0);
     adapter.notifyDataSetChanged();
     return true;
 }
Run Code Online (Sandbox Code Playgroud)

这会触发您预期的动画.但缺点是当您处理大量数据时,此操作可能会很昂贵

选项2:直接在布局上调用onItemMoved

此方法以某种方式执行正确的计算,并且视图不会搞砸.您需要维护布局对象的引用:

case R.id.debug:{

    Data data0=datas.get(0);
    Data data1=datas.get(1);

    datas.set(0,data1);
    datas.set(1,data0);
    staggeredGridLayoutManager.requestSimpleAnimationsInNextLayout();
    staggeredGridLayoutManager.onItemsMoved(recyclerView,0,1,1);


    return true;
}
Run Code Online (Sandbox Code Playgroud)

这里有缺点,它适用于不同的动画(淡入/淡出).这也不会调用任何数据更改观察者,因为StaggeredGridLayoutManager不会传播调用RecyclerView.您可能需要创建动画才能产生相同的效果.

选项3:通知notifyItemChanged

由于明显的正当理由,这是您不喜欢的选项:

case R.id.debug: {
    Collections.swap(datas, 0, 1);
    adapter.notifyItemChanged(0);
    adapter.notifyItemChanged(1);
    return true;
}
Run Code Online (Sandbox Code Playgroud)

总而言之,基于代码的性质,不明确的文档和使用StaggeredGridLayoutManager开发的性质看起来处于早期阶段,并且将在不久的将来成熟.只有修复了布局问题,才能实现一个好的解决方案.到那时我们必须使用一些具有不同权衡的解决方法.