所谓瀑布流效果,简单说就是宽度相同但是高度不同的一大堆图片,分成几列,然后像水流一样向下排列,并随着用户的上下滑动自动加载更多的图片内容。
语言描述比较抽象,具体效果看下面的截图:

其实这个效果在web上应用的还蛮多的,在android上也有一些应用有用到。因为看起来参差不齐,所以比较有新鲜感,不像传统的九宫格那样千篇一律。
网络上相关的文章也有几篇,但是整理后发现要么忽略了OOM的处理,要么代码的逻辑相对来说有一点混乱,滑动效果也有一点卡顿。
所以后来自己干脆换了一下思路,重新实现了这样一个瀑布流效果。目前做的测试不多,但是加载几千张图片还没有出现过OOM的情况,滑动也比较流畅。下面大体讲解一下实现思路。
要想比较好的实现这个效果主要有两个重点:
一是在用户滑动到底部的时候加载下一组图片内容的处理。
二是当加载图片比较多的情况下,对图片进行回收,防止OOM的处理。
对于第一点,主要是加载时机的判断以及加载内容的异步处理。这一部分其实理解起来还是比较容易,具体可以参见下面给出的源码。
对于第二点,在进行回收的时候,我们的整体思路是以用户当前看到的这一个屏幕为基准,向上两屏以及向下两屏一共有5屏的内容,超出这5屏范围的bitmap将被回收。
在向上滚动的时候,将回收超过下方两屏范围的bitmap,并重载进入上方两屏的bitmap。
在向下滚动的时候,将回收超过上方两屏范围的bitmap,并重载进入下方两屏的bitmap。
具体的实现思路还是参见源码,我有给出比较详细的注释。

先来看一下项目的结构:

WaterFall.java
package com.carrey.waterfall.waterfall;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Random;

import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* 瀑布流
* 某些参数做了固定设置,如果想扩展功能,可自行修改
* @author carrey
*
*/
public class WaterFall extends ScrollView {

