所谓瀑布流效果,简单说就是宽度相同但是高度不同的一大堆图片,分成几列,然后像水流一样向下排列,并随着用户的上下滑动自动加载更多的图片内容。

    语言描述比较抽象,具体效果看下面的截图:

       

    其实这个效果在web上应用的还蛮多的,在android上也有一些应用有用到。因为看起来参差不齐,所以比较有新鲜感,不像传统的九宫格那样千篇一律。

    网络上相关的文章也有几篇,但是整理后发现要么忽略了OOM的处理,要么代码的逻辑相对来说有一点混乱,滑动效果也有一点卡顿。

    所以后来自己干脆换了一下思路,重新实现了这样一个瀑布流效果。目前做的测试不多,但是加载几千张图片还没有出现过OOM的情况,滑动也比较流畅。

    本文原创,如需转载,请注明转载地址:http://blog.csdn.net/carrey1989/article/details/10950673

    下面大体讲解一下实现思路。

    要想比较好的实现这个效果主要有两个重点:

    一是在用户滑动到底部的时候加载下一组图片内容的处理。

    二是当加载图片比较多的情况下,对图片进行回收,防止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);
} @Override
public 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);
} @Override
protected 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);
//回收上方超过两屏bitmap
FlowingView topItem = (FlowingView) colLayout.getChildAt(currentTopLineIndex[i]);
if (topItem.getFootHeight() < t - 2 * viewHeight) {
topItem.recycle();
currentTopLineIndex[i] ++;
}
//重载下方进入(+1)两屏以内bitmap
FlowingView 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);
//回收下方超过两屏bitmap
FlowingView bomItem = (FlowingView) colLayout.getChildAt(currentBomLineIndex[i]);
if (bomItem.getFootHeight() > t + 3 * viewHeight) {
bomItem.recycle();
currentBomLineIndex[i] --;
}
//重载上方进入(-1)两屏以内bitmap
FlowingView 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();
} @Override
public 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();
}
@Override
public 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;
} @Override
public 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);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(width, height);
} @Override
protected 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() { @Override
public 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() { @Override
public void run() {
imageBmp.recycle();
imageBmp = null;
postInvalidate();
}
}).start();
} @Override
public boolean onLongClick(View v) {
Toast.makeText(getContext(), "long click : " + index, Toast.LENGTH_SHORT).show();
return true;
} @Override
public 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 { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); WaterFall waterFall = (WaterFall) findViewById(R.id.waterfall);
waterFall.setup();
} }

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" > <com.carrey.waterfall.waterfall.WaterFall
android:id="@+id/waterfall"
android:layout_width="match_parent"
android:layout_height="match_parent"/> </RelativeLayout>

源码下载

Android UI 之WaterFall瀑布流效果的更多相关文章

  1. [Android Pro] RecyclerView实现瀑布流效果(二)

    referece to : http://blog.csdn.net/u010687392 在上篇中我们知道RecyclerView中默认给我们提供了三种布局管理器,分别是LinearLayoutMa ...

  2. wpf 客户端【JDAgent桌面助手】开发详解(三) 瀑布流效果实现与UI虚拟化优化大数据显示

    目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...

  3. js 实现图片瀑布流效果,可更改配置参数 带完整版解析代码[waterFall.js]

    前言:         本人纯小白一个,有很多地方理解的没有各位大牛那么透彻,如有错误,请各位大牛指出斧正!小弟感激不尽.         本篇文章为您分析一下原生JS实现图片瀑布流效果 页面需求 1 ...

  4. RecylerView完美实现瀑布流效果

    RecylerView包含三种布局管理器,分别是LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager,对应实现单行列表,多行 ...

  5. RecyclerView实现瀑布流效果(图文详解+源码奉送)

    最近有时间研究了一下RecyclerView,果然功能强大啊,能实现的效果还是比较多的,那么今天给大家介绍一个用RecyclerView实现的瀑布流效果. 先来一张效果图: 看看怎么实现吧: 整体工程 ...

  6. 手把手教你js原生瀑布流效果实现

    手把手教你js原生瀑布流效果实现 什么是瀑布流效果 首先,让我们先看一段动画: 在动画中,我们不难发现,这个动画有以下特点: 1.所有的图片的宽度都是一样的 2.所有的图片的高度是不一样的 3.图片一 ...

  7. 【前端】用jQuery实现瀑布流效果

    jQuery实现瀑布流效果 何为瀑布流: 瀑布流,又称瀑布流式布局.是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部.最早 ...

  8. javascript瀑布流效果

    javascript瀑布流效果 其实javascript瀑布流 前几年都已经很流行了(特别是美丽说,蘑菇街),最近看到网上有人问这个瀑布流效果,所以自己有空的时候就研究了下,其实也是研究别人的代码,研 ...

  9. 实现RecyclerView下拉刷新和上拉加载更多以及RecyclerView线性、网格、瀑布流效果演示

    实现RecyclerView下拉刷新和上拉加载更多以及RecyclerView线性.网格.瀑布流效果演示 效果预览 实例APP 小米应用商店 使用方法 build.gradle文件 dependenc ...

随机推荐

  1. (Problem 57)Square root convergents

    It is possible to show that the square root of two can be expressed as an infinite continued fractio ...

  2. [lua]笔试-按字典序列出指指定的序列的位置

    计算方法: n的阶乘记为f(n), s为输入序列, sub(i)为s的i到n的子序列.A(i)为第i位对应的字母在子序列sub(i)中的字典顺序 N(s) = sum_{1,n} T(i)*(A(i) ...

  3. [转] HTC:html组件

    ~~~不时会看到 用htc实现 hover, border-raius等效果,可以用来弥补IE6-8的不足.那么htc到底是什么呢? 1.摘要 本文在实例的基础上讨论了HTC(HTML Compone ...

  4. C/C++用strncpy()与strstr()分割与匹配查找字符串

    最近做题遇到分割与匹配字符串的题目(hdu5311),看来别人的代码,才知道有strncpy()和strstr()函数,于是搜集了一点资料,记录一下基本用法. 一.strncpy() char * s ...

  5. FFmpeg源码结构图 - 解码

    ===================================================== FFmpeg的库函数源码分析文章列表: [架构图] FFmpeg源码结构图 - 解码 FFm ...

  6. 微软阵营稳定的好消息:.NET开源、Visual Studio 自由

    今天各个IT社区,头版头条说的是微软.NET开源了.宇宙中最好的IED–Visual Studio Community 2013将免费提供给用户的消息. <宇宙中最强大的开发环境免费了! > ...

  7. java字符串输出

    package mytest; public class Mycode { public static void main(String[] args){ String[]seasons = {&qu ...

  8. MacBook外接显示器设置方法(新手入门贴)

    小屏幕的MacBook/MacBook Pro放在桌上长时间使用,眼睛比较累,而且,长时间低头看屏幕,易得颈椎病,绝对有损健康.配一台大屏幕的外置显示器不失为两全其美的好办法. 首先,得买一台中意的大 ...

  9. [Swust OJ 566]--开N方数(牛顿切线法解高次方程)

    题目链接:http://acm.swust.edu.cn/problem/0566/ Time limit(ms): 1000 Memory limit(kb): 65535   Descriptio ...

  10. iOS开发笔记--使用blend改变图片颜色

    最近对Core Animation和Core Graphics的内容东西比较感兴趣,自己之前也在这块相对薄弱,趁此机会也想补习一下这块的内容,所以之后几篇可能都会是对CA和CG学习的记录的文章. 在应 ...