在Android的RecyclerView

时间:2015-11-14 16:27:52

标签: android animation android-recyclerview

RecyclerView不同,ListView没有简单的方法来设置空视图,因此必须手动管理它,以便在适配器项目的情况下显示空视图count是0。

实现这一点,起初我尝试在修改底层结构(在我的情况下为ArrayList)后立即调用空视图逻辑,例如:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
        updateEmptyView();
    }
});

它可以做到这一点,但有一个缺点:当删除最后一个元素时,在移除动画之后立即显示空视图。所以我决定等到动画结束然后更新UI。

令我惊讶的是,我找不到在RecyclerView中监听动画事件的好方法。首先想到的是使用isRunning这样的方法:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
        recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
            @Override
            public void onAnimationsFinished() {
                updateEmptyView();
            }
        });
    }
});

不幸的是,在这种情况下回调立即运行,因为此时内部ItemAnimator仍未处于“运行”状态。所以,问题是:如何正确使用ItemAnimator.isRunning()方法并且有更好的方法来实现所需的结果,即在删除单个元素的动画后显示空视图完成

11 个答案:

答案 0 :(得分:18)

目前,我发现解决此问题的唯一方法是扩展ItemAnimator并将其传递给RecyclerView,如下所示:

recyclerView.setItemAnimator(new DefaultItemAnimator() {
    @Override
    public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
        updateEmptyView();
    }
});

但是这种技术并不普遍,因为我必须从ItemAnimator使用的具体RecyclerView实现扩展。如果CoolItemAnimator内部有私人内部CoolRecyclerView,我的方法根本无效。

PS:我的同事建议以下列方式将ItemAnimator包裹在decorator内:

recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));

虽然对于这样一个琐碎的任务来说似乎有点过分,但这样会很好,但是无论如何都不可能在这种情况下创建装饰器,因为ItemAnimator有一个方法setListener(),它受到包保护所以我显然无法包装它,以及几种最终方法。

答案 1 :(得分:9)

我有一些更通用的案例,我希望在同时删除或添加一个或多个项目时检测回收者视图何时完全动画完成。

我尝试过Roman Petrenko的答案,但在这种情况下它不起作用。问题是为recycleler视图中的每个条目调用onAnimationFinished。大多数条目都没有改变,因此onAnimationFinished被或多或少地瞬间调用。但是对于添加和删除动画需要一点时间,因此稍后会调用它。

这导致至少两个问题。假设您有一个名为doStuff()的方法,您希望在动画完成时运行该方法。

  1. 如果你只是在doStuff()中拨打onAnimationFinished,那么你会为回收站视图中的每个项目调用一次,这可能不是你想要做的。

  2. 如果您只是在第一次调用doStuff()时调用onAnimationFinished,您可能会在上一个动画完成之前很久就调用它。

  3. 如果你知道有多少项动画,你可以确保在上一个动画结束时调用doStuff()。但我还没有找到任何方法知道有多少剩余的动画排队等候。

    我对此问题的解决方案是让回收器视图首先使用new Handler().post()开始设置动画,然后设置一个在动画准备就绪时调用的isRunning()侦听器。之后,它会重复该过程,直到所有视图都被动画化。

    void changeAdapterData() {
        // ...
        // Changes are made to the data held by the adapter
        recyclerView.getAdapter().notifyDataSetChanged();
    
        // The recycler view have not started animating yet, so post a message to the
        // message queue that will be run after the recycler view have started animating.
        new Handler().post(waitForAnimationsToFinishRunnable);
    }
    
    private Runnable waitForAnimationsToFinishRunnable = new Runnable() {
        @Override
        public void run() {
            waitForAnimationsToFinish();
        }
    };
    
    // When the data in the recycler view is changed all views are animated. If the
    // recycler view is animating, this method sets up a listener that is called when the
    // current animation finishes. The listener will call this method again once the
    // animation is done.
    private void waitForAnimationsToFinish() {
        if (recyclerView.isAnimating()) {
            // The recycler view is still animating, try again when the animation has finished.
            recyclerView.getItemAnimator().isRunning(animationFinishedListener);
            return;
        }
    
        // The recycler view have animated all it's views
        onRecyclerViewAnimationsFinished();
    }
    
    // Listener that is called whenever the recycler view have finished animating one view.
    private RecyclerView.ItemAnimator.ItemAnimatorFinishedListener animationFinishedListener =
            new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
        @Override
        public void onAnimationsFinished() {
            // The current animation have finished and there is currently no animation running,
            // but there might still be more items that will be animated after this method returns.
            // Post a message to the message queue for checking if there are any more
            // animations running.
            new Handler().post(waitForAnimationsToFinishRunnable);
        }
    };
    
    // The recycler view is done animating, it's now time to doStuff().
    private void onRecyclerViewAnimationsFinished() {
        doStuff();
    }
    