    /** 延迟发送message的handler */private DelayHandler delayHandler;/** 添加单元到瀑布流中的Handler */private AddItemHandler addItemHandler;/** ScrollView直接包裹的LinearLayout */private LinearLayout containerLayout;/** 存放所有的列Layout */private ArrayList<LinearLayout> colLayoutArray;/** 当前所处的页面(已经加载了几次) */private int currentPage;/** 存储每一列中向上方向的未被回收bitmap的单元的最小行号 */private int[] currentTopLineIndex;/** 存储每一列中向下方向的未被回收bitmap的单元的最大行号 */private int[] currentBomLineIndex;/** 存储每一列中已经加载的最下方的单元的行号 */private int[] bomLineIndex;/** 存储每一列的高度 */private int[] colHeight;/** 所有的图片资源路径 */private String[] imageFilePaths;/** 瀑布流显示的列数 */private int colCount;/** 瀑布流每一次加载的单元数量 */private int pageCount;/** 瀑布流容纳量 */private int capacity;private Random random;/** 列的宽度 */private int colWidth;private boolean isFirstPage;public WaterFall(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init();}public WaterFall(Context context, AttributeSet attrs) {super(context, attrs);init();}public WaterFall(Context context) {super(context);init();}/** 基本初始化工作 */private void init() {delayHandler = new DelayHandler(this);addItemHandler = new AddItemHandler(this);colCount = 4;//默认情况下是4列pageCount = 30;//默认每次加载30个瀑布流单元capacity = 10000;//默认容纳10000张图random = new Random();colWidth = getResources().getDisplayMetrics().widthPixels / colCount;colHeight = new int[colCount];currentTopLineIndex = new int[colCount];currentBomLineIndex = new int[colCount];bomLineIndex = new int[colCount];colLayoutArray = new ArrayList<LinearLayout>();}/*** 在外部调用 第一次装载页面 必须调用*/public void setup() {containerLayout = new LinearLayout(getContext());containerLayout.setBackgroundColor(Color.WHITE);LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);addView(containerLayout, layoutParams);for (int i = 0; i < colCount; i++) {LinearLayout colLayout = new LinearLayout(getContext());LinearLayout.LayoutParams colLayoutParams = new LinearLayout.LayoutParams(colWidth, LinearLayout.LayoutParams.WRAP_CONTENT);colLayout.setPadding(2, 2, 2, 2);colLayout.setOrientation(LinearLayout.VERTICAL);containerLayout.addView(colLayout, colLayoutParams);colLayoutArray.add(colLayout);}try {imageFilePaths = getContext().getAssets().list("images");} catch (IOException e) {e.printStackTrace();}//添加第一页addNextPageContent(true);}@Overridepublic boolean onTouchEvent(MotionEvent ev) {switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_UP://手指离开屏幕的时候向DelayHandler延时发送一个信息,然后DelayHandler//届时来判断当前的滑动位置,进行不同的处理。delayHandler.sendMessageDelayed(delayHandler.obtainMessage(), 200);break;}return super.onTouchEvent(ev);}@Overrideprotected void onScrollChanged(int l, int t, int oldl, int oldt) {//在滚动过程中,回收滚动了很远的bitmap,防止OOM/*---回收算法说明:* 回收的整体思路是:* 我们只保持当前手机显示的这一屏以及上方两屏和下方两屏 一共5屏内容的Bitmap,* 超出这个范围的单元Bitmap都被回收。* 这其中又包括了一种情况就是之前回收过的单元的重新加载。* 详细的讲解:* 向下滚动的时候:回收超过上方两屏的单元Bitmap,重载进入下方两屏以内Bitmap* 向上滚动的时候:回收超过下方两屏的单元bitmao,重载进入上方两屏以内bitmap* ---*/int viewHeight = getHeight();if (t > oldt) {//向下滚动if (t > 2 * viewHeight) {for (int i = 0; i < colCount; i++) {LinearLayout colLayout = colLayoutArray.get(i);//回收上方超过两屏bitmapFlowingView topItem = (FlowingView) colLayout.getChildAt(currentTopLineIndex[i]);if (topItem.getFootHeight() < t - 2 * viewHeight) {topItem.recycle();currentTopLineIndex[i] ++;}//重载下方进入(+1)两屏以内bitmapFlowingView bomItem = (FlowingView) colLayout.getChildAt(Math.min(currentBomLineIndex[i] + 1, bomLineIndex[i]));if (bomItem.getFootHeight() <= t + 3 * viewHeight) {bomItem.reload();currentBomLineIndex[i] = Math.min(currentBomLineIndex[i] + 1, bomLineIndex[i]);}}}} else {//向上滚动for (int i = 0; i < colCount; i++) {LinearLayout colLayout = colLayoutArray.get(i);//回收下方超过两屏bitmapFlowingView bomItem = (FlowingView) colLayout.getChildAt(currentBomLineIndex[i]);if (bomItem.getFootHeight() > t + 3 * viewHeight) {bomItem.recycle();currentBomLineIndex[i] --;}//重载上方进入(-1)两屏以内bitmapFlowingView topItem = (FlowingView) colLayout.getChildAt(Math.max(currentTopLineIndex[i] - 1, 0));if (topItem.getFootHeight() >= t - 2 * viewHeight) {topItem.reload();currentTopLineIndex[i] = Math.max(currentTopLineIndex[i] - 1, 0);}}}super.onScrollChanged(l, t, oldl, oldt);}/*** 这里之所以要用一个Handler,是为了使用他的延迟发送message的函数* 延迟的效果在于,如果用户快速滑动,手指很早离开屏幕,然后滑动到了底部的时候,* 因为信息稍后发送,在手指离开屏幕到滑动到底部的这个时间差内,依然能够加载图片* @author carrey**/private static class DelayHandler extends Handler {private WeakReference<WaterFall> waterFallWR;private WaterFall waterFall;public DelayHandler(WaterFall waterFall) {waterFallWR = new WeakReference<WaterFall>(waterFall);this.waterFall = waterFallWR.get();}@Overridepublic void handleMessage(Message msg) {//判断当前滑动到的位置,进行不同的处理if (waterFall.getScrollY() + waterFall.getHeight() >= waterFall.getMaxColHeight() - 20) {//滑动到底部,添加下一页内容waterFall.addNextPageContent(false);} else if (waterFall.getScrollY() == 0) {//滑动到了顶部} else {//滑动在中间位置}super.handleMessage(msg);}}/*** 添加单元到瀑布流中的Handler* @author carrey**/private static class AddItemHandler extends Handler {private WeakReference<WaterFall> waterFallWR;private WaterFall waterFall;public AddItemHandler(WaterFall waterFall) {waterFallWR = new WeakReference<WaterFall>(waterFall);this.waterFall = waterFallWR.get();}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case 0x00:FlowingView flowingView = (FlowingView)msg.obj;waterFall.addItem(flowingView);break;}super.handleMessage(msg);}}/*** 添加单元到瀑布流中* @param flowingView*/private void addItem(FlowingView flowingView) {int minHeightCol = getMinHeightColIndex();colLayoutArray.get(minHeightCol).addView(flowingView);colHeight[minHeightCol] += flowingView.getViewHeight();flowingView.setFootHeight(colHeight[minHeightCol]);if (!isFirstPage) {bomLineIndex[minHeightCol] ++;currentBomLineIndex[minHeightCol] ++;}}/*** 添加下一个页面的内容*/private void addNextPageContent(boolean isFirstPage) {this.isFirstPage = isFirstPage;//添加下一个页面的pageCount个单元内容for (int i = pageCount * currentPage; i < pageCount * (currentPage + 1) && i < capacity; i++) {new Thread(new PrepareFlowingViewRunnable(i)).run();}currentPage ++;}/*** 异步加载要添加的FlowingView* @author carrey**/private class PrepareFlowingViewRunnable implements Runnable {private int id;public PrepareFlowingViewRunnable (int id) {this.id = id;}@Overridepublic void run() {FlowingView flowingView = new FlowingView(getContext(), id, colWidth);String imageFilePath = "images/" + imageFilePaths[random.nextInt(imageFilePaths.length)];flowingView.setImageFilePath(imageFilePath);flowingView.loadImage();addItemHandler.sendMessage(addItemHandler.obtainMessage(0x00, flowingView));}}/*** 获得所有列中的最大高度* @return*/private int getMaxColHeight() {int maxHeight = colHeight[0];for (int i = 1; i < colHeight.length; i++) {if (colHeight[i] > maxHeight)maxHeight = colHeight[i];}return maxHeight;}/*** 获得目前高度最小的列的索引* @return*/private int getMinHeightColIndex() {int index = 0;for (int i = 1; i < colHeight.length; i++) {if (colHeight[i] < colHeight[index])index = i;}return index;}

}
复制代码
FlowingView.java
package com.carrey.waterfall.waterfall;

import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import android.widget.Toast;
/**
* 瀑布流中流动的单元
* @author carrey
*
*/
public class FlowingView extends View implements View.OnClickListener, View.OnLongClickListener {

