【问题标题】:RecyclerView Q&A回收站查看问答
【发布时间】:2016-02-15 02:17:27
【问题描述】:

我正在创建一个问答,其中每个问题都是一张卡片。答案开始显示第一行,但当点击它时,它应该展开以显示完整的答案。

当答案展开/折叠时,RecyclerView 的其余部分应进行动画处理,以便为展开或折叠腾出空间以避免显示空白。

我在RecyclerView animations 上观看了演讲,并相信我想要一个自定义的 ItemAnimator,我在其中覆盖 animateChange。那时我应该创建一个 ObjectAnimator 来为 View 的 LayoutParams 的高度设置动画。不幸的是,我很难将它们联系在一起。我在覆盖 canReuseUpdatedViewHolder 时也返回了 true,所以我们重用了同一个 viewholder。

@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
    return true;
}


@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
                             @NonNull final RecyclerView.ViewHolder newHolder,
                             @NonNull ItemHolderInfo preInfo,
                             @NonNull ItemHolderInfo postInfo) {
    Log.d("test", "Run custom animation.");

    final ColorsAdapter.ColorViewHolder holder = (ColorsAdapter.ColorViewHolder) newHolder;

    FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) holder.tvColor.getLayoutParams();
    ObjectAnimator halfSize = ObjectAnimator.ofInt(holder.tvColor.getLayoutParams(), "height", params.height, 0);
    halfSize.start();
    return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
}

现在我只是想制作一些动画,但没有任何反应......有什么想法吗?

【问题讨论】:

  • 我更新了我的答案,因为我发现它不是你想要的。
  • @George Mulligan 工作得很好
  • @eimmer 你解决问题了吗。
  • @GeorgeMulligan 的回答是我见过的最好的回答。我正在开发一个 github 项目,以提供您可以在此处找到的工作代码的完整示例。 github.com/rvail2/RecyclerViewHeightAnimations
  • 你看到我的回答了吗@eimmer,试试这个也是很好的答案,顺便说一句,georgeMulligan 也很好

标签: android animation android-recyclerview


【解决方案1】:

我认为您的动画无法正常工作,因为您无法以这种方式为LayoutParams 制作动画,尽管如果可以的话,它会很整洁。我尝试了你的代码,它所做的只是让我的观点跳到了新的高度。我发现让它工作的唯一方法是使用ValueAnimator,如下例所示。

在使用DefaultItemAnimator 通过更新其可见性来显示/隐藏视图时,我注意到了一些缺点。尽管它确实为新视图腾出了空间并根据可展开视图的可见性上下动画其余项目,但我注意到它没有为可展开视图的高度设置动画。它只是使用 alpha 值简单地淡入淡出。

下面是一个自定义的ItemAnimator,它具有基于在ViewHolder 布局中隐藏/显示LinearLayout 的大小和alpha 动画。它还允许重复使用相同的ViewHolder,并在用户快速点击标题时尝试正确处理部分动画:

public static class MyAnimator extends DefaultItemAnimator {
    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
        return true;
    }

    private HashMap<RecyclerView.ViewHolder, AnimatorState> animatorMap = new HashMap<>();

    @Override
    public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull final RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        final ValueAnimator heightAnim;
        final ObjectAnimator alphaAnim;

        final CustomAdapter.ViewHolder vh = (CustomAdapter.ViewHolder) newHolder;
        final View expandableView = vh.getExpandableView();
        final int toHeight; // save height for later in case reversing animation

        if(vh.isExpanded()) {
            expandableView.setVisibility(View.VISIBLE);

            // measure expandable view to get correct height
            expandableView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
            toHeight = expandableView.getMeasuredHeight();
            alphaAnim = ObjectAnimator.ofFloat(expandableView, "alpha", 1f);
        } else {
            toHeight = 0;
            alphaAnim = ObjectAnimator.ofFloat(expandableView, "alpha", 0f);
        }

        heightAnim = ValueAnimator.ofInt(expandableView.getHeight(), toHeight);
        heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                expandableView.getLayoutParams().height = (Integer) heightAnim.getAnimatedValue();
                expandableView.requestLayout();
            }
        });

        AnimatorSet animSet = new AnimatorSet()
                .setDuration(getChangeDuration());
        animSet.playTogether(heightAnim, alphaAnim);
        animSet.addListener(new Animator.AnimatorListener() {
            private boolean isCanceled;

            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationEnd(Animator animation) {
                if(!vh.isExpanded() && !isCanceled) {
                    expandableView.setVisibility(View.GONE);
                }

                dispatchChangeFinished(vh, false);
                animatorMap.remove(newHolder);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isCanceled = true;
            }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        });

        AnimatorState animatorState = animatorMap.get(newHolder);
        if(animatorState != null) {
            animatorState.animSet.cancel();

            // animation already running. Set start current play time of
            // new animations to keep them smooth for reverse animation
            alphaAnim.setCurrentPlayTime(animatorState.alphaAnim.getCurrentPlayTime());
            heightAnim.setCurrentPlayTime(animatorState.heightAnim.getCurrentPlayTime());

            animatorMap.remove(newHolder);
        }

        animatorMap.put(newHolder, new AnimatorState(alphaAnim, heightAnim, animSet));

        dispatchChangeStarting(newHolder, false);
        animSet.start();

        return false;
    }

    public static class AnimatorState {
        final ValueAnimator alphaAnim, heightAnim;
        final AnimatorSet animSet;

        public AnimatorState(ValueAnimator alphaAnim, ValueAnimator heightAnim, AnimatorSet animSet) {
            this.alphaAnim = alphaAnim;
            this.heightAnim = heightAnim;
            this.animSet = animSet;
        }
    }
}

这是使用稍作修改的RecyclerView 演示的结果。

更新:

刚刚注意到你的用例在重新阅读问题后实际上有点不同。您有一个文本视图,只想显示其中的一行,然后将其展开以显示所有行。幸运的是,这简化了自定义动画师:

public static class MyAnimator extends DefaultItemAnimator {
    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
        return true;
    }

    private HashMap<RecyclerView.ViewHolder, ValueAnimator> animatorMap = new HashMap<>();

    @Override
    public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull final RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        ValueAnimator prevAnim = animatorMap.get(newHolder);
        if(prevAnim != null) {
            prevAnim.reverse();
            return false;
        }

        final ValueAnimator heightAnim;
        final CustomAdapter.ViewHolder vh = (CustomAdapter.ViewHolder) newHolder;
        final TextView tv = vh.getExpandableTextView();

        if(vh.isExpanded()) {
            tv.measure(View.MeasureSpec.makeMeasureSpec(((View) tv.getParent()).getWidth(), View.MeasureSpec.AT_MOST), View.MeasureSpec.UNSPECIFIED);
            heightAnim = ValueAnimator.ofInt(tv.getHeight(), tv.getMeasuredHeight());
        } else {
            Paint.FontMetrics fm = tv.getPaint().getFontMetrics();
            heightAnim = ValueAnimator.ofInt(tv.getHeight(), (int)(Math.abs(fm.top) + Math.abs(fm.bottom)));
        }

        heightAnim.setDuration(getChangeDuration());
        heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                tv.getLayoutParams().height = (Integer) heightAnim.getAnimatedValue();
                tv.requestLayout();
            }
        });

        heightAnim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                dispatchChangeFinished(vh, false);
                animatorMap.remove(newHolder);
            }

            @Override
            public void onAnimationCancel(Animator animation) { }

            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        });

        animatorMap.put(newHolder, heightAnim);

        dispatchChangeStarting(newHolder, false);
        heightAnim.start();

        return false;
    }
}

还有新的演示:

【讨论】:

  • 我认为这是让动画正确的最佳答案。注意:当 ViewHolders 被回收时,跟踪 viewholder 中的动画可能会出现问题。
  • @eimmer 您的注释很好,但我认为ViewHolder 不会在动画进行时被回收。 Yigit 在他的blog 中提到,可以通过LayoutManager 删除视图,但保留在RecyclerView 中,以便动画正常运行。我希望我能找到视频的源代码,这样我们就可以看到他们正在做什么来反转动画。
  • @eimmer 非常有趣的一点,因为我正在努力实现这一点。但是在查看 Chet Haas 的 2015 Android 峰会视频后,我可以看到您没有使用 recordPreLayoutInformationrecordPostLayoutInformation 在绑定 VH 之前和绑定 VH 之后调用 notifyItemChanged 时捕获值。在我的情况下,这会导致以下项目快速向下平移,仅向上平移,然后才能很好地跟随高度动画。你的方法会更好吗?我真的很努力地坚持 Chet Haas 的解释,因为他是 Recyclerview 的幕后推手。谢谢!
