自定义RecyclerView的LayoutManager。删除项动画完成后自动测量。

7
我创建了一个适用于RecyclerView自定义布局管理器。我在我的管理器构造函数中通过setAutoMeasureEnabled(true)启用了自动测量功能。
RecyclerViewlayout_height属性设置为wrap_content时,这个LayoutManager可以根据其中的项测量出RecyclerView的高度。它运行得很完美,但当我删除底部的最后一项时,删除动画正在播放,它导致RecyclerView在动画完成之前就已经测量出了其高度。
请看这个gif图。enter image description here 绿色背景是一个RecyclerView。
如你所料,对于添加项,这种行为是正确的,因为在这种情况下需要在动画之前测量容器的大小。
我该如何处理这种情况,使得RecyclerView在动画结束后自动测量呢?我在onLayoutChildren中有所有子项位置逻辑,但我认为这不需要在此问题上进行解释,可能会很宽泛和不清楚。或许我应该手动处理LayoutManageronMeasure拦截器(当删除项时调用4次(在onItemsRemoved调用之前调用一次)),这样我就可以根据在删除开始时接收到的高度设置测量高度了。
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
    super.onMeasure(recycler, state, widthSpec, heightSpec);
    requestSimpleAnimationsInNextLayout();
    if (!isAutoMeasureEnabled()) {
        setMeasuredDimension(getWidth(), preHeight);
    }
}

@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
    super.onItemsRemoved(recyclerView, positionStart, itemCount);
    setAutoMeasureEnabled(false);

    new Handler(Looper.myLooper()).postDelayed(new Runnable() {
        @Override
        public void run() {
            setAutoMeasureEnabled(true);
            requestSimpleAnimationsInNextLayout();
            requestLayout();
        }
    }, 400);

    preHeight = //calculate height before deletion
}

如您所见,我提供了一个处理程序,其中包含约延迟400毫秒的值,该处理程序在动画完成后执行自动测量,因此这可能导致需要的结果,但动画的持续时间不是固定的,而且我没有看到任何可能监听动画完成事件的可能性。

所以,期望的行为是:

enter image description here

顺便说一下,LinearLayoutManager的自动测量方式也是相同的。

如果您可以通过文本描述算法指引我走向正确的方向,那就足够了。


1
我无法改进我的问题质量,因为你没有任何评论就将我的问题投票降低了。 - Beloo
在我看来,这个问题被完美地提出并得到了完美的回答... - Maarten
3个回答

6

我终于想明白了。所以,我的解决方案:

onAdapterChanged 方法中订阅 dataChanges 事件并禁用自动测量。

 @Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
                             RecyclerView.Adapter newAdapter) {
    newAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            // on api 16 this is invoked before onItemsRemoved
            super.onItemRangeRemoved(positionStart, itemCount);
            /** we detected removing event, so should process measuring manually
             */
            setAutoMeasureEnabled(false);
        }
    });
     //Completely scrap the existing layout
    removeAllViews();
}

onMeasure中,如果禁用了自动测量,请手动测量,使用当前的尺寸:
 @Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
    super.onMeasure(recycler, state, widthSpec, heightSpec);

    if (!isAutoMeasureEnabled()) {
        // we should perform measuring manually
        // so request animations
        requestSimpleAnimationsInNextLayout();
        //keep size until remove animation will be completed
        setMeasuredDimension(getWidth(), getHeight());
    }
}

动画完成后执行自动测量:

 @Override
public void onItemsRemoved(final RecyclerView recyclerView, int positionStart, int itemCount) {
    super.onItemsRemoved(recyclerView, positionStart, itemCount);
    //subscribe to next animations tick
    postOnAnimation(new Runnable() {
        @Override
        public void run() {
            //listen removing animation
            recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
                @Override
                public void onAnimationsFinished() {
                    //when removing animation finished return auto-measuring back
                    setAutoMeasureEnabled(true);
                    // and process onMeasure again
                    requestLayout();
                }
            });
        }
    });

这个回调链是安全的,因为只有在布局管理器没有从RecyclerView分离时,postOnAnimation runnable才会被执行。

1
请添加方法isAutoMeasureEnabled()的重写,因为在LinearLayoutManager中它总是返回true(是的,在那里硬编码了),所以如果没有这个,你的代码将无法正常工作。 - Warcello
1
每当适配器通知已删除一个或多个项目时,都会调用onItemRangeRemoved,但有些情况下,删除的项目数= 0(例如,可扩展的回收视图-当组折叠且不包含任何项目时)。因此,为了覆盖这种情况,您还必须在onItemRangeRemoved中添加检查,并仅在itemCount> 0时调用setAutoMeasureEnabled(false) - David Rauca

3

@Beloo的回答对我来说没有用,我怀疑是因为setAutoMeasureEnabled已被弃用。我只重写了onItemsRemovedonMeasureisAutoMeasureEnabled才使它起作用。

首先创建一个变量来跟踪isAutoMeasureEnabled属性:

private boolean autoMeasureEnabled = true;

然后重写isAutoMeasureEnabled,使用该变量:

@Override
public boolean isAutoMeasureEnabled (){
    return autoMeasureEnabled;
}

现在您可以随意设置变量autoMeasureEnabled。覆盖onItemsRemoved函数:
@Override
public void onItemsRemoved(@NonNull final RecyclerView recyclerView, int positionStart, int itemCount) {
    super.onItemsRemoved(recyclerView, positionStart, itemCount);
    autoMeasureEnabled = false;
    postOnAnimation(new Runnable() {
        @Override
        public void run() {
            recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
                @Override
                public void onAnimationsFinished() {
                    autoMeasureEnabled = true;
                    requestLayout();
                }
            });
        }
    });
}

同样也需要像@Beloo的回答一样重写onMeasure方法:

    @Override
public void onMeasure(@NotNull RecyclerView.Recycler recycler, @NotNull RecyclerView.State state, int widthSpec, int heightSpec) {
    super.onMeasure(recycler, state, widthSpec, heightSpec);

    if (!isAutoMeasureEnabled()) {
        // we should perform measuring manually
        // so request animations
        requestSimpleAnimationsInNextLayout();
        //keep size until remove animation will be completed
        setMeasuredDimension(getWidth(), getHeight());
    }
}

这似乎是有效的。动画很快,但时间控制得当。

1
这对我有效,但是我确实需要像@Beloo一样覆盖函数onAdapterChanged - Jeroen Flietstra

0

正确的方法是在RecyclerView本身上使用转换,以便它也可以动画化自己的边界变化。

来自:https://android-developers.googleblog.com/2016/02/android-support-library-232.html

请注意,尽管RecyclerView会动画化其子项,但它不会动画化自己的边界变化。如果您想要在RecyclerView边界发生变化时动画化它们,可以使用Transition API。

因此,如here所述,TransitionManager.beginDelayedTransition(recyclerView)应该可以正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接