    /** 单元的编号,在整个瀑布流中是唯一的,可以用来标识身份 */private int index;/** 单元中要显示的图片Bitmap */private Bitmap imageBmp;/** 图像文件的路径 */private String imageFilePath;/** 单元的宽度,也是图像的宽度 */private int width;/** 单元的高度,也是图像的高度 */private int height;/** 画笔 */private Paint paint;/** 图像绘制区域 */private Rect rect;/** 这个单元的底部到它所在列的顶部之间的距离 */private int footHeight;public FlowingView(Context context, int index, int width) {super(context);this.index = index;this.width = width;init();}/*** 基本初始化工作*/private void init() {setOnClickListener(this);setOnLongClickListener(this);paint = new Paint();paint.setAntiAlias(true);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(width, height);}@Overrideprotected void onDraw(Canvas canvas) {//绘制图像canvas.drawColor(Color.WHITE);if (imageBmp != null && rect != null) {canvas.drawBitmap(imageBmp, null, rect, paint);}super.onDraw(canvas);}/*** 被WaterFall调用异步加载图片数据*/public void loadImage() {InputStream inStream = null;try {inStream = getContext().getAssets().open(imageFilePath);imageBmp = BitmapFactory.decodeStream(inStream);inStream.close();inStream = null;} catch (IOException e) {e.printStackTrace();}if (imageBmp != null) {int bmpWidth = imageBmp.getWidth();int bmpHeight = imageBmp.getHeight();height = (int) (bmpHeight * width / bmpWidth);rect = new Rect(0, 0, width, height);}}/*** 重新加载回收了的Bitmap*/public void reload() {if (imageBmp == null) {new Thread(new Runnable() {@Overridepublic void run() {InputStream inStream = null;try {inStream = getContext().getAssets().open(imageFilePath);imageBmp = BitmapFactory.decodeStream(inStream);inStream.close();inStream = null;postInvalidate();} catch (IOException e) {e.printStackTrace();}}}).start();}}/*** 防止OOM进行回收*/public void recycle() {if (imageBmp == null || imageBmp.isRecycled()) return;new Thread(new Runnable() {@Overridepublic void run() {imageBmp.recycle();imageBmp = null;postInvalidate();}}).start();}@Overridepublic boolean onLongClick(View v) {Toast.makeText(getContext(), "long click : " + index, Toast.LENGTH_SHORT).show();return true;}@Overridepublic void onClick(View v) {Toast.makeText(getContext(), "click : " + index, Toast.LENGTH_SHORT).show();}/*** 获取单元的高度* @return*/public int getViewHeight() {return height;}/*** 设置图片路径* @param imageFilePath*/public void setImageFilePath(String imageFilePath) {this.imageFilePath = imageFilePath;}public Bitmap getImageBmp() {return imageBmp;}public void setImageBmp(Bitmap imageBmp) {this.imageBmp = imageBmp;}public int getFootHeight() {return footHeight;}public void setFootHeight(int footHeight) {this.footHeight = footHeight;}

}
复制代码
MainActivity.java
package com.carrey.waterfall;

import com.carrey.waterfall.waterfall.WaterFall;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);WaterFall waterFall = (WaterFall) findViewById(R.id.waterfall);waterFall.setup();}

}
复制代码
activity_main.xml