【解决方案2】:

您不必实现自定义ItemAnimator,默认DefaultItemAnimator 已经支持您需要的功能。但是,您需要告诉这个 Animator 哪些视图发生了变化。我猜你在适配器中调用notifyDataSetChanged()。这可以防止 RecyclerView 中单个更改项目的动画(在您的情况下是项目的展开/折叠)。

对于已更改的项目,您应该使用notifyItemChanged(int position)。这是一个简短的itemClicked(int position) 方法,用于展开/折叠 RecyclerView 中的视图。 expandedPosition 字段跟踪当前展开的项目:

private void itemClicked(int position) {
    if (expandedPosition == -1) {
        // selected first item
        expandedPosition = position;
        notifyItemChanged(position);
    } else if (expandedPosition == position) {
        // collapse currently expanded item
        expandedPosition = -1;
        notifyItemChanged(position);
    } else {
        // collapse previously expanded item and expand new item
        int oldExpanded = expandedPosition;
        expandedPosition = position;
        notifyItemChanged(oldExpanded);
        notifyItemChanged(position);
    }
}

这是结果:

【讨论】:

  • 那么你实际上是如何折叠视图的呢?你只是将它的高度设置为0吗?将可见性设置为 GONE?应用自定义动画?
  • @eimmer 每次调用notifyItemChanged 时,都会为相关位置调用适配器中的方法onBindViewHolder。提供元素的扩展版本来获取动画就足够了。 gif 中的项目视图只是 LinearLayout 中的两个 TextViews。折叠/展开版本的区别在于第二个TextView中的文字较长
  • 这是一种简单而简洁的方法,但它确实有一些缺点。在这种情况下,ViewHolders 通过淡入淡出进行交换,并且更改项目的实际大小变化不是动画的。在此示例中并不明显,因为您只是在扩展文本,但如果您将文本完全替换为不同的内容,则文本将与新旧视图持有者重叠。此外,由于高度不是动画的,因此如果用户快速展开和折叠会很尴尬。也许我在测试时做错了什么,但我写了一个自定义动画师来尝试克服下面的这些问题。
【解决方案3】:

根据文档,您需要在 animateChange 中返回 false 或稍后调用 runPendingAnimations。尝试返回 false。

http://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemAnimator.html

