美文网首页Android知识Android开发经验谈Android开发
FlexboxLayout——是时候展现真正的瀑布流了(实现篇)

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

作者: GavinLi369 | 来源:发表于2017-07-13 08:14 被阅读4250次

    前言

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

    相关文章

      网友评论

        本文标题:FlexboxLayout——是时候展现真正的瀑布流了(实现篇)

        本文链接:https://www.haomeiwen.com/subject/nnkyhxtx.html