首先感谢 启舰 前辈,他的 Android自定义控件三部曲 真的很经典,推荐大家去看,本篇文章也是借鉴他的 自定义控件三部曲之动画篇(十三)——实现ListView Item进入动画 完成的。
目录
一.找到为 item 添加动画的合适合适位置
想为 item 添加入场动画,就要在 RecyclerView 的 adapter 中做文章。
我希望列表 item 能够在每次 进入屏幕可见区域的时候都执行动画,而不是只在进入列表界面的时候有动画(像BaseRecyclerViewAdapterHelper那样的item入场动画),那就去 RecyclerView.Adapter 下找一找有没有和 item 可见性相关的方法。那就是 onViewAttachedToWindow(VH holder) 方法。
/**
* Called when a view created by this adapter has been attached to a window.
*
* <p>This can be used as a reasonable signal that the view is about to be seen
* by the user. If the adapter previously freed any resources in
* {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
* those resources should be restored here.</p>
*
* @param holder Holder of the view being attached
*/
public void onViewAttachedToWindow(@NonNull VH holder) {
}
注释翻译大意:该方法在 adapter 创建的 view 依附到 window 中时调用,这能用作 view 刚好对用户可见的合理信号。
通过 VH holder 可以获取 itemView,进而为 itemView 添加动画。
二.使用 Animation 创建 item 动画
1.创建 item 的布局 xml
item_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical">
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_dark"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:text="This is item 0"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
2.adapter 类
package com.leading.recyclerviewitemsanimationtest;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.CustomViewHolder> {
private static final String TAG = "CustomRecyclerViewAdapt";
private ArrayList<String> mData;
private RecyclerView mRecyclerView;
/**
* RecyclerView 是否正在向上滚动
*/
private boolean isPullingUp = false;
/**
* RecyclerView 是否正在向下滚动
*/
private boolean isPullDown = false;
/**
* RecyclerView item的入场动画
*/
private Animation bottomInAnim;
private Animation topInAnim;
public CustomRecyclerViewAdapter(ArrayList<String> mData, final RecyclerView mRecyclerView) {
this.mData = mData;
this.mRecyclerView = mRecyclerView;
bottomInAnim = AnimationUtils.loadAnimation(mRecyclerView.getContext(), R.anim.bottom_up_in_anim);
topInAnim = AnimationUtils.loadAnimation(mRecyclerView.getContext(), R.anim.top_down_in_anim);
/**
* 为RecyclerView设置滚动监听
*/
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
isPullingUp = (dy > 0);
isPullDown = (dy < 0);
}
});
}
@Override
public void onViewAttachedToWindow(@NonNull final CustomViewHolder holder) {
super.onViewAttachedToWindow(holder);
//清除当前显示区域中所有item的动画
int childCount = mRecyclerView.getChildCount();
Log.e(TAG, "onViewAttachedToWindow: childCount--> " + childCount);
for (int i = 0; i < childCount; i++) {
View child = mRecyclerView.getChildAt(i);
if (child != null) {
child.clearAnimation();
}
}
//然后给当前item添加上动画
if (isPullDown) {
holder.itemView.startAnimation(topInAnim);
}
if (isPullingUp) {
holder.itemView.startAnimation(bottomInAnim);
}
}
@NonNull
@Override
public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_recyclerview_item, parent, false);
return new CustomViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull CustomViewHolder holder, int position) {
holder.textView.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
static class CustomViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
CustomViewHolder(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.item_tv);
}
}
}
在构造方法中传入列表数据源 ArrayList<String> mData 和RecyclerView 对象 RecyclerView mRecyclerView。我想要 item 在列表向上或向下滚动时都有动画,那就需要监听 RecyclerView的滚动了,
/**
* 为RecyclerView设置滚动监听
*/
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
isPullingUp = (dy > 0);
isPullDown = (dy < 0);
}
});
让我们来看下 RecyclerView.OnScrollListener 的源码
/**
* An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
* has occurred on that RecyclerView.
* <p>
* @see RecyclerView#addOnScrollListener(OnScrollListener)
* @see RecyclerView#clearOnChildAttachStateChangeListeners()
*
*/
public abstract static class OnScrollListener {
/**
* Callback method to be invoked when RecyclerView's scroll state changes.
*
* @param recyclerView The RecyclerView whose scroll state has changed.
* @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
* {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
*/
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState){}
/**
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
* called after the scroll has completed.
* <p>
* This callback will also be called if visible item range changes after a layout
* calculation. In that case, dx and dy will be 0.
*
* @param recyclerView The RecyclerView which scrolled.
* @param dx The amount of horizontal scroll.
* @param dy The amount of vertical scroll.
*/
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){}
}
重点看下这个抽象类中的 onScrolled() 方法,注释翻译大意为:当 RecyclerView 被滚动时调用的回调方法,该方法将在滚动完成后调用。如果在布局计算后可见 item 的 范围发生变化,也将调用此回调,在这种情况下,dX和dY将是0。
我们知道 android 中视图的坐标是以左上角为圆点的,x轴向右为正,y轴向下为正,所以 dy>0 时 是列表向上滚动,dy<0 时列表向下滚动,所以在 onScrolled 方法中通过dy的正负就可以知道列表向上还是向下滚动了,可以设置两个 flag成员变量来记录并在adapter 的 onViewAttachedToWindow() 方法中为 item 添加不同的动画。
3.从 xml 中加载 item 的动画
anim/bottom_up_in_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="800">
<translate
android:fromYDelta="100%"
android:toYDelta="0" />
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
</set>
,
anim/top_down_in_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="800">
<translate
android:fromYDelta="-100%"
android:toYDelta="0" />
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
</set>
使用 AnimationUtils 的 loadAnimation 方法来从 xml 中加载动画。
在 adapter 的 onViewAttachedToWindow() 方法中,当列表向上滑动时,我们为当前刚刚显示到屏幕可见区域的 item 添加从 bottom_up_in_anim.xml 加载得到的动画;当列表向下滑动时,我们为当前刚刚显示到屏幕可见区域的 item 添加从 top_down_in_anim.xml 加载得到的动画。
这样操作,当快速滑动列表时,多个 item 会在较短的时间内同时呈现在屏幕上,这些 item 都会执行动画。为了在向下或向上滑动列表时,只让屏幕第一个可见的或最后一个可见的 item 执行动画,在为 item 添加动画之前,需要遍历清除其他 item 的动画。
//清除当前显示区域中所有item的动画
int childCount = mRecyclerView.getChildCount();
Log.e(TAG, "onViewAttachedToWindow: childCount--> " + childCount);
for (int i = 0; i < childCount; i++) {
View child = mRecyclerView.getChildAt(i);
if (child != null) {
child.clearAnimation();
}
}
清除之后,再为当前 item 添加动画
//然后给当前item添加上动画
if (isPullDown) {
holder.itemView.startAnimation(topInAnim);
}
if (isPullingUp) {
holder.itemView.startAnimation(bottomInAnim);
}
4.主界面 MainActivity
布局 layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.class
package com.leading.recyclerviewitemsanimationtest;
import android.os.Bundle;
import java.util.ArrayList;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
SpacesItemDecoration spacesItemDecoration = new SpacesItemDecoration(20);
recyclerView.addItemDecoration(spacesItemDecoration);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
CustomRecyclerViewAdapter customRecyclerViewAdapter = new CustomRecyclerViewAdapter(getData(), recyclerView);
recyclerView.setAdapter(customRecyclerViewAdapter);
}
private ArrayList<String> getData() {
ArrayList<String> data = new ArrayList<>();
for (int i = 0; i < 20; i++) {
data.add("This is Item " + i);
}
return data;
}
}
这里给 RecyclerView 设置了 ItemDecoration 来为 item 添加间距。
SpacesItemDecoration.class
package com.leading.recyclerviewitemsanimationtest;
import android.graphics.Rect;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpacesItemDecoration(int space) {
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
// Add top margin only for the first item to avoid double space between items
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = space;
}
}
}
5.效果
运行效果