RecyclerView实现画廊效果
目录
RecyclerView实现画廊效果 1
目标的画廊效果 2
RecyclerView提供的默认效果 2
关键要点 2
实现步骤 2
4.1添加依赖 2
4.2定义子视图布局 3
4.3定义Acitivity布局 3
4.4初始化RecyclerView 4
4.5自定义ItemDecoration 5
4.6自定义LinearLayoutManager 7
代码分享 10
目标的画廊效果
希望实现选中的布局始终处于屏幕位置的中央,同时滑动后能够停留在屏幕位置的中央,如以下效果图所示:
快速滑动的效果
慢速滑动的效果
最后一张图片停止的效果,居中显示
第一张图停住效果
RecyclerView提供的默认效果
RecyclerView提供的默认效果是从第一个显示,而且滑动结束后可以停留在任意位置。无法确保选中的位置居中显示。
关键要点
要实现这个目标,通过阅读多篇其他人的介绍文档,以及整理其他文章未重点说明的内容,总结起来主要的重点难点包括以下内容:
序号 | 重点难点 | 思路 |
---|---|---|
1 | 实现第一个视图和最后一个视图显示时位置居中 | 针对第一个视图和最后一个视图设置边距,确保显示时能够居中 |
2 | 计算第一个视图和最后一个视图合适的边距 | 使用RecyclerView布局的宽度的1/2 – 子视图布局的宽度的1/2 |
3 | 子视图尚无法测量时,获取其布局宽度 | 通过子视图绘制完毕后测量 |
4 | 实现平滑的滑动效果 | 使用SmoothScroller设置恰当的滑动速度,也可以根据待滑动的距离的差异设置差异化速度,避免滑动时间过长 |
4.实现步骤
实现步骤中仅列举重要的代码片段的讲解,最后会提供全部的详细源码供参考。
4.1添加依赖
implementation 'androidx.appcompat:appcompat:1.1.0'
//Glide
implementation ("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"
}
4.2定义子视图布局
布局由一个图片标题和图片构成
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="125dp">
<ImageView
android:id="@+id/imageview"
android:layout_width="100dp"
android:layout_height="100dp"/>
<TextView
android:id="@+id/textview"
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"/>
</LinearLayout>
4.3定义Acitivity布局
设置一个RecyclerView,本次重写了RecyclerView为了实现选中位置放大,以及两侧位置相应缩小的效果
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="基于RecyclerView实现照片画廊效果"
android:layout_marginBottom="100dp"/>
<com.guo.picturegallery.GalleryRecyclerView
android:id="@+id/recycler_view_gallery"
android:layout_width="match_parent"
android:layout_height="250dp"/>
</LinearLayout>
4.4初始化RecyclerView
设置Adapter , LayoutManager 和Decoration
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 对RecyclerView初始化
*/
RecyclerView recyclerViewGallery = findViewById(R.id.recycler_view_gallery);
PictureAdapter adapter = new PictureAdapter();
adapter.setDataList(getStarsList());
/**
* 基于自定义的LayoutManager实现子视图在RecyclerView在屏幕的位置居中
*/
LinearLayoutManager layoutManager = new GalleryLayoutManager(this,RecyclerView.HORIZONTAL,false);
recyclerViewGallery.setLayoutManager(layoutManager);
recyclerViewGallery.addItemDecoration(new HorizontalDecoration(10));
recyclerViewGallery.setAdapter(adapter);
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerViewGallery);
recyclerViewGallery.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
/**
* 点击屏幕中的非居中位置时,可以使点击的项目平滑移动至屏幕中央
*/
adapter.setOnItemClickListener(new BaseAdapter.OnItemClickListener() {
@Override
public void onItemClick(int pos) {
recyclerViewGallery.smoothScrollToPosition(pos);
}
});
}
4.5自定义ItemDecoration
自定义ItemDecoration,设置第一个视图和最后一个视图偏移的距离,确保第一个视图和最后一个视图在屏幕中居中。
如上图所示,要实现第一个视图和最后一个视图居中,最重要的是准确计算边距,边距为RecyclerView布局宽度与子视图宽度差的一半。具体实现代码如下:
/**
* 自定义ItemDecoration,设置第一个视图和最后一个视图偏移的距离,确保第一个视图和最后一个视图在屏幕中居中<P/>
* @author mailanglideguozhe 20210520
*/
public class HorizontalDecoration extends RecyclerView.ItemDecoration {
private int space = 0;
/**
* 第一个视图和最后一个视图偏移的距离
*/
private int distance = 0;
private static final String TAG = "HorizontalDecoration";
/**
* 设置RecyclerView子视图的边距,本示例仅用于定义两个子视图之间的边距,为space*2
* @param space 设置的边距
*/
public HorizontalDecoration(int space) {
this.space = space;
}
/**
* 获取子视图的边距
* @param view 子视图
* @param parent RecyclerView对象
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int pos = parent.getChildAdapterPosition(view);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams)view.getLayoutParams();
/**
* 仅计算一次偏移边距即可,无需重复计算<P/>
* 由于此时View并未完成测量,无法基于测量获取其宽度;思路是在view绘制完成后再进行测量,并设置第一个的左边距
*/
if(distance <= 0){
view.post(new Runnable() {
@Override
public void run() {
distance = dtDistance(parent,view);
//设置第一个视图的左边距
View childView = parent.getChildAt(0);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams)childView.getLayoutParams();
layoutParams.setMargins(distance,0,space,0);
childView.setLayoutParams(layoutParams);
//打开后默认显示第一个(居中显示)
parent.scrollToPosition(0);
}
});
}
/**
* 通过设置Item左右边距实现第一个左侧和最后一个右侧设置边距,确保显示的视图位于屏幕中间
*/
int itemCount = parent.getAdapter().getItemCount();
if(pos == 0){
layoutParams.setMargins(distance,0,space,0);
}else if(pos == itemCount-1){
layoutParams.setMargins(space,0,distance,0);
}else {
layoutParams.setMargins(space,0,space,0);
}
/**
* 更新子视图的边距
*/
view.setLayoutParams(layoutParams);
super.getItemOffsets(outRect, view, parent, state);
}
/**
* 为了使第一个和最后一个item居中,需要设置相应偏移,偏移量为RecyclerView布局宽度减去子视图的一半<P/>
* 注意此处由于子视图并未实例化完成,无法通过测量得知其宽度,故需要直接获取布局宽度参数得知<P/>
*/
public int dtDistance(RecyclerView recyclerView , View childView){
int width = recyclerView.getWidth() != 0 ? recyclerView.getWidth():recyclerView.getMeasuredWidth();
//此处需要获取子视图布局的宽度,注意此处由于子视图并未实例化完成,无法通过测量得知其宽度
childView.getMeasuredWidth();
int childWidth = childView.getWidth();
//第一个视图左侧偏移量,最后一个视图右侧偏移量
return width/2 -childWidth/2;
}
}
4.6自定义LinearLayoutManager
为了点击item使相应的item居中显示,且实现平滑的滚动,重写LinearLayoutManager。原始的RecyclerView是可以停留在任意位置的,在目标场景下,必须确保有一个视图是在RecyclerView中间位置的,因为在原始逻辑的基础上需要重新校正RecyclerView子视图的位置。校正
距离的计算逻辑如下:
实现代码如下:
/**
* 为了点击item使相应的item居中显示,且实现RecyclerView的平缓滑动,重写LinearLayoutManager<P/>
* @author mailanglideguozhe 20210520
*/
public class GalleryLayoutManager extends LinearLayoutManager {
public GalleryLayoutManager(Context context) {
super(context);
}
public GalleryLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public GalleryLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 重写smoothScrollToPosition方法,实现平滑滑动及停留在中间位置
* @param recyclerView
* @param state
* @param position 目标位置
*/
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
RecyclerView.SmoothScroller smoothScroller = new RecyclerviewSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
/**
* 重写LinearSmoothScroller自定义 中间对齐需要的校正位置
*/
private static class RecyclerviewSmoothScroller extends LinearSmoothScroller{
public RecyclerviewSmoothScroller(Context context) {
super(context);
}
/**
* 为了确保子视图在显示的位置中间位置,需要设置应当校正子视图的位置。<P/>
* 子视图校正需要移动的距离为Recycler布局中间位置与子视图中间位置的距离<P/>
* @param viewStart 子视图的左侧位置
* @param viewEnd 子视图的右侧位置
* @param boxStart RecyclerView视图的左侧位置
* @param boxEnd RecyclerView视图的左侧位置
* @return 返回子视图校正需要移动的距离
*/
@Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
}
/**
* 计算滑动的速度,返回1px滑动所需的时间,举例 如返回0.8f,即滑动1000个像素点距离需要0.8s<P/>
* @param displayMetrics
* @return 返回1px滑动所需的时间,单位ms
*/
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return super.calculateSpeedPerPixel(displayMetrics);
}
}
}
至此实现此功能的核心逻辑全部讲解完毕
代码分享
本文是第一次撰写技术分享文章,由于个人经验不足,难免出现错漏,欢迎大家指正。关于Android源码分析过程才是本源,由于时间问题,将放在下一篇介绍,以下是该项目源码,如果你觉得效果满意,还请帮忙点赞。如有宝贵建议,欢迎留言交流。
https://gitee.com/com_mailanglidegezhe/gallery-recycler-view.git