前言
FlexboxLayout已经出来有一年多的时间了,之所以现在才写这篇文章,主要是因为之前的FlexboxLayoutManager一直不支持findPosition (find(First|Last)(Completely)?VisibleItemPosition)方法。瀑布流之所以叫做瀑布流,就是因为他的无限上拉加载能力,而findPostion方法又是实现上拉加载的重中之重,缺了上拉加载的瀑布流又怎么能算作真正的瀑布流呢?而FlexboxLayout在前不久的0.3.0-alpha4版本中终于加入了findPostion*方法,所以,是时候带大家实现真正的瀑布流了。
两种风格
瀑布流最早起源于Pinterest网站,发展到现在逐渐形成了两种风格。一种是竖版,保持图片的宽度一致而高度参差不齐,Pinterest采用的就是这种风格:
在FlexboxLayout推出之前大多数Android设备上使用的都是这种瀑布流,感兴趣同学可以看看郭霖大神的这篇文章:Android瀑布流照片墙实现,体验不规则排列的美感
而另一种则是Google Image采用的横版风格,图片的高度保持一致,利用宽度的不同造成参差错落的感觉,这也是我们今天将要实现的效果:
FlexboxLayout简介
FlexboxLayout是Google在一年多以前开源的一款在Android平台上支持CSS Flexible Box Layout Module的项目,对前端有所了解的同学一定不会对这款布局陌生。而在Google推出这款布局之后,人们发现这款布局可以很方便的实现对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进行图片加载。
至此一个完整的瀑布流就已经实现了:
图片加载优化
细心的同学可能会发现其实上面的效果图是经过剪辑的,实际使用的加载时间远不止此。我们必须对图片的加载进行优化,首先用Android Device Moniter对图片的加载过程进行查看:
可以看到,AsyncTask的耗时长达18s之多,观察上面LoadIamgeTask的代码发现,15张图片是按顺序依次进行网络加载的。很容易就能想到,如果数张图片并行加载应该可以节省很多的时间。
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;
}
...
}
我们使用为每张图片的加载都新开一个线程,同时使用线程池对这些线程进行管理。
这是优化过后的效果,这回可没有进行任何剪辑:
再看一下Android Device Moniter的数据:
15张图片分为15个线程加载,最慢图片也只消耗了2s,最终整个AsyncTask也只有6s多的时间,优化的时间还是非常可观的。
结语
到这里我们的文章就要告一段落了,这次我们不仅使用FlexboxLayout实现了瀑布流,同时也对图片的加载进行优化。其实可以做的优化还有很多,比如使用LruCache、DiskLruCache实现内存缓存和磁盘缓存,也可以加入一些更炫酷的上拉加载效果,这里就不多做介绍了。这个瀑布流的源码也可以在我的开源项目GavinLi369/Translator里找到,当然,如果喜欢这个项目别忘了点个star,谢谢支持。