【讨论】:

    【解决方案4】:

    试试这个课程:

    import android.animation.Animator;
    import android.animation.ValueAnimator;
    import android.graphics.Paint;
    import android.support.v7.widget.RecyclerView;
    import android.view.View;
    import android.view.animation.AccelerateDecelerateInterpolator;
    import android.widget.TextView;
    
    /**
     * Created by ankitagrawal on 2/14/16.
     */
    
    public class AnimatedViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
    
        private int originalHeight = 0;
        private boolean mIsViewExpanded = false;
        private TextView textView;
    
        // ..... CODE ..... //
        public AnimatedViewHolder(View v) {
            super(v);
            v.setOnClickListener(this);
    
            // Initialize other views, like TextView, ImageView, etc. here
    
            // If isViewExpanded == false then set the visibility
            // of whatever will be in the expanded to GONE
    
            if (!mIsViewExpanded) {
                // Set Views to View.GONE and .setEnabled(false)
                textView.setLines(1);
            }
    
        }
        @Override
        public void onClick(final View view) {
    
            // Declare a ValueAnimator object
            ValueAnimator valueAnimator;
            if(mIsViewExpanded) {
                view.measure(View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.AT_MOST), View.MeasureSpec.UNSPECIFIED);
                mIsViewExpanded = false;
                valueAnimator = ValueAnimator.ofInt(view.getHeight(), view.getMeasuredHeight());
            } else {
                Paint.FontMetrics fm = ((TextView)view).getPaint().getFontMetrics();
                valueAnimator = ValueAnimator.ofInt(view.getHeight(), (int) (Math.abs(fm.top) + Math.abs(fm.bottom)));
                mIsViewExpanded = true;
            }
            valueAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationEnd(Animator animation) {
                }
    
                @Override
                public void onAnimationCancel(Animator animation) { }
    
                @Override
                public void onAnimationStart(Animator animation) { }
    
                @Override
                public void onAnimationRepeat(Animator animation) { }
            });
    
            valueAnimator.setDuration(200);
            valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                public void onAnimationUpdate(ValueAnimator animation) {
                    view.getLayoutParams().height = (Integer) animation.getAnimatedValue();
                    view.requestLayout();
                }
            });
    
    
            valueAnimator.start();
    
        }
    }
    

    这种方法的优点是它只为 onClick 事件添加动画,最适合您的要求。

    向视图中添加动画对您的要求来说太繁琐了。 文档中的 itemAnimator 和 itemAnimator 是用于布局项目的动画,因此也不是最适合您的要求。

    【讨论】:

      【解决方案5】:

      对于展开和折叠动画 android 有 github 库。 ExpandableRecyclerView

      1).在build.gradle文件中添加依赖

      dependencies {
          compile 'com.android.support:recyclerview-v7:22.2.0'
          compile 'com.bignerdranch.android:expandablerecyclerview:1.0.3'
      }
      

      Image of Expand & Collapse Animation

      2) RecyclerView 动画的展开和折叠动画

      public static class ExampleViewHolder extends RecyclerView.ViewHolder 
          implements View.OnClickListener {
      
          private int originalHeight = 0;
          private boolean isViewExpanded = false;
          private YourCustomView yourCustomView
      
          public ExampleViewHolder(View v) {
           super(v);
           v.setOnClickListener(this);
      
           // Initialize other views, like TextView, ImageView, etc. here
      
           // If isViewExpanded == false then set the visibility 
           // of whatever will be in the expanded to GONE
      
           if (isViewExpanded == false) {
               // Set Views to View.GONE and .setEnabled(false)
               yourCustomView.setVisibility(View.GONE);
               yourCustomView.setEnabled(false);
           }
      
          }
      
          @Override
          public void onClick(final View view) {
              // If the originalHeight is 0 then find the height of the View being used 
              // This would be the height of the cardview
              if (originalHeight == 0) {
                      originalHeight = view.getHeight();
                  }
      
              // Declare a ValueAnimator object
              ValueAnimator valueAnimator;
                  if (!mIsViewExpanded) {
                      yourCustomView.setVisibility(View.VISIBLE);
                      yourCustomView.setEnabled(true);
                      mIsViewExpanded = true;
                      valueAnimator = ValueAnimator.ofInt(originalHeight, originalHeight + (int) (originalHeight * 2.0)); // These values in this method can be changed to expand however much you like
                  } else {
                      mIsViewExpanded = false;
                      valueAnimator = ValueAnimator.ofInt(originalHeight + (int) (originalHeight * 2.0), originalHeight);
      
                      Animation a = new AlphaAnimation(1.00f, 0.00f); // Fade out
      
                      a.setDuration(200);
                      // Set a listener to the animation and configure onAnimationEnd
                      a.setAnimationListener(new Animation.AnimationListener() {
                          @Override
                          public void onAnimationStart(Animation animation) {
      
                          }
      
                          @Override
                          public void onAnimationEnd(Animation animation) {
                              yourCustomView.setVisibility(View.INVISIBLE);
                              yourCustomView.setEnabled(false);
                          }
      
                          @Override
                          public void onAnimationRepeat(Animation animation) {
      
                          }
                      });
      
                      // Set the animation on the custom view
                      yourCustomView.startAnimation(a);
                  }
                  valueAnimator.setDuration(200);
                  valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
                  valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                      public void onAnimationUpdate(ValueAnimator animation) {
                          Integer value = (Integer) animation.getAnimatedValue();
                          view.getLayoutParams().height = value.intValue();
                          view.requestLayout();
                      }
                  });
                  valueAnimator.start();
          }
      
      }
      

      希望这会对你有所帮助。

      【讨论】:

      • 当 android 本机执行此操作时,您不需要外部库。
      • 从 NerdRanch 提供的示例中,这使用了两个单独的视图持有者。如果没有其他方法,这些似乎绝对是最后的手段。
      猜你喜欢
      • 1970-01-01
      • 2020-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-27
      • 1970-01-01
      相关资源
      最近更新 更多