今天小编给大家分享一下Android怎么使用RecyclerView实现瀑布流界面的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
什么是瀑布流
最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数做的好的大厂的APP都是这种布局、尤以UGC(UGC它的中文意思是用户生产内容的意思,简称为UGC)为主的APP采用此布局最多像:知乎上的精品贴、推荐贴、小红书种草等都是这种风格。
瀑布流布局的优点为:
1.吸引用户,当用户在浏览瀑布流式布局的时候(这里抛开懒加载),用户会产生一种错觉,就是信息是不停的在更新的,这会激发用户的好奇心,使用户不停的往下滑动。
2.良好视觉体验,采用瀑布流布局的方式可以打破常规网站布局排版,给用户眼前一亮的新鲜感,用户在浏览内容时会感到很有新鲜感,带来良好的视觉体验。
3.更好的适应移动端,由于移动设备屏幕比电脑小,一个屏幕显示的内容不会非常多,因此可能要经常翻页。而在建网站时使用瀑布流布局,用户则只需要进行滚动就能够不断浏览内容。(这一点和懒加载有一点像)
怎么实现瀑布流
网上有一些第三方控件使用了瀑布流,但是这些第三方控件都已经废弃或者是停更了。这些第三方控件本人都用过,不是有各种BUG就是把问题搞了很复杂。这东西其实很简单,一天内就可以做出生产级别的应用了,哪有这么难。
难就是难在太多初学者为了赶项目或者说很多人急功近利,只想着copy paste,因此搞了一堆其实无用的代码还把问题“混搅”了。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
基于MVVM设计模式的RecyclerView实现瀑布流代码
工程整体结构
这是一个使用androidx的基于mvvm的工程。
至于如何把一个工程变成androidx和mvvm此处就不再赘述了,在我前面的博客中已经写了很详细了。
布局
activity_main.xml布局
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</layout>
瀑布流中具体的明细布局-rv_item.xml
在明细布局里,整体瀑布流墙就两个元素,一个是照片的url另一个是文本框,实现很简单。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="org.mk.android.demo.demo.staggerdrecyclerview.RVBean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/rvImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitXY"
app:url="@{item.url}" />
<TextView
android:id="@+id/rvTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:layout_margin="4dp"
android:text="@{item.text}" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
现在就来看我们的代码。
后端代码
RVBean.java
package org.mk.android.demo.demo.staggerdrecyclerview;
import android.util.Log;
import android.widget.ImageView;
import androidx.databinding.BindingAdapter;
import com.bumptech.glide.Glide;
import java.util.Objects;
public class RVBean {
private String url;
private String text;
private final static String TAG = "DemoStaggerdRecyclerView";
@BindingAdapter("url")
public static void loadImg(ImageView imageView, String url) {
Glide.with(imageView).load(url).into(imageView);
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public RVBean(String url, String text) {
this.url = url;
this.text = text;
}
@Override
public boolean equals(Object o) {
if (this == o) {
//Log.i(TAG, ">>>>>>this==o return true");
return true;
}
if (o == null || getClass() != o.getClass()) {
//Log.i(TAG, ">>>>>>o==null||getClass()!=o.getClass() is false");
return false;
}
RVBean rvBean = (RVBean) o;
if (rvBean.url.length() != url.length() || rvBean.text.length() != text.length()) {
//Log.i(TAG, ">>>>>>target length()!=existed url length");
return false;
}
if(url.equals(rvBean.url)&&text.equals(rvBean.text)){
//Log.i(TAG,">>>>>>url euqlas && text equals");
return true;
}else{
//Log.i(TAG,">>>>>>not url euqlas && text equals");
return false;
}
}
@Override
public int hashCode() {
int hashCode = Objects.hash(url, text);
//Log.i(TAG, ">>>>>>hashCode->" + hashCode);
return hashCode;
}
}
代码中它定义了两个元素,一个为文本框,一个为用于加载网络图片的url,网络图片我用的是我另一台VM上的Nginx做的静态图片资源服务。
RVAdapter.java
package org.mk.android.demo.demo.staggerdrecyclerview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import org.mk.android.demo.demo.staggerdrecyclerview.databinding.RvItemBinding;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
public class RVAdapter extends RecyclerView.Adapter<RVAdapter.VH> {
private Context context;
private List<RVBean> rvBeans;
private final static String TAG = "DemoStaggerdRecyclerView";
public RVAdapter(Context context, List<RVBean> rvBeans) {
this.context = context;
this.rvBeans = rvBeans;
}
@Override
public int getItemViewType(int position) {
return position;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new VH(DataBindingUtil.inflate(
LayoutInflater.from(context), R.layout.rv_item, parent, false).getRoot());
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
//try {
RvItemBinding binding = DataBindingUtil.bind(holder.itemView);
//binding.rvTextView.setText(rvBeans.get(position).getText());
binding.setItem(rvBeans.get(position));
/*
//Set size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//这个参数设置为true才有效,
Bitmap bmp = BitmapFactory.decodeFile(rvBeans.get(position).getUrl(), options);
//这里的bitmap是个空
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Glide.with(context).load(rvBeans.get(position).getUrl()).override(outWidth,
outHeight).into(binding.rvImageView);
} catch (Exception e) {
Log.e(TAG, ">>>>>>onbindViewHolder error: " + e.getMessage(), e);
}
*/
}
@Override
public int getItemCount() {
return rvBeans.size();
}
public class VH extends RecyclerView.ViewHolder {
public VH(@NonNull View itemView) {
super(itemView);
}
}
//增加外部调用增加一条记录
public void refreshDatas(List<RVBean> datas) {
int pc=0;
if (datas != null && datas.size() > 0) {
int oldSize = rvBeans.size();
//List<RVBean> refreshedData = new ArrayList<RVBean>();
boolean isItemExisted = false;
for (Iterator<RVBean> newData = datas.iterator(); newData.hasNext(); ) {
RVBean a = newData.next();
for (Iterator<RVBean> existedData = rvBeans.iterator(); existedData.hasNext(); ) {
RVBean b = existedData.next();
if (b.equals(a)) {
{
isItemExisted = true;
//Log.i(TAG, b.getText() + " -> " + b.getUrl() + " is existed");
break;
}
}
}
if (!isItemExisted) {
pc+=1;
rvBeans.add(a);
}
}
Log.i(TAG,">>>>>>pc->"+pc);
if(pc>0){
notifyItemRangeChanged(oldSize,rvBeans.size());
}
}
}
}
核心代码导读
1.这个adapter用的正是mvvm设计模式做的adapter;
2.这个adapter和网上那些错误、有坑的例子最大的不同在于getItemViewType方法内必须返回position,否则你的瀑布流在上划加载新数据时会产生界面内对照片重新进行左右切换、重排、或者把照片底部留出很大一块空白如:左边垂直排3张,右边一大边空白或者反之亦然的情况;
3.必须使用notifyItemRangeChanged来通知刷新新数据,网上很多例子用的是notifyDataSetChange或者是其它相关的notify,它们都是错的,这是因为RecyclerViewer在上划下划时会导致整个瀑布流重新布局、而RecyclerView里用的是Glide异步加载网络图片的,这会导致组件看到有一个组片就去开始计算它的高度而实际这个照片还未加载好因此才会导致RecyclerView在上划下划时整体布局重新刷新和重布局。一定记得这个notifyItemRangeChanged,同时这个方法在使用前加载所有的图片(list数据),传参有两个参数,参数1:加载新数据前原数据行.size(),参数2:新加载数据.size();
有了adapter我们来看我们的应用了。
应用前先别急,我们自定义了一个StaggeredGridLayoutManager。
自定义FullyStaggeredGridLayoutManager
这边先说一下,为什么要自定义这个StaggeredGridLayoutManager?
public class FullyStaggeredGridLayoutManager extends StaggeredGridLayoutManager {
大家可以认为,这个类是一个策略。这也是网上绝大部分教程根本不说的,这个策略就是从根本上避免RecyclerViewer在上划下划时不要进行左右切换、重新布局、图片闪烁以及“解决Scrollview中嵌套RecyclerView实现瀑布流时无法显示的问题,同时修复了子View显示时底部多出空白区域的问题”用的,我在代码中也做了注释。
此处要敲一下黑板了。因此整个RecyclerView要做到类淘宝、抖音的这种用户体验必须是adapter里的代码和这个自定义StaggeredGridLayoutManager结合起来才能做到。
因此我们下面就来看在MainActivity里如何把adapter结合着这个自定义的StaggeredGridLayoutManager的应用吧。
先上我们自定义的这个StaggeredGridLayoutManager-我们在此把它命名叫做:FullyStaggeredGridLayoutManager的全代码。
FullyStaggeredGridLayoutManager.java代码
package org.mk.android.demo.demo.staggerdrecyclerview;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import java.lang.reflect.Field;
/**
* @descride 解决Scrollview中嵌套RecyclerView实现瀑布流时无法显示的问题,同时修复了子View显示时底部多出空白区域的问题
*/
public class FullyStaggeredGridLayoutManager extends StaggeredGridLayoutManager {
private static boolean canMakeInsetsDirty = true;
private static Field insetsDirtyField = null;
private static final int CHILD_WIDTH = 0;
private static final int CHILD_HEIGHT = 1;
private static final int DEFAULT_CHILD_SIZE = 100;
private int spanCount = 0;
private final int[] childDimensions = new int[2];
private int[] childColumnDimensions;
private int childSize = DEFAULT_CHILD_SIZE;
private boolean hasChildSize;
private final Rect tmpRect = new Rect();
public FullyStaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public FullyStaggeredGridLayoutManager(int spanCount, int orientation) {
super(spanCount, orientation);
this.spanCount = spanCount;
}
public static int makeUnspecifiedSpec() {
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec,
int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
final int unspecified = makeUnspecifiedSpec();
if (exactWidth && exactHeight) {
// in case of exact calculations for both dimensions let's use default "onMeasure" implementation
super.onMeasure(recycler, state, widthSpec, heightSpec);
return;
}
final boolean vertical = getOrientation() == VERTICAL;
initChildDimensions(widthSize, heightSize, vertical);
int width = 0;
int height = 0;
// it's possible to get scrap views in recycler which are bound to old (invalid) adapter entities. This
// happens because their invalidation happens after "onMeasure" method. As a workaround let's clear the
// recycler now (it should not cause any performance issues while scrolling as "onMeasure" is never
// called whiles scrolling)
recycler.clear();
final int stateItemCount = state.getItemCount();
final int adapterItemCount = getItemCount();
childColumnDimensions = new int[adapterItemCount];
// adapter always contains actual data while state might contain old data (f.e. data before the animation is
// done). As we want to measure the view with actual data we must use data from the adapter and not from the
// state
for (int i = 0; i < adapterItemCount; i++) {
if (vertical) {
if (!hasChildSize) {
if (i < stateItemCount) {
// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
// we will use previously calculated dimensions
measureChild(recycler, i, widthSize, unspecified, childDimensions);
} else {
logMeasureWarning(i);
}
}
childColumnDimensions[i] = childDimensions[CHILD_HEIGHT];
//height += childDimensions[CHILD_HEIGHT];
if (i == 0) {
width = childDimensions[CHILD_WIDTH];
}
if (hasHeightSize && height >= heightSize) {
break;
}
} else {
if (!hasChildSize) {
if (i < stateItemCount) {
// we should not exceed state count, otherwise we'll get IndexOutOfBoundsException. For such items
// we will use previously calculated dimensions
measureChild(recycler, i, unspecified, heightSize, childDimensions);
} else {
logMeasureWarning(i);
}
}
width += childDimensions[CHILD_WIDTH];
if (i == 0) {
height = childDimensions[CHILD_HEIGHT];
}
if (hasWidthSize && width >= widthSize) {
break;
}
}
}
int[] maxHeight = new int[spanCount];
for (int i = 0; i < adapterItemCount; i++) {
int position = i % spanCount;
if (i < spanCount) {
maxHeight[position] += childColumnDimensions[i];
} else if (position < spanCount) {
int mixHeight = maxHeight[0];
int mixPosition = 0;
for (int j = 0; j < spanCount; j++) {
if (mixHeight > maxHeight[j]) {
mixHeight = maxHeight[j];
mixPosition = j;
}
}
maxHeight[mixPosition] += childColumnDimensions[i];
}
}
for (int i = 0; i < spanCount; i++) {
for (int j = 0; j < spanCount - i - 1; j++) {
if (maxHeight[j] < maxHeight[j + 1]) {
int temp = maxHeight[j];
maxHeight[j] = maxHeight[j + 1];
maxHeight[j + 1] = temp;
}
}
}
height = maxHeight[0];//this is max height
if (exactWidth) {
width = widthSize;
} else {
width += getPaddingLeft() + getPaddingRight();
if (hasWidthSize) {
width = Math.min(width, widthSize);
}
}
if (exactHeight) {
height = heightSize;