FlexboxLayout——是時候展現真正的瀑布流了(實現篇)

前言

FlexboxLayout已經出來有一年多的時間了,之所以現在才寫這篇文章,主要是因為之前的FlexboxLayoutManager一直不支持findPosition (find(First|Last)(Completely)?VisibleItemPosition)方法。瀑布流之所以叫做瀑布流,就是因為他的無限上拉加載能力,而findPostion方法又是實現上拉加載的重中之重,缺了上拉加載的瀑布流又怎么能算作真正的瀑布流呢?而FlexboxLayout在前不久的0.3.0-alpha4版本中終于加入了findPostion*方法,所以,是時候帶大家實現真正的瀑布流了。

兩種風格

瀑布流最早起源于Pinterest網站,發(fā)展到現在逐漸形成了兩種風格。一種是豎版,保持圖片的寬度一致而高度參差不齊,Pinterest采用的就是這種風格:

Pinterest

在FlexboxLayout推出之前大多數Android設備上使用的都是這種瀑布流,感興趣同學可以看看郭霖大神的這篇文章:Android瀑布流照片墻實現,體驗不規(guī)則排列的美感

而另一種則是Google Image采用的橫版風格,圖片的高度保持一致,利用寬度的不同造成參差錯落的感覺,這也是我們今天將要實現的效果:

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)化過后的效果,這回可沒有進行任何剪輯:

優(yōu)化后

再看一下Android Device Moniter的數據:

優(yōu)化后的數據

15張圖片分為15個線程加載,最慢圖片也只消耗了2s,最終整個AsyncTask也只有6s多的時間,優(yōu)化的時間還是非??捎^的。

結語

到這里我們的文章就要告一段落了,這次我們不僅使用FlexboxLayout實現了瀑布流,同時也對圖片的加載進行優(yōu)化。其實可以做的優(yōu)化還有很多,比如使用LruCache、DiskLruCache實現內存緩存和磁盤緩存,也可以加入一些更炫酷的上拉加載效果,這里就不多做介紹了。這個瀑布流的源碼也可以在我的開源項目GavinLi369/Translator里找到,當然,如果喜歡這個項目別忘了點個star,謝謝支持。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容