前言
FlexboxLayout已經出來有一年多的時間了,之所以現在才寫這篇文章,主要是因為之前的FlexboxLayoutManager一直不支持findPosition (find(First|Last)(Completely)?VisibleItemPosition)方法。瀑布流之所以叫做瀑布流,就是因為他的無限上拉加載能力,而findPostion方法又是實現上拉加載的重中之重,缺了上拉加載的瀑布流又怎么能算作真正的瀑布流呢?而FlexboxLayout在前不久的0.3.0-alpha4版本中終于加入了findPostion*方法,所以,是時候帶大家實現真正的瀑布流了。
兩種風格
瀑布流最早起源于Pinterest網站,發(fā)展到現在逐漸形成了兩種風格。一種是豎版,保持圖片的寬度一致而高度參差不齊,Pinterest采用的就是這種風格:

在FlexboxLayout推出之前大多數Android設備上使用的都是這種瀑布流,感興趣同學可以看看郭霖大神的這篇文章:Android瀑布流照片墻實現,體驗不規(guī)則排列的美感
而另一種則是Google Image采用的橫版風格,圖片的高度保持一致,利用寬度的不同造成參差錯落的感覺,這也是我們今天將要實現的效果:

FlexboxLayout簡介
FlexboxLayout是Google在一年多以前開源的一款在Android平臺上支持CSS Flexible Box Layout Module的項目,對前端有所了解的同學一定不會對這款布局陌生。而在Google推出這款布局之后,人們發(fā)現這款布局可以很方便的實現對RecyclerView的支持,于是就有了FlexboxLayoutManager,也就給了我們只需要寥寥幾行代碼就實現瀑布流的機會。
圖片資源獲取
要想實現瀑布流,首先需要的當然是源源不斷的圖片資源,這里我選擇采用Pexels網站的資源,由于實現的過程跟今天的主題關系不大,就不詳細介紹了,下面是實現代碼:
public class PexelsImageUtil {
private static final String SEARCH_URL = "https://www.pexels.com/search/";
private String mKey;
private int mPage;
public PexelsImageUtil(String key) {
mKey = key;
mPage = 1;
}
/**
* @return 15個圖片鏈接
*/
public List<String> getImageLinks() throws IOException {
if(Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("不能在主線程使用網絡");
}
URL url = new URL(SEARCH_URL + mKey + "?page=" + mPage++);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if(connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("網絡連接錯誤");
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
StringBuilder html = new StringBuilder();
String temp;
while((temp = bufferedReader.readLine()) != null) {
html.append(temp).append("\r\n");
}
bufferedReader.close();
connection.disconnect();
return findImageLinksFromHtml(html.toString());
}
private List<String> findImageLinksFromHtml(String html) {
List<String> links = new ArrayList<>();
Pattern pattern = Pattern.compile("src=\"(http.+?)\"");
Matcher matcher = pattern.matcher(html);
while(matcher.find()) {
links.add(matcher.group(1));
}
return links;
}
}
注意不要忘了添加網絡權限:
<uses-permission android:name="android.permission.INTERNET"/>
圖片顯示
有了可以顯示的圖片資源就可以開始實現我們的瀑布流了,首先我們需要在Activity中初始化我們的RecyclerView及FlexboxLayoutManager:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private FlexboxLayoutManager mLayoutManager;
private ImageAdapter mAdapter;
private PexelsImageUtil mPexelsImageUtil;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.rv_images);
mLayoutManager = new FlexboxLayoutManager(this);
//設置主軸為水平方向,從左到右
mLayoutManager.setFlexDirection(FlexDirection.ROW);
//換行
mLayoutManager.setFlexWrap(FlexWrap.WRAP);
//設置副軸對齊方式
mLayoutManager.setAlignItems(AlignItems.STRETCH);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new ImageAdapter();
mRecyclerView.setAdapter(mAdapter);
mAdapter.showLoadingFooter();
mPexelsImageUtil = new PexelsImageUtil("girl");
new LoadImageTask(this, mAdapter).execute(mPexelsImageUtil);
}
static class LoadImageTask extends AsyncTask<PexelsImageUtil, Void, List<Bitmap>> {
private WeakReference<ImageAdapter> mAdapterWeakReference;
private WeakReference<MainActivity> mActivityWeakReference;
public LoadImageTask(MainActivity activity, ImageAdapter adapter) {
mActivityWeakReference = new WeakReference<>(activity);
mAdapterWeakReference = new WeakReference<>(adapter);
}
@Override
protected List<Bitmap> doInBackground(PexelsImageUtil... pexelsImageUtils) {
List<Bitmap> images = new ArrayList<>();
List<String> imageLinks = null;
try {
imageLinks = pexelsImageUtils[0].getImageLinks();
} catch (IOException e) {
e.printStackTrace();
}
if(imageLinks != null && imageLinks.size() != 0) {
for (int i = 0; i < imageLinks.size(); i++) {
String link = imageLinks.get(i);
try {
images.add(getImage(link));
} catch (IOException e) {
e.printStackTrace();
}
}
}
return images;
}
@Override
protected void onPostExecute(List<Bitmap> bitmaps) {
int positionStart = mAdapterWeakReference.get().getItemCount();
mAdapterWeakReference.get().addImages(bitmaps);
mAdapterWeakReference.get().notifyItemRangeInserted(positionStart,
bitmaps.size());
}
private Bitmap getImage(String urlStr) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if(connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("網絡連接錯誤");
}
try (InputStream in = connection.getInputStream()) {
return BitmapFactory.decodeStream(in);
} finally {
connection.disconnect();
}
}
}
}
我們在onCreate方法里初始化了FlexboxLayoutManager,并對各項屬性進行了設置。當然,FlexboxLayoutManager支持的屬性遠不止這些,這里由于篇幅所限就不多做介紹了,感興趣的同學可以看一下FlexboxLayout項目的README文件,里面對FlexboxLayout的各項屬性都有很詳細的說明。
接下來我們需要完成RecyclerView的Adapter類:
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ImageViewHolder> {
private List<Bitmap> mImages = new ArrayList<>();
@Override
public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new Holder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_image, parent, false));
}
@Override
public void onBindViewHolder(ImageViewHolder holder, int position) {
holder.mImageView.setImageBitmap(mImages.get(position));
ViewGroup.LayoutParams params =holder.mImageView.getLayoutParams();
if(params instanceof FlexboxLayoutManager.LayoutParams) {
FlexboxLayoutManager.LayoutParams flexBoxParams = (FlexboxLayoutManager.LayoutParams) params;
flexBoxParams.setFlexGrow(1.0f);
}
}
@Override
public int getItemCount() {
return mImages.size();
}
public void addImages(List<Bitmap> images) {
mImages.addAll(images);
}
class ImageViewHolder extends RecyclerView.ViewHolder {
private ImageView mImageView;
public Holder(View itemView) {
super(itemView);
mImageView = (ImageView) itemView.findViewById(R.id.img_content);
}
}
}
到這里就已經有了瀑布流的大概樣子了:

上拉加載
實現了圖片的顯示,接下來就要面對瀑布流的另一大特性——上拉加載了,我們需要對Adapter類加以改造,加入底部的加載視圖:
public class ImageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Bitmap> mImages = new ArrayList<>();
private boolean hasFooter = false;
private static final int TYPE_FOOTER = -1;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == TYPE_FOOTER) {
return new LoadingFooterHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_loading, parent, false));
} else {
return new ImageViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_image, parent, false));
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if(!hasFooter || position != mImages.size() &&
viewHolder instanceof ImageViewHolder) {
ImageViewHolder imageViewHolder = (ImageViewHolder) viewHolder;
imageViewHolder.mImageView.setImageBitmap(mImages.get(position));
ViewGroup.LayoutParams params = imageViewHolder.mImageView.getLayoutParams();
if (params instanceof FlexboxLayoutManager.LayoutParams) {
FlexboxLayoutManager.LayoutParams flexBoxParams = (FlexboxLayoutManager.LayoutParams) params;
flexBoxParams.setFlexGrow(1.0f);
}
}
}
@Override
public int getItemViewType(int position) {
if(hasFooter && position == mImages.size()) {
return TYPE_FOOTER;
} else {
return super.getItemViewType(position);
}
}
@Override
public int getItemCount() {
return hasFooter ? mImages.size() + 1 : mImages.size();
}
public void addImages(List<Bitmap> images) {
mImages.addAll(images);
}
public void showLoadingFooter() {
hasFooter = true;
notifyItemInserted(mImages.size());
}
public void removeLoadingFooter() {
hasFooter = false;
notifyItemRemoved(mImages.size());
}
class ImageViewHolder extends RecyclerView.ViewHolder {
private ImageView mImageView;
public ImageViewHolder(View itemView) {
super(itemView);
mImageView = (ImageView) itemView.findViewById(R.id.img_content);
}
}
class LoadingFooterHolder extends RecyclerView.ViewHolder {
public LoadingFooterHolder(View itemView) {
super(itemView);
}
}
}
這里使用了ItemViewType,在RecyclerView底部加入一個ViewType為TYPE_FOOTER的加載視圖。
之后我們在MainActivity里加入上拉加載的判斷,這時候就要用到我們文章開始提到的findPostion*方法了:
public class MainActivity extends AppCompatActivity {
...
private boolean mIsLoading = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mRecyclerView.addOnScrollListener(new ScrollLoadingListener());
}
class ScrollLoadingListener extends RecyclerView.OnScrollListener {
private int mLastVisibleItem;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(!mIsLoading && newState == RecyclerView.SCROLL_STATE_IDLE &&
mLastVisibleItem + 1 == mAdapter.getItemCount()) {
mIsLoading = true;
mAdapter.showLoadingFooter();
new LoadImageTask(MainActivity.this, mAdapter).execute(mPexelsImageUtil);
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mLastVisibleItem = mLayoutManager.findLastCompletelyVisibleItemPosition();
}
}
...
}
這里我們使用findLastCompletelyVisibleItemPosition方法,當判定最后一張圖片顯示完全的時候加入上拉加載視圖,同時啟動LoadImageTask進行圖片加載。
至此一個完整的瀑布流就已經實現了:

