【问题标题】:Detect animation finish in Android's RecyclerView在 Android 的 RecyclerView 中检测动画完成
【发布时间】:2015-11-14 16:27:52
【问题描述】:

RecyclerViewListView 不同,它没有一种简单的方法来为其设置空视图,因此必须手动管理它,在适配器的项目计数为 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() 方法,是否有更好的方法来达到预期的效果,即 在移除单个元素的动画后显示空视图完成了

【问题讨论】:

    标签: android animation android-recyclerview


    【解决方案1】:

    我有一个更通用的情况,当一个或多个项目同时被删除或添加时,我想检测回收器视图何时完全完成动画。

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

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

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

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

    如果您知道要制作动画的项目数量,您可以确保在最后一个动画结束时调用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();
    }
    

    【讨论】:

    • 它工作得很好,而且我试过notifyItemRemoved(position)的方法,它也工作得很好。
    • 非常优雅的解决方案。谢谢。
    • 非常感谢@nibarius
    • @nibarius 它说 - 新的 Handler() 实现方法
    • Handler 的默认构造函数最近已被弃用,我认为 new Handler(Looper.getMainLooper()) 可能会更好。
    【解决方案2】:

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

    boolean isRunning(@Nullable ItemAnimatorFinishedListener listener)
    

    示例(Kotlin):

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

    【讨论】:

      【解决方案3】:

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

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

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


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

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

      这会很好,尽管对于这样一个微不足道的任务似乎有点矫枉过正,但在这种情况下创建装饰器无论如何都是不可能的,因为ItemAnimator 有一个方法setListener() 是包保护的,所以我显然不能包装它,还有几个final方法。

      【讨论】:

      • 嘿罗曼。你有没有遇到过更集中的解决方案来解决这个问题?
      • @Ryan:不幸的是,没有。但是自从我回答以来,我没有研究过这个问题。
      【解决方案4】:

      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
          }
      }
      

      【讨论】:

      • 有趣的事情。这个方法对我来说被调用了 3 次。 2 进入活动时,退出时一次。虽然,否则它似乎确实可靠。我将添加一个标志,让它只运行一次代码。
      【解决方案5】:

      扩展 Roman Petrenko 的回答,如果您使用 androidx recycler view 和 kotlin,您可以这样做:

              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
              }
      

      【讨论】:

        【解决方案6】:

        对我有用的是:

        • 检测到视图支架已被移除
        • 在这种情况下,注册一个监听器,以便在调用 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);
        }}
        

        【讨论】:

          【解决方案7】:

          在这样的场景中,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 块。 (60fps 为 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()
          }
          

          【讨论】:

            【解决方案8】:

            这是一个基于 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)
                        }
                    }
                }
            )
            

            【讨论】:

            • 没用。回调在动画之前立即调用
            【解决方案9】:

            为了扩展 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));
            

            同样,它并不是在所有 ItemAnimator 中都完全通用,但它至少“巩固了基础”,所以如果你有多个不同的项目动画师,你可以在这里按照相同的模式实现一个工厂方法,然后您的 recyclerview 配置只是指定您想要的 ItemAnimator。

            【讨论】:

              【解决方案10】:

              在我的情况下,我想在动画结束后删除一堆项目(并添加新项目)。但是每个持有者都会触发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
                  }
              }
              

              【讨论】:

                【解决方案11】:

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

                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
                    }
                }
                

                }

                【讨论】:

                  猜你喜欢
                  • 2023-03-04
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-12-08
                  • 2015-04-05
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-05-25
                  相关资源
                  最近更新 更多