答案 2 :(得分:2)

对我有用的是:

  • 检测到视图持有者已被删除
  • 在这种情况下,注册一个监听器,以便在调用dispatchAnimationsFinished()时收到通知
  • 完成所有动画后,调用侦听器执行任务(updateEmptyView()
public class CompareItemAnimator extends DefaultItemAnimator implements RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {

private OnItemAnimatorListener mOnItemAnimatorListener;

public interface OnItemAnimatorListener {
    void onAnimationsFinishedOnItemRemoved();
}

@Override
public void onAnimationsFinished() {
    if (mOnItemAnimatorListener != null) {
        mOnItemAnimatorListener.onAnimationsFinishedOnItemRemoved();
    }
}

public void setOnItemAnimatorListener(OnItemAnimatorListener onItemAnimatorListener) {
    mOnItemAnimatorListener = onItemAnimatorListener;
}

@Override
public void onRemoveFinished(RecyclerView.ViewHolder viewHolder) {
    isRunning(this);
}}

答案 3 :(得分:2)

这是一个基于nibarius的answer的Kotlin扩展方法。

fun RecyclerView.executeAfterAllAnimationsAreFinished(
    callback: (RecyclerView) -> Unit
) = post(
    object : Runnable {
        override fun run() {
            if (isAnimating) {
                // itemAnimator is guaranteed to be non-null after isAnimating() returned true
                itemAnimator!!.isRunning {
                    post(this)
                }
            } else {
                callback(this@executeAfterAllAnimationsAreFinished)
            }
        }
    }
)

答案 4 :(得分:1)

扩展Roman Petrenko的答案,如果您将Kotlin与androidx回收器视图一起使用,则可以执行以下操作:

        taskListRecycler.apply {
            itemAnimator = object : DefaultItemAnimator() {
                override fun onAddFinished(item: RecyclerView.ViewHolder?) {
                    super.onAddFinished(item)
                    //Extend
                }

                override fun onRemoveFinished(item: RecyclerView.ViewHolder?) {
                    super.onRemoveFinished(item)
                    //Extend
                }
            }
            layoutManager = LinearLayoutManager(context)
            adapter = taskListAdapter
        }

答案 5 :(得分:1)

在ItemAnimator类中有一个方法,当所有项目动画完成时会调用该方法:

    /**
     * Method which returns whether there are any item animations currently running.
     * This method can be used to determine whether to delay other actions until
     * animations end.
     *
     * @return true if there are any item animations currently running, false otherwise.
     */
     public abstract boolean isRunning();

您可以覆盖它以检测所有项目动画何时结束:

recyclerView.itemAnimator = object : DefaultItemAnimator() {
    override fun isRunning(): Boolean {
        val isAnimationRunning = super.isRunning()
        if(!isAnimationRunning) {
            // YOUR CODE
        }
        return isAnimationRunning
    }
}

答案 6 :(得分:1)

从最新的 androidx.recyclerview:recyclerview:1.2.0 中检查 ItemAnimator 方法:

boolean isRunning(@Nullable ItemAnimatorFinishedListener listener)

示例(科特林):

recyclerView.itemAnimator?.isRunning {
    // do whatever you need to
}

答案 7 :(得分:0)

为了扩展Roman Petrenko的答案,我也没有真正的通用答案,但我确实发现工厂模式是一种有用的方法,至少可以清理一些残余物。这个问题。

public class ItemAnimatorFactory {

    public interface OnAnimationEndedCallback{
        void onAnimationEnded();
    }
    public static RecyclerView.ItemAnimator getAnimationCallbackItemAnimator(OnAnimationEndedCallback callback){
        return new FadeInAnimator() {
            @Override
            public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
                callback.onAnimationEnded();
                super.onAnimationEnded(viewHolder);
            }
        };
    }
}

在我的情况下,我使用的库提供了我已经使用的FadeInAnimator。我在工厂方法中使用Roman的解决方案来挂钩onAnimationEnded事件,然后将事件传递回链。

然后,当我配置我的recyclerview时,我将回调指定为我根据recyclerview项目计数更新视图的方法:

mRecyclerView.setItemAnimator(ItemAnimatorFactory.getAnimationCallbackItemAnimator(this::checkSize));

同样,它并非在所有任何和所有ItemAnimators中都是完全通用的,但它至少可以巩固这一点,所以如果你有多个不同的项目动画师,你可以实现一个工厂这里的方法遵循相同的模式,然后您的recyclerview配置只是指定您想要的ItemAnimator。

答案 8 :(得分:0)