圖片加載優(yōu)化
細心的同學可能會發(fā)現其實上面的效果圖是經過剪輯的,實際使用的加載時間遠不止此。我們必須對圖片的加載進行優(yōu)化,首先用Android Device Moniter對圖片的加載過程進行查看:

可以看到,AsyncTask的耗時長達18s之多,觀察上面LoadIamgeTask的代碼發(fā)現,15張圖片是按順序依次進行網絡加載的。很容易就能想到,如果數張圖片并行加載應該可以節(jié)省很多的時間。
static class LoadImageTask extends AsyncTask<PexelsImageUtil, Void, List<Bitmap>> {
...
private final ThreadPoolExecutor mExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
private AtomicInteger mOffset = new AtomicInteger(0);
@Override
protected List<Bitmap> doInBackground(PexelsImageUtil... pexelsImageUtils) {
List<Bitmap> images = new ArrayList<>();
List<String> imageLinks;
try {
imageLinks = pexelsImageUtils[0].getImageLinks();
final CountDownLatch latch = new CountDownLatch(imageLinks.size());
for(int i = 0; i < imageLinks.size(); i++) {
mExecutor.execute(() -> {
String link = imageLinks.get(mOffset.getAndIncrement());
try {
Bitmap image = getImage(link);
synchronized (images) {
images.add(image);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return images;
}
...
}
我們使用為每張圖片的加載都新開一個線程,同時使用線程池對這些線程進行管理。
這是優(yōu)化過后的效果,這回可沒有進行任何剪輯:

再看一下Android Device Moniter的數據:

15張圖片分為15個線程加載,最慢圖片也只消耗了2s,最終整個AsyncTask也只有6s多的時間,優(yōu)化的時間還是非??捎^的。
結語
到這里我們的文章就要告一段落了,這次我們不僅使用FlexboxLayout實現了瀑布流,同時也對圖片的加載進行優(yōu)化。其實可以做的優(yōu)化還有很多,比如使用LruCache、DiskLruCache實現內存緩存和磁盤緩存,也可以加入一些更炫酷的上拉加載效果,這里就不多做介紹了。這個瀑布流的源碼也可以在我的開源項目GavinLi369/Translator里找到,當然,如果喜歡這個項目別忘了點個star,謝謝支持。