Android入門(mén)之使用RecyclerView完美實(shí)現(xiàn)瀑布流界面詳解
前言
網(wǎng)上充滿(mǎn)著不完善的基于RecyclerView的瀑布流實(shí)現(xiàn),要么根本是錯(cuò)的、要么就是只知其一不知其二、要么就是一充訴了一堆無(wú)用代碼、要么用的是古老的MVC設(shè)計(jì)模式。
一個(gè)真正的、用戶(hù)體驗(yàn)類(lèi)似于淘寶、抖音的瀑布流怎么實(shí)現(xiàn)目前基本為無(wú)解。因?yàn)楸救苏米约嚎臻e時(shí)也在做前后臺(tái)一體化開(kāi)發(fā),所以直接從本人自己項(xiàng)目里(mao-sir.com)公開(kāi)一部分核心代碼以便于加快大家的學(xué)習(xí)進(jìn)度,以使得各位不要在這種小東西上折騰太多時(shí)間。
什么是瀑布流
注意看這邊的這個(gè)布局,大家有沒(méi)有發(fā)覺(jué)這些照片分成左右、最多的還有分成三列的,但是每一行的照片、視頻、內(nèi)容的高度是“錯(cuò)開(kāi)”的。最早采用此布局的網(wǎng)站是Pinterest,逐漸在國(guó)內(nèi)流行開(kāi)來(lái)。國(guó)內(nèi)大多數(shù)做的好的大廠的APP都是這種布局、尤以UGC(UGC它的中文意思是用戶(hù)生產(chǎn)內(nèi)容的意思,簡(jiǎn)稱(chēng)為UGC)為主的APP采用此布局最多像:知乎上的精品貼、推薦貼、小紅書(shū)種草等都是這種風(fēng)格。
瀑布流布局的優(yōu)點(diǎn)為:
1.吸引用戶(hù),當(dāng)用戶(hù)在瀏覽瀑布流式布局的時(shí)候(這里拋開(kāi)懶加載),用戶(hù)會(huì)產(chǎn)生一種錯(cuò)覺(jué),就是信息是不停的在更新的,這會(huì)激發(fā)用戶(hù)的好奇心,使用戶(hù)不停的往下滑動(dòng)。
2.良好視覺(jué)體驗(yàn),采用瀑布流布局的方式可以打破常規(guī)網(wǎng)站布局排版,給用戶(hù)眼前一亮的新鮮感,用戶(hù)在瀏覽內(nèi)容時(shí)會(huì)感到很有新鮮感,帶來(lái)良好的視覺(jué)體驗(yàn)。
3.更好的適應(yīng)移動(dòng)端,由于移動(dòng)設(shè)備屏幕比電腦小,一個(gè)屏幕顯示的內(nèi)容不會(huì)非常多,因此可能要經(jīng)常翻頁(yè)。而在建網(wǎng)站時(shí)使用瀑布流布局,用戶(hù)則只需要進(jìn)行滾動(dòng)就能夠不斷瀏覽內(nèi)容。(這一點(diǎn)和懶加載有一點(diǎn)像)
怎么實(shí)現(xiàn)瀑布流
網(wǎng)上有一些第三方控件使用了瀑布流,但是這些第三方控件都已經(jīng)廢棄或者是停更了。這些第三方控件本人都用過(guò),不是有各種BUG就是把問(wèn)題搞了很復(fù)雜。這東西其實(shí)很簡(jiǎn)單,一天內(nèi)就可以做出生產(chǎn)級(jí)別的應(yīng)用了,哪有這么難。
難就是難在太多初學(xué)者為了趕項(xiàng)目或者說(shuō)很多人急功近利,只想著copy paste,因此搞了一堆其實(shí)無(wú)用的代碼還把問(wèn)題“混攪”了。
因此本篇會(huì)從本質(zhì)上還原最最干凈的瀑布流,同時(shí)本人會(huì)基于mvvm設(shè)計(jì)模式來(lái)講這個(gè)瀑布流。我們用的就是Android原生的控件:RecyclerView組件。下面是類(lèi)淘寶、抖音用戶(hù)極致體驗(yàn)的一個(gè)瀑布流“正例”(我們?cè)谖奈催€會(huì)給出一個(gè)體驗(yàn)不好的反例供讀者對(duì)比)。
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rv" android:layout_width="match_parent" android:layout_height="wrap_content"> </androidx.recyclerview.widget.RecyclerView>
下面我們來(lái)看實(shí)現(xiàn) 。
基于MVVM設(shè)計(jì)模式的RecyclerView實(shí)現(xiàn)瀑布流代碼
工程整體結(jié)構(gòu)
這是一個(gè)使用androidx的基于mvvm的工程。
至于如何把一個(gè)工程變成androidx和mvvm此處就不再贅述了,在我前面的博客中已經(jīng)寫(xiě)了很詳細(xì)了。
布局
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>
瀑布流中具體的明細(xì)布局-rv_item.xml
在明細(xì)布局里,整體瀑布流墻就兩個(gè)元素,一個(gè)是照片的url另一個(gè)是文本框,實(shí)現(xiàn)很簡(jiǎn)單。
<?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>
現(xiàn)在就來(lái)看我們的代碼。
后端代碼
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; } }
代碼中它定義了兩個(gè)元素,一個(gè)為文本框,一個(gè)為用于加載網(wǎng)絡(luò)圖片的url,網(wǎng)絡(luò)圖片我用的是我另一臺(tái)VM上的Nginx做的靜態(tài)圖片資源服務(wù)。
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;//這個(gè)參數(shù)設(shè)置為true才有效, Bitmap bmp = BitmapFactory.decodeFile(rvBeans.get(position).getUrl(), options); //這里的bitmap是個(gè)空 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); } } //增加外部調(diào)用增加一條記錄 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()); } } } }
核心代碼導(dǎo)讀
1.這個(gè)adapter用的正是mvvm設(shè)計(jì)模式做的adapter;
2.這個(gè)adapter和網(wǎng)上那些錯(cuò)誤、有坑的例子最大的不同在于getItemViewType方法內(nèi)必須返回position,否則你的瀑布流在上劃加載新數(shù)據(jù)時(shí)會(huì)產(chǎn)生界面內(nèi)對(duì)照片重新進(jìn)行左右切換、重排、或者把照片底部留出很大一塊空白如:左邊垂直排3張,右邊一大邊空白或者反之亦然的情況;
3.必須使用notifyItemRangeChanged來(lái)通知刷新新數(shù)據(jù),網(wǎng)上很多例子用的是notifyDataSetChange或者是其它相關(guān)的notify,它們都是錯(cuò)的,這是因?yàn)镽ecyclerViewer在上劃下劃時(shí)會(huì)導(dǎo)致整個(gè)瀑布流重新布局、而RecyclerView里用的是Glide異步加載網(wǎng)絡(luò)圖片的,這會(huì)導(dǎo)致組件看到有一個(gè)組片就去開(kāi)始計(jì)算它的高度而實(shí)際這個(gè)照片還未加載好因此才會(huì)導(dǎo)致RecyclerView在上劃下劃時(shí)整體布局重新刷新和重布局。一定記得這個(gè)notifyItemRangeChanged,同時(shí)這個(gè)方法在使用前加載所有的圖片(list數(shù)據(jù)),傳參有兩個(gè)參數(shù),參數(shù)1:加載新數(shù)據(jù)前原數(shù)據(jù)行.size(),參數(shù)2:新加載數(shù)據(jù).size();
有了adapter我們來(lái)看我們的應(yīng)用了。
應(yīng)用前先別急,我們自定義了一個(gè)StaggeredGridLayoutManager。
自定義FullyStaggeredGridLayoutManager
這邊先說(shuō)一下,為什么要自定義這個(gè)StaggeredGridLayoutManager?
public class FullyStaggeredGridLayoutManager extends StaggeredGridLayoutManager {
大家可以認(rèn)為,這個(gè)類(lèi)是一個(gè)策略。這也是網(wǎng)上絕大部分教程根本不說(shuō)的,這個(gè)策略就是從根本上避免RecyclerViewer在上劃下劃時(shí)不要進(jìn)行左右切換、重新布局、圖片閃爍以及“解決Scrollview中嵌套R(shí)ecyclerView實(shí)現(xiàn)瀑布流時(shí)無(wú)法顯示的問(wèn)題,同時(shí)修復(fù)了子View顯示時(shí)底部多出空白區(qū)域的問(wèn)題”用的,我在代碼中也做了注釋。
此處要敲一下黑板了。因此整個(gè)RecyclerView要做到類(lèi)淘寶、抖音的這種用戶(hù)體驗(yàn)必須是adapter里的代碼和這個(gè)自定義StaggeredGridLayoutManager結(jié)合起來(lái)才能做到。
因此我們下面就來(lái)看在MainActivity里如何把a(bǔ)dapter結(jié)合著這個(gè)自定義的StaggeredGridLayoutManager的應(yīng)用吧。
先上我們自定義的這個(gè)StaggeredGridLayoutManager-我們?cè)诖税阉凶觯篎ullyStaggeredGridLayoutManager的全代碼。
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中嵌套R(shí)ecyclerView實(shí)現(xiàn)瀑布流時(shí)無(wú)法顯示的問(wèn)題,同時(shí)修復(fù)了子View顯示時(shí)底部多出空白區(qū)域的問(wèn)題 */ 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; } else { height += getPaddingTop() + getPaddingBottom(); if (hasHeightSize) { height = Math.min(height, heightSize); } } setMeasuredDimension(width, height); } private void logMeasureWarning(int child) { if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." + "To remove this message either use #setChildSize() method or don't run RecyclerView animations"); } } private void initChildDimensions(int width, int height, boolean vertical) { if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) { // already initialized, skipping return; } if (vertical) { childDimensions[CHILD_WIDTH] = width; childDimensions[CHILD_HEIGHT] = childSize; } else { childDimensions[CHILD_WIDTH] = childSize; childDimensions[CHILD_HEIGHT] = height; } } @Override public void setOrientation(int orientation) { // might be called before the constructor of this class is called //noinspection ConstantConditions if (childDimensions != null) { if (getOrientation() != orientation) { childDimensions[CHILD_WIDTH] = 0; childDimensions[CHILD_HEIGHT] = 0; } } super.setOrientation(orientation); } public void clearChildSize() { hasChildSize = false; setChildSize(DEFAULT_CHILD_SIZE); } public void setChildSize(int childSize) { hasChildSize = true; if (this.childSize != childSize) { this.childSize = childSize; requestLayout(); } } private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) { final View child; try { child = recycler.getViewForPosition(position); } catch (IndexOutOfBoundsException e) { if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "LinearLayoutManager doesn't work well with animations. Consider switching them off", e); } return; } final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams(); final int hPadding = getPaddingLeft() + getPaddingRight(); final int vPadding = getPaddingTop() + getPaddingBottom(); final int hMargin = p.leftMargin + p.rightMargin; final int vMargin = p.topMargin + p.bottomMargin; // we must make insets dirty in order calculateItemDecorationsForChild to work makeInsetsDirty(p); // this method should be called before any getXxxDecorationXxx() methods calculateItemDecorationsForChild(child, tmpRect); final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child); final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child); final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally()); final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically()); child.measure(childWidthSpec, childHeightSpec); dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin; dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin; // as view is recycled let's not keep old measured values makeInsetsDirty(p); recycler.recycleView(child); } private static void makeInsetsDirty(RecyclerView.LayoutParams p) { if (!canMakeInsetsDirty) { return; } try { if (insetsDirtyField == null) { insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty"); insetsDirtyField.setAccessible(true); } insetsDirtyField.set(p, true); } catch (NoSuchFieldException e) { onMakeInsertDirtyFailed(); } catch (IllegalAccessException e) { onMakeInsertDirtyFailed(); } } private static void onMakeInsertDirtyFailed() { canMakeInsetsDirty = false; if (BuildConfig.DEBUG) { Log.w("LinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect"); } } }
MainActivity.java
從這兒開(kāi)始我們要進(jìn)入正題了,這邊要說(shuō)真正的RecyclerView的應(yīng)用了。
在此演示代碼塊里為了同時(shí)便于初學(xué)者和正在尋找RecyclerView上劃下劃時(shí)錯(cuò)位過(guò)大、重新布局影響體驗(yàn)的老手同時(shí)閱讀和查詢(xún)問(wèn)題時(shí)方便,我要說(shuō)一下整個(gè)Demo代碼運(yùn)行的設(shè)計(jì)思路如下:
1.上手先加載12條數(shù)據(jù);
2.對(duì)上劃(手指按住屏幕向上拉),一直拉、拉、拉,拉到第12條,觸發(fā)了RecyclerView的onScrollStateChanged中的“往上拉拉不動(dòng)”事件后開(kāi)始再加載6條數(shù)據(jù),以模擬實(shí)際項(xiàng)目中的“翻頁(yè)時(shí)走一下后臺(tái)API取新數(shù)據(jù)”的過(guò)程;
package org.mk.android.demo.demo.staggerdrecyclerview; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import org.mk.android.demo.demo.staggerdrecyclerview.databinding.ActivityMainBinding; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; private List<RVBean> rvBeanList = new ArrayList<>(); private RVAdapter adapter; private final static String TAG = "DemoStaggerdRecyclerView"; private final static String CDN_URL="http://172.16.4.249/mkcdn"; private FullyStaggeredGridLayoutManager slm=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.activity_main, null, false); setContentView(binding.getRoot()); slm=new FullyStaggeredGridLayoutManager(2, FullyStaggeredGridLayoutManager.VERTICAL); binding.rv.setLayoutManager(slm); ((SimpleItemAnimator)binding.rv.getItemAnimator()).setSupportsChangeAnimations(false); ((DefaultItemAnimator) binding.rv.getItemAnimator()).setSupportsChangeAnimations(false); binding.rv.getItemAnimator().setChangeDuration(0); binding.rv.setHasFixedSize(true); initData(); } private void initData() { rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_13.jpeg", "1")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_14.jpeg", "2")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_15.jpeg", "3")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_16.jpeg", "4")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_17.jpeg", "5")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_18.jpeg", "6")); rvBeanList.add(new RVBean(CDN_URL+"img/recommend/recom_19.jpeg", "7")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_20.jpeg", "8")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_21.jpeg", "9")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_22.jpeg", "10")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_23.jpeg", "11")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_24.jpeg", "12")); adapter = new RVAdapter(this, rvBeanList); //主要就是這個(gè)LayoutManager,就是用這個(gè)來(lái)實(shí)現(xiàn)瀑布流的,2表示有2列(垂直)或3行(水平),我們這里用的垂直VERTICAL //binding.rv.addItemDecoration(new SpaceItemDecoration(2, 20)); binding.rv.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) { Log.i(TAG, "上拉拉不動(dòng)時(shí)觸發(fā)加載新數(shù)據(jù)"); rvBeanList = new ArrayList<>(); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_25.jpeg", "13")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_26.jpeg", "14")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_27.jpeg", "15")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_28.jpeg", "16")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_29.jpeg", "17")); rvBeanList.add(new RVBean(CDN_URL+"/img/recommend/recom_30.jpeg", "18")); adapter.refreshDatas(rvBeanList); } if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) { Log.i(TAG, "下拉拉不動(dòng)時(shí)觸發(fā)加載新數(shù)據(jù)"); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); slm.invalidateSpanAssignments();//防止第一行到頂部有空白 } }); //((SimpleItemAnimator)RecyclerView.getItemAnimator()).setSupportsChangeAnimations(false); binding.rv.setAdapter(adapter); } }
核心代碼導(dǎo)讀:
以下這一陀就是我說(shuō)的使用自定義的StaggeredGridLayoutManager+adapter中的事件覆蓋一起實(shí)現(xiàn)了著名的RecyclerView上劃下劃時(shí)重新布局、翻頁(yè)的梗。
同時(shí),一定要記得覆蓋RecyclerView的onScrolled事件,在事件中加入這樣的代碼
@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); slm.invalidateSpanAssignments();//防止第一行到頂部有空白 }
總結(jié)
正確的做法
因此這邊總結(jié)一下共有5個(gè)點(diǎn),做到這5個(gè)點(diǎn)才能真正避免網(wǎng)上一堆的關(guān)于RecyclerView快速上劃下劃時(shí)整個(gè)界面產(chǎn)生了春左右列切換、重新布局、布局不合理如:左邊垂直排了3-4個(gè)圖片而把右邊留出一大塊空白的梗的綜合手段:
1.必須使用一個(gè)自定義的StaggeredGridLayoutManager,你可以直接使用我博客中的代碼,它來(lái)自于我正在制作的上生產(chǎn)的mao-sir.com的app中的代碼,這是我的個(gè)人的一個(gè)開(kāi)源中臺(tái)產(chǎn)品;
2.必須要設(shè)置上面代碼截圖中的4個(gè)屬性即:
SimpleItemAnimator里的setSupportsChangeAnimations(false);
DefaultItemAnimator里的setSupportsChangeAnimations(false);
setChangeDuration(0);
setHasFixedSize(true);
3.必須要覆蓋RecyclerView的onScrolled方法,在方法里設(shè)置防止第一行到頂部有空白的操作;
4.必須要在adapter里覆蓋getItemViewType,在方法內(nèi)返回position;
5.必須要在adapter里刷新數(shù)據(jù)時(shí)使用:notifyItemRangeChanged;
錯(cuò)誤的做法
這邊我說(shuō)網(wǎng)上絕大部分示例是錯(cuò)的可能是客氣了點(diǎn),因該說(shuō)可以搜到的全是錯(cuò)的。我們也來(lái)總結(jié)一下,希望各位不要再去踩這種坑了。
1.把圖片的尺寸預(yù)先存在后臺(tái)、每次接口取圖片時(shí)后臺(tái)把這個(gè)尺寸返回給到RecyclerView的adapter。。。這是得有多。。。無(wú)聊的做法,覆蓋一個(gè)getItemViewType不就同樣實(shí)現(xiàn)了這樣的手法?
2.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE)的做法是錯(cuò)的,設(shè)完后圖片要么在一開(kāi)始進(jìn)入界面時(shí)顯示不出要么顯示不全,我不知道這個(gè)問(wèn)題最早是誰(shuí)想出的解決辦法?怎么自己也不去驗(yàn)證一下對(duì)不對(duì)?
3.網(wǎng)上還有一種手法也是錯(cuò)的離譜,就是在onBindViewHolder方法里通過(guò)BitMap+GLIDE重設(shè)圖片的尺寸,這種方法根本無(wú)效;
下面我們來(lái)感受一下使用了網(wǎng)上錯(cuò)誤的手法后,這個(gè)瀑布流會(huì)變成什么樣,大家就能感受到我說(shuō)的這種:體驗(yàn)問(wèn)題了。這種體驗(yàn)問(wèn)題在APP上如果沒(méi)有做好會(huì)直接要了你這家企業(yè)的“口碑的命”。
來(lái)看錯(cuò)誤手法導(dǎo)致的差勁的瀑布流體驗(yàn)
請(qǐng)看下面的“反例”的演示,注意到了上劃或者下劃時(shí)左右列切換、重排的問(wèn)題了沒(méi)?給大家感受一下。
以上就是Android入門(mén)之使用RecyclerView完美實(shí)現(xiàn)瀑布流界面詳解的詳細(xì)內(nèi)容,更多關(guān)于Android RecyclerView瀑布流界面的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Android使用RecyclerView實(shí)現(xiàn)列表數(shù)據(jù)選擇操作
- kotlin android extensions 插件實(shí)現(xiàn)示例詳解
- 一文讀懂Android?Kotlin的數(shù)據(jù)流
- Android使用ViewBinding的詳細(xì)步驟(Kotlin簡(jiǎn)易版)
- Android studio listview實(shí)現(xiàn)列表數(shù)據(jù)顯示 數(shù)據(jù)循環(huán)顯示效果
- Android列表組件ListView使用詳解之動(dòng)態(tài)加載或修改列表數(shù)據(jù)
- Android kotlin RecyclerView遍歷json實(shí)現(xiàn)列表數(shù)據(jù)的案例
相關(guān)文章
總結(jié)安卓(Android)中常用的跳轉(zhuǎn)工具
在大家日常開(kāi)發(fā)的時(shí)候經(jīng)常會(huì)用到各式各樣的跳轉(zhuǎn),如跳轉(zhuǎn)到QQ、微信聊天界面、跳轉(zhuǎn)到聯(lián)系人界面或者跳轉(zhuǎn)到瀏覽器和照相機(jī)等等之類(lèi)的,本文將常用到的一些跳轉(zhuǎn)集合到一起,這樣更方便大家以后使用,有需要的小伙伴們可以參考借鑒。2016-08-08Android實(shí)現(xiàn)探探圖片滑動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)探探圖片滑動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Mac Android Studio 3.0 Terminal 中文亂碼問(wèn)題處理
本文給大家分享的是在更新Android Studio 3.0之后,使用Terminal時(shí),發(fā)現(xiàn) git log 命令查看歷史 log會(huì)亂碼,以及最后的解決方法,推薦給小伙伴們2017-11-11基于Flutter實(shí)現(xiàn)圖片選擇和圖片上傳
Flutter?的圖片選擇插件很多,包括了官方的?image_picker,multi_image_picker(基于2.0出了?multi_image_picker2)等等。本文將利用這些插件實(shí)現(xiàn)圖片選擇和圖片上傳,需要的可以參考一下2022-03-03Android實(shí)現(xiàn)倒計(jì)時(shí)結(jié)束后跳轉(zhuǎn)頁(yè)面功能
最近在工作中遇到一個(gè)需求,需要在倒計(jì)時(shí)一段時(shí)間后進(jìn)行跳轉(zhuǎn)頁(yè)面,通過(guò)查找相關(guān)資料發(fā)現(xiàn)其中涉及的知識(shí)還不少,所以分享出來(lái),下面這篇文章主要給大家介紹了關(guān)于Android實(shí)現(xiàn)倒計(jì)時(shí)結(jié)束后跳轉(zhuǎn)頁(yè)面功能的相關(guān)資料,需要的朋友可以參考下。2017-11-11Android中Textview和圖片同行顯示(文字超出用省略號(hào),圖片自動(dòng)靠右邊)
Android中Textview和圖片同行顯示,文字超出用省略號(hào)顯示,圖片自動(dòng)靠右邊??吹竭@個(gè)問(wèn)題本來(lái)認(rèn)為是一個(gè)很正常的需求,看起來(lái)很簡(jiǎn)單,但是做起來(lái)卻遇到了很蛋疼的問(wèn)題,怎么搞的都不行,堵了很長(zhǎng)時(shí)間,下面說(shuō)一下解決的方案,希望遇到這樣問(wèn)題的朋友可以使用。2016-12-12詳解Android原生json和fastjson的簡(jiǎn)單使用
本文主要介紹了Android原生json和fastjson的簡(jiǎn)單使用,具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01