android 瀑布流 的实现相关推荐

  1. android 瀑布流 github,GitHub - youxilua/waterfall4android: android瀑布流

    waterfall4android android瀑布流 #android 瀑布流的实现详解,附源码# ##介绍## 原作者表示: 试过在1万张可以流畅的滑动,不出现内存溢出情况 ##设计思路## 之 ...

  2. android 瀑布流 github,GitHub - coong4/android_waterfall: Android版的瀑布流布局

    Android瀑布流实例 此项目由于最初设计问题,导致现在问题比较多,暂时停止维护. 我现在在其他类似的瀑布流上进行完善开发, ####请关注: 有必要解释一下程序为什么采用addview方式而不是做 ...

  3. Android瀑布流照片墙、滑动切换图片

    继续上一篇博客中提到的反编译"马蜂窝自由行"app. 今天看到下方这段效果不错,决定实现出来. 从Gif中我们看出这个其实就是一个照片墙加上一个图片滑动.在查看图片时还有个放大缩小 ...

  4. android 瀑布流

    最终效果图 1.item_demo5.xml <?xml version="1.0" encoding="utf-8"?> <LinearLa ...

  5. android瀑布流效果(仿蘑菇街)

    Android 转载分享(10)  我们还是来看一款示例:(蘑菇街)           看起来很像我们的gridview吧,不过又不像,因为item大小不固定的,看起来是不是别有一番风味,确实如此. ...

  6. android瀑布流列表两边跳,Android Fragment + RecyclerView瀑布流布局

    Android Fragment + RecyclerView瀑布流布局 Android Fragment + RecyclerView瀑布流布局 目录 1.瀑布流效果图 2.使用RecyclerVi ...

  7. Android瀑布流仿京东,(二) 仿京东顶部伸缩渐变丶自定义viewpager指示器丶viewpager3D回廊丶recyclerview瀑布流...

    效果图如下: demo2.gif Demo2 1.仿京东首页顶部轮播图+搜索栏渐变 public class GradientScrollView extends ScrollView { publi ...

  8. android 瀑布流 空白,Android瀑布流优化,解决Recyclerview展示大批量图片时Item自动切换、闪烁、空白等问题...

    本文涉及的代码案例可以在下方的链接中找到,如果对你有帮助,请给个Star(#^.^#) 问题分析 这段时间业务需求用到RecyclerView瀑布流加载并展示大批量图片,但一开始单纯使用Recycle ...

  9. android 瀑布流的实现(用recyclerview的实现的)

    先看下效果图 代码的整体布局: 首先要做的就是导入v7包,这个v7的位置就在自己的sdk目录下,具体位置 我的博客说过了,这里就不再说了, 下面就是布局代码 main_activity <Rel ...

最新文章

  1. T-SQL学习中--内联接,外连接,交叉连接
  2. 如何在 Windows Server 2003 中创建漫游用户配置文件
  3. QT的QGraphicsAnchorLayout类的使用
  4. 浅析商业银行“业务连续性管理体系”的构建
  5. Animation Property Animation 使用
  6. 终端执行php,PHP命令行执行PHP脚本的注意事项总结
  7. 3种类型的程序员:“虫族”,“人族”,“神族”(转载)
  8. C#LeetCode刷题-广度优先搜索
  9. STM32学习1之ADC+DMA(使用定时器触发)
  10. windows下python环境搭建_Win7怎么搭建Python环境 win7系统搭建Python环境的方法
  11. Hello,移动WEB—px,dp,dpr像素基础
  12. 在手语世界里,健听人、数字人与听障人的交织
  13. 清除SQLServer日志
  14. git 分支管理策略(7)
  15. Windows10更新后,如何删除多出来的OEM分区?
  16. 请问投稿中要求上传的author_投稿要求
  17. 如何使用数据包破解游戏 - 从这里开始
  18. 少儿编程是智商税吗?不花钱让孩子赢在起跑线
  19. 什么是数学建模?如何在数学建模中拿奖?通过建模学到了啥?
  20. GaussDB(for MySQL)近数据处理(NDP)解锁查询新姿势

热门文章

  1. PA1--实现基础设施、表达式求值和监视点
  2. 案例19:Java私房菜定制上门服务系统设计与实现开题报告
  3. Quantum Espresso Hands-On实战训练(五)——电荷密度可视化
  4. Kaldi-Timit 训练
  5. html 3D球状旋转标签云文字云效果
  6. 【shaderforge学习笔记】 Rotator节点
  7. 毕业工作五年的总结和感悟(上)
  8. 【基于时间特征交互和引导细化的遥感变化检测 】2022TGRS
  9. oracle数据误删怎么恢复,Oracle数据误删了怎么恢复
  10. 并发——锁升级(偏向锁,轻量级锁,重量级锁,及常见锁)