FlexboxLayout——是时候展现真正的瀑布流了(实现篇)

前言

FlexboxLayout已经出来有一年多的时间了,之所以现在才写这篇文章,主要是因为之前的FlexboxLayoutManager一直不支持findPosition (find(First|Last)(Completely)?VisibleItemPosition)方法。瀑布流之所以叫做瀑布流,就是因为他的无限上拉加载能力,而findPostion方法又是实现上拉加载的重中之重,缺了上拉加载的瀑布流又怎么能算作真正的瀑布流呢?而FlexboxLayout在前不久的0.3.0-alpha4版本中终于加入了findPostion*方法,所以,是时候带大家实现真正的瀑布流了。

两种风格

瀑布流最早起源于Pinterest网站,发展到现在逐渐形成了两种风格。一种是竖版,保持图片的宽度一致而高度参差不齐,Pinterest采用的就是这种风格:

Pinterest

在FlexboxLayout推出之前大多数Android设备上使用的都是这种瀑布流,感兴趣同学可以看看郭霖大神的这篇文章:Android瀑布流照片墙实现,体验不规则排列的美感

而另一种则是Google Image采用的横版风格,图片的高度保持一致,利用宽度的不同造成参差错落的感觉,这也是我们今天将要实现的效果:

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,谢谢支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容