在我的情况下,我想在动画结束后删除一堆项目(并添加新项目)。但是isAnimating事件是为每个持有者触发的,因此@SqueezyMo的函数不会在所有项目上同时执行我的操作。因此,我在Animator中实现了一个侦听器,并提供了一种方法来检查最后一个动画是否完成。

动画师

class ClashAnimator(private val listener: Listener) : DefaultItemAnimator() {

    internal var winAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()
    internal var exitAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()

    private var lastAddAnimatedItem = -2

    override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
        return true
    }

    interface Listener {
        fun dispatchRemoveAnimationEnded()
    }

    private fun dispatchChangeFinishedIfAllAnimationsEnded(holder: ClashAdapter.ViewHolder) {
        if (winAnimationsMap.containsKey(holder) || exitAnimationsMap.containsKey(holder)) {
            return
        }
        listener.dispatchRemoveAnimationEnded() //here I dispatch the Event to my Fragment

        dispatchAnimationFinished(holder)
    }

    ...
}

片段

class HomeFragment : androidx.fragment.app.Fragment(), Injectable, ClashAdapter.Listener, ClashAnimator.Listener {
    ...
    override fun dispatchRemoveAnimationEnded() {
        mAdapter.removeClash() //will execute animateRemove
        mAdapter.addPhotos(photos.subList(0,2), picDimens[1]) //will execute animateAdd
    }
}

答案 9 :(得分:0)

请注意,如果没有动画,则不会调用该操作

fun RecyclerView.onDefaultAnimationFinished(action: () -> Unit, scope: CoroutineScope) {
var startedWaiting = false

fun waitForAllAnimations() {
    if (!isAnimating) {
        action()
        return
    }
    scope.launch(Dispatchers.IO) {
        delay(25)
    }

    scope.launch(Dispatchers.Main) {
        waitForAllAnimations()
    }
}

itemAnimator = object : DefaultItemAnimator() {
    override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
        super.onAnimationFinished(viewHolder)
        if (!startedWaiting)
            waitForAllAnimations()

        startedWaiting = true
    }
}

}

答案 10 :(得分:-1)

在这种情况下,对于如此琐碎的事情,API的设计如此差劲,我只是巧妙地蛮力地将其强加了。

您总是可以运行一个后台任务或线程,该任务会定期轮询动画器是否正在运行,如果动画器未运行,则执行代码。

如果您是RxJava的粉丝,则可以使用我制作的此扩展功能:

/**
 * Executes the code provided by [onNext] once as soon as the provided [predicate] is true.
 * All this is done on a background thread and notified on the main thread just like
 * [androidObservable].
 */
inline fun <reified T> T.doInBackgroundOnceWhen(
        crossinline predicate: (T) -> Boolean,
        period: Number = 100,
        timeUnit: java.util.concurrent.TimeUnit =
                java.util.concurrent.TimeUnit.MILLISECONDS,
        crossinline onNext: T.() -> Unit): Disposable {
    var done = false
    return Observable.interval(period.toLong(), timeUnit, Schedulers.computation())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.computation())
            .takeWhile { !done }
            .subscribe {
                if (predicate(this)) {
                    onNext(this)
                    done = true
                }
            }
}

您可以这样做:

recyclerView.doInBackgroundOnceWhen(
    predicate = { adapter.isEmpty && !recyclerView.itemAnimator.isRunning },
    period = 17, timeUnit = TimeUnit.MILLISECONDS) {
    updateEmptyView()
}

它的作用是每隔17毫秒检查一次谓词是否满足,如果满足,则执行onNext块。 (60帧每秒17毫秒)

这在计算上昂贵且效率低下 ...但是可以完成工作。

我目前首选的处理方式是利用Android的本机Choreographer,它使您可以在下一帧执行回调,只要有可能。

使用Android Choreographer:

/**
 * Uses [Choreographer] to evaluate the [predicate] every frame, if true will execute [onNextFrame]
 * once and discard the callback.
 * 
 * This runs on the main thread!
 */
inline fun doOnceChoreographed(crossinline predicate: (frameTimeNanos: Long) -> Boolean,
                               crossinline onNextFrame: (frameTimeNanos: Long) -> Unit) {
    var callback: (Long) -> Unit = {}
    callback = {
        if (predicate(it)) {
            onNextFrame(it)
            Choreographer.getInstance().removeFrameCallback(callback)
            callback = {}
        } else Choreographer.getInstance().postFrameCallback(callback)
    }
    Choreographer.getInstance().postFrameCallback(callback)
}

一个警告,与RxJava实现不同,它在主线程上执行。

然后您可以像这样轻松地调用它:

doOnceChoreographed(predicate = { adapter.isEmpty && !recyclerView.itemAnimator.isRunning }) {
    updateEmptyView()
}