Android UI 之WaterFall瀑布流效果

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

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

       

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

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

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

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

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

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

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

    二是当加载图片比较多的情况下,对图片进行回收,防止OOM的处理。

    对于第一点,主要是加载时机的判断以及加载内容的异步处理。这一部分其实理解起来还是比较容易,具体可以参见下面给出的源码。

    对于第二点,在进行回收的时候,我们的整体思路是以用户当前看到的这一个屏幕为基准,向上两屏以及向下两屏一共有5屏的内容,超出这5屏范围的bitmap将被回收。

    在向上滚动的时候,将回收超过下方两屏范围的bitmap,并重载进入上方两屏的bitmap。

    在向下滚动的时候,将回收超过上方两屏范围的bitmap,并重载进入下方两屏的bitmap。

    具体的实现思路还是参见源码,我有给出比较详细的注释。

先来看一下项目的结构:


WaterFall.java

[java]  view plain copy
  1. package com.carrey.waterfall.waterfall;  
  2.   
  3. import java.io.IOException;  
  4. import java.lang.ref.WeakReference;  
  5. import java.util.ArrayList;  
  6. import java.util.Random;  
  7.   
  8. import android.content.Context;  
  9. import android.graphics.Color;  
  10. import android.os.Handler;  
  11. import android.os.Message;  
  12. import android.util.AttributeSet;  
  13. import android.view.MotionEvent;  
  14. import android.widget.LinearLayout;  
  15. import android.widget.ScrollView;  
  16. /** 
  17.  * 瀑布流 
  18.  * 某些参数做了固定设置,如果想扩展功能,可自行修改 
  19.  * @author carrey 
  20.  * 
  21.  */  
  22. public class WaterFall extends ScrollView {  
  23.       
  24.     /** 延迟发送message的handler */  
  25.     private DelayHandler delayHandler;  
  26.     /** 添加单元到瀑布流中的Handler */  
  27.     private AddItemHandler addItemHandler;  
  28.       
  29.     /** ScrollView直接包裹的LinearLayout */  
  30.     private LinearLayout containerLayout;  
  31.     /** 存放所有的列Layout */  
  32.     private ArrayList<LinearLayout> colLayoutArray;  
  33.       
  34.     /** 当前所处的页面(已经加载了几次) */  
  35.     private int currentPage;  
  36.       
  37.     /** 存储每一列中向上方向的未被回收bitmap的单元的最小行号 */  
  38.     private int[] currentTopLineIndex;  
  39.     /** 存储每一列中向下方向的未被回收bitmap的单元的最大行号 */  
  40.     private int[] currentBomLineIndex;  
  41.     /** 存储每一列中已经加载的最下方的单元的行号 */  
  42.     private int[] bomLineIndex;  
  43.     /** 存储每一列的高度 */  
  44.     private int[] colHeight;  
  45.       
  46.     /** 所有的图片资源路径 */  
  47.     private String[] imageFilePaths;  
  48.       
  49.     /** 瀑布流显示的列数 */  
  50.     private int colCount;  
  51.     /** 瀑布流每一次加载的单元数量 */  
  52.     private int pageCount;  
  53.     /** 瀑布流容纳量 */  
  54.     private int capacity;  
  55.       
  56.     private Random random;  
  57.       
  58.     /** 列的宽度 */  
  59.     private int colWidth;  
  60.       
  61.     private boolean isFirstPage;  
  62.   
  63.     public WaterFall(Context context, AttributeSet attrs, int defStyle) {  
  64.         super(context, attrs, defStyle);  
  65.         init();  
  66.     }  
  67.   
  68.     public WaterFall(Context context, AttributeSet attrs) {  
  69.         super(context, attrs);  
  70.         init();  
  71.     }  
  72.   
  73.     public WaterFall(Context context) {  
  74.         super(context);  
  75.         init();  
  76.     }  
  77.       
  78.     /** 基本初始化工作 */  
  79.     private void init() {  
  80.         delayHandler = new DelayHandler(this);  
  81.         addItemHandler = new AddItemHandler(this);  
  82.         colCount = 4;//默认情况下是4列  
  83.         pageCount = 30;//默认每次加载30个瀑布流单元  
  84.         capacity = 10000;//默认容纳10000张图  
  85.         random = new Random();  
  86.         colWidth = getResources().getDisplayMetrics().widthPixels / colCount;  
  87.           
  88.         colHeight = new int[colCount];  
  89.         currentTopLineIndex = new int[colCount];  
  90.         currentBomLineIndex = new int[colCount];  
  91.         bomLineIndex = new int[colCount];  
  92.         colLayoutArray = new ArrayList<LinearLayout>();  
  93.     }  
  94.       
  95.     /** 
  96.      * 在外部调用 第一次装载页面 必须调用 
  97.      */  
  98.     public void setup() {  
  99.         containerLayout = new LinearLayout(getContext());  
  100.         containerLayout.setBackgroundColor(Color.WHITE);  
  101.         LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(  
  102.                 LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);  
  103.         addView(containerLayout, layoutParams);  
  104.           
  105.         for (int i = 0; i < colCount; i++) {  
  106.             LinearLayout colLayout = new LinearLayout(getContext());  
  107.             LinearLayout.LayoutParams colLayoutParams = new LinearLayout.LayoutParams(  
  108.                     colWidth, LinearLayout.LayoutParams.WRAP_CONTENT);  
  109.             colLayout.setPadding(2222);  
  110.             colLayout.setOrientation(LinearLayout.VERTICAL);  
  111.               
  112.             containerLayout.addView(colLayout, colLayoutParams);  
  113.             colLayoutArray.add(colLayout);  
  114.         }  
  115.           
  116.         try {  
  117.             imageFilePaths = getContext().getAssets().list("images");  
  118.         } catch (IOException e) {  
  119.             e.printStackTrace();  
  120.         }  
  121.         //添加第一页  
  122.         addNextPageContent(true);  
  123.     }  
  124.   
  125.     @Override  
  126.     public boolean onTouchEvent(MotionEvent ev) {  
  127.         switch (ev.getAction()) {  
  128.         case MotionEvent.ACTION_DOWN:  
  129.             break;  
  130.         case MotionEvent.ACTION_UP:  
  131.             //手指离开屏幕的时候向DelayHandler延时发送一个信息,然后DelayHandler  
  132.             //届时来判断当前的滑动位置,进行不同的处理。  
  133.             delayHandler.sendMessageDelayed(delayHandler.obtainMessage(), 200);  
  134.             break;  
  135.         }  
  136.         return super.onTouchEvent(ev);  
  137.     }  
  138.       
  139.     @Override  
  140.     protected void onScrollChanged(int l, int t, int oldl, int oldt) {  
  141.         //在滚动过程中,回收滚动了很远的bitmap,防止OOM  
  142.         /*---回收算法说明: 
  143.          * 回收的整体思路是: 
  144.          * 我们只保持当前手机显示的这一屏以及上方两屏和下方两屏 一共5屏内容的Bitmap, 
  145.          * 超出这个范围的单元Bitmap都被回收。 
  146.          * 这其中又包括了一种情况就是之前回收过的单元的重新加载。 
  147.          * 详细的讲解: 
  148.          * 向下滚动的时候:回收超过上方两屏的单元Bitmap,重载进入下方两屏以内Bitmap 
  149.          * 向上滚动的时候:回收超过下方两屏的单元bitmao,重载进入上方两屏以内bitmap 
  150.          * ---*/  
  151.         int viewHeight = getHeight();  
  152.         if (t > oldt) {//向下滚动  
  153.             if (t > 2 * viewHeight) {  
  154.                 for (int i = 0; i < colCount; i++) {  
  155.                     LinearLayout colLayout = colLayoutArray.get(i);  
  156.                     //回收上方超过两屏bitmap  
  157.                     FlowingView topItem = (FlowingView) colLayout.getChildAt(currentTopLineIndex[i]);  
  158.                     if (topItem.getFootHeight() < t - 2 * viewHeight) {  
  159.                         topItem.recycle();  
  160.                         currentTopLineIndex[i] ++;  
  161.                     }  
  162.                     //重载下方进入(+1)两屏以内bitmap  
  163.                     FlowingView bomItem = (FlowingView) colLayout.getChildAt(Math.min(currentBomLineIndex[i] + 1, bomLineIndex[i]));  
  164.                     if (bomItem.getFootHeight() <= t + 3 * viewHeight) {  
  165.                         bomItem.reload();  
  166.                         currentBomLineIndex[i] = Math.min(currentBomLineIndex[i] + 1, bomLineIndex[i]);  
  167.                     }  
  168.                 }  
  169.             }  
  170.         } else {//向上滚动  
  171.             for (int i = 0; i < colCount; i++) {  
  172.                 LinearLayout colLayout = colLayoutArray.get(i);  
  173.                 //回收下方超过两屏bitmap  
  174.                 FlowingView bomItem = (FlowingView) colLayout.getChildAt(currentBomLineIndex[i]);  
  175.                 if (bomItem.getFootHeight() > t + 3 * viewHeight) {  
  176.                     bomItem.recycle();  
  177.                     currentBomLineIndex[i] --;  
  178.                 }  
  179.                 //重载上方进入(-1)两屏以内bitmap  
  180.                 FlowingView topItem = (FlowingView) colLayout.getChildAt(Math.max(currentTopLineIndex[i] - 10));  
  181.                 if (topItem.getFootHeight() >= t - 2 * viewHeight) {  
  182.                     topItem.reload();  
  183.                     currentTopLineIndex[i] = Math.max(currentTopLineIndex[i] - 10);  
  184.                 }  
  185.             }  
  186.         }  
  187.         super.onScrollChanged(l, t, oldl, oldt);  
  188.     }  
  189.       
  190.     /** 
  191.      * 这里之所以要用一个Handler,是为了使用他的延迟发送message的函数 
  192.      * 延迟的效果在于,如果用户快速滑动,手指很早离开屏幕,然后滑动到了底部的时候, 
  193.      * 因为信息稍后发送,在手指离开屏幕到滑动到底部的这个时间差内,依然能够加载图片 
  194.      * @author carrey 
  195.      * 
  196.      */  
  197.     private static class DelayHandler extends Handler {  
  198.         private WeakReference<WaterFall> waterFallWR;  
  199.         private WaterFall waterFall;  
  200.         public DelayHandler(WaterFall waterFall) {  
  201.             waterFallWR = new WeakReference<WaterFall>(waterFall);  
  202.             this.waterFall = waterFallWR.get();  
  203.         }  
  204.           
  205.         @Override  
  206.         public void handleMessage(Message msg) {  
  207.             //判断当前滑动到的位置,进行不同的处理  
  208.             if (waterFall.getScrollY() + waterFall.getHeight() >=   
  209.                     waterFall.getMaxColHeight() - 20) {  
  210.                 //滑动到底部,添加下一页内容  
  211.                 waterFall.addNextPageContent(false);  
  212.             } else if (waterFall.getScrollY() == 0) {  
  213.                 //滑动到了顶部  
  214.             } else {  
  215.                 //滑动在中间位置  
  216.             }  
  217.             super.handleMessage(msg);  
  218.         }  
  219.     }  
  220.       
  221.     /** 
  222.      * 添加单元到瀑布流中的Handler 
  223.      * @author carrey 
  224.      * 
  225.      */  
  226.     private static class AddItemHandler extends Handler {  
  227.         private WeakReference<WaterFall> waterFallWR;  
  228.         private WaterFall waterFall;  
  229.         public AddItemHandler(WaterFall waterFall) {  
  230.             waterFallWR = new WeakReference<WaterFall>(waterFall);  
  231.             this.waterFall = waterFallWR.get();  
  232.         }  
  233.         @Override  
  234.         public void handleMessage(Message msg) {  
  235.             switch (msg.what) {  
  236.             case 0x00:  
  237.                 FlowingView flowingView = (FlowingView)msg.obj;  
  238.                 waterFall.addItem(flowingView);  
  239.                 break;  
  240.             }  
  241.             super.handleMessage(msg);  
  242.         }  
  243.     }  
  244.     /** 
  245.      * 添加单元到瀑布流中 
  246.      * @param flowingView 
  247.      */  
  248.     private void addItem(FlowingView flowingView) {  
  249.         int minHeightCol = getMinHeightColIndex();  
  250.         colLayoutArray.get(minHeightCol).addView(flowingView);  
  251.         colHeight[minHeightCol] += flowingView.getViewHeight();  
  252.         flowingView.setFootHeight(colHeight[minHeightCol]);  
  253.           
  254.         if (!isFirstPage) {  
  255.             bomLineIndex[minHeightCol] ++;  
  256.             currentBomLineIndex[minHeightCol] ++;  
  257.         }  
  258.     }  
  259.       
  260.     /** 
  261.      * 添加下一个页面的内容 
  262.      */  
  263.     private void addNextPageContent(boolean isFirstPage) {  
  264.         this.isFirstPage = isFirstPage;  
  265.           
  266.         //添加下一个页面的pageCount个单元内容  
  267.         for (int i = pageCount * currentPage;   
  268.                 i < pageCount * (currentPage + 1) && i < capacity; i++) {  
  269.             new Thread(new PrepareFlowingViewRunnable(i)).run();  
  270.         }  
  271.         currentPage ++;  
  272.     }  
  273.       
  274.     /** 
  275.      * 异步加载要添加的FlowingView 
  276.      * @author carrey 
  277.      * 
  278.      */  
  279.     private class PrepareFlowingViewRunnable implements Runnable {  
  280.         private int id;  
  281.         public PrepareFlowingViewRunnable (int id) {  
  282.             this.id = id;  
  283.         }  
  284.           
  285.         @Override  
  286.         public void run() {  
  287.             FlowingView flowingView = new FlowingView(getContext(), id, colWidth);  
  288.             String imageFilePath = "images/" + imageFilePaths[random.nextInt(imageFilePaths.length)];  
  289.             flowingView.setImageFilePath(imageFilePath);  
  290.             flowingView.loadImage();  
  291.             addItemHandler.sendMessage(addItemHandler.obtainMessage(0x00, flowingView));  
  292.         }  
  293.           
  294.     }  
  295.       
  296.     /** 
  297.      * 获得所有列中的最大高度 
  298.      * @return 
  299.      */  
  300.     private int getMaxColHeight() {  
  301.         int maxHeight = colHeight[0];  
  302.         for (int i = 1; i < colHeight.length; i++) {  
  303.             if (colHeight[i] > maxHeight)  
  304.                 maxHeight = colHeight[i];  
  305.         }  
  306.         return maxHeight;  
  307.     }  
  308.       
  309.     /** 
  310.      * 获得目前高度最小的列的索引 
  311.      * @return 
  312.      */  
  313.     private int getMinHeightColIndex() {  
  314.         int index = 0;  
  315.         for (int i = 1; i < colHeight.length; i++) {  
  316.             if (colHeight[i] < colHeight[index])  
  317.                 index = i;  
  318.         }  
  319.         return index;  
  320.     }  
  321. }  

FlowingView.java

[java]  view plain copy
  1. package com.carrey.waterfall.waterfall;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5.   
  6. import android.content.Context;  
  7. import android.graphics.Bitmap;  
  8. import android.graphics.BitmapFactory;  
  9. import android.graphics.Canvas;  
  10. import android.graphics.Color;  
  11. import android.graphics.Paint;  
  12. import android.graphics.Rect;  
  13. import android.view.View;  
  14. import android.widget.Toast;  
  15. /** 
  16.  * 瀑布流中流动的单元 
  17.  * @author carrey 
  18.  * 
  19.  */  
  20. public class FlowingView extends View implements View.OnClickListener, View.OnLongClickListener {  
  21.       
  22.     /** 单元的编号,在整个瀑布流中是唯一的,可以用来标识身份 */  
  23.     private int index;  
  24.       
  25.     /** 单元中要显示的图片Bitmap */  
  26.     private Bitmap imageBmp;  
  27.     /** 图像文件的路径 */  
  28.     private String imageFilePath;  
  29.     /** 单元的宽度,也是图像的宽度 */  
  30.     private int width;  
  31.     /** 单元的高度,也是图像的高度 */  
  32.     private int height;  
  33.       
  34.     /** 画笔 */  
  35.     private Paint paint;  
  36.     /** 图像绘制区域 */  
  37.     private Rect rect;  
  38.       
  39.     /** 这个单元的底部到它所在列的顶部之间的距离 */  
  40.     private int footHeight;  
  41.       
  42.     public FlowingView(Context context, int index, int width) {  
  43.         super(context);  
  44.         this.index = index;  
  45.         this.width = width;  
  46.         init();  
  47.     }  
  48.       
  49.     /** 
  50.      * 基本初始化工作 
  51.      */  
  52.     private void init() {  
  53.         setOnClickListener(this);  
  54.         setOnLongClickListener(this);  
  55.         paint = new Paint();  
  56.         paint.setAntiAlias(true);  
  57.     }  
  58.       
  59.     @Override  
  60.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  61.         setMeasuredDimension(width, height);  
  62.     }  
  63.       
  64.     @Override  
  65.     protected void onDraw(Canvas canvas) {  
  66.         //绘制图像  
  67.         canvas.drawColor(Color.WHITE);  
  68.         if (imageBmp != null && rect != null) {  
  69.             canvas.drawBitmap(imageBmp, null, rect, paint);  
  70.         }  
  71.         super.onDraw(canvas);  
  72.     }  
  73.       
  74.     /** 
  75.      * 被WaterFall调用异步加载图片数据 
  76.      */  
  77.     public void loadImage() {  
  78.         InputStream inStream = null;  
  79.         try {  
  80.             inStream = getContext().getAssets().open(imageFilePath);  
  81.             imageBmp = BitmapFactory.decodeStream(inStream);  
  82.             inStream.close();  
  83.             inStream = null;  
  84.         } catch (IOException e) {  
  85.             e.printStackTrace();  
  86.         }  
  87.         if (imageBmp != null) {  
  88.             int bmpWidth = imageBmp.getWidth();  
  89.             int bmpHeight = imageBmp.getHeight();  
  90.             height = (int) (bmpHeight * width / bmpWidth);  
  91.             rect = new Rect(00, width, height);  
  92.         }  
  93.     }  
  94.       
  95.     /** 
  96.      * 重新加载回收了的Bitmap 
  97.      */  
  98.     public void reload() {  
  99.         if (imageBmp == null) {  
  100.             new Thread(new Runnable() {  
  101.                   
  102.                 @Override  
  103.                 public void run() {  
  104.                     InputStream inStream = null;  
  105.                     try {  
  106.                         inStream = getContext().getAssets().open(imageFilePath);  
  107.                         imageBmp = BitmapFactory.decodeStream(inStream);  
  108.                         inStream.close();  
  109.                         inStream = null;  
  110.                         postInvalidate();  
  111.                     } catch (IOException e) {  
  112.                         e.printStackTrace();  
  113.                     }  
  114.                 }  
  115.             }).start();  
  116.         }  
  117.     }  
  118.       
  119.     /** 
  120.      * 防止OOM进行回收 
  121.      */  
  122.     public void recycle() {  
  123.         if (imageBmp == null || imageBmp.isRecycled())   
  124.             return;  
  125.         new Thread(new Runnable() {  
  126.               
  127.             @Override  
  128.             public void run() {  
  129.                 imageBmp.recycle();  
  130.                 imageBmp = null;  
  131.                 postInvalidate();  
  132.             }  
  133.         }).start();  
  134.     }  
  135.       
  136.     @Override  
  137.     public boolean onLongClick(View v) {  
  138.         Toast.makeText(getContext(), "long click : " + index, Toast.LENGTH_SHORT).show();  
  139.         return true;  
  140.     }  
  141.   
  142.     @Override  
  143.     public void onClick(View v) {  
  144.         Toast.makeText(getContext(), "click : " + index, Toast.LENGTH_SHORT).show();  
  145.     }  
  146.   
  147.     /** 
  148.      * 获取单元的高度 
  149.      * @return 
  150.      */  
  151.     public int getViewHeight() {  
  152.         return height;  
  153.     }  
  154.     /** 
  155.      * 设置图片路径 
  156.      * @param imageFilePath 
  157.      */  
  158.     public void setImageFilePath(String imageFilePath) {  
  159.         this.imageFilePath = imageFilePath;  
  160.     }  
  161.   
  162.     public Bitmap getImageBmp() {  
  163.         return imageBmp;  
  164.     }  
  165.   
  166.     public void setImageBmp(Bitmap imageBmp) {  
  167.         this.imageBmp = imageBmp;  
  168.     }  
  169.   
  170.     public int getFootHeight() {  
  171.         return footHeight;  
  172.     }  
  173.   
  174.     public void setFootHeight(int footHeight) {  
  175.         this.footHeight = footHeight;  
  176.     }  
  177. }  

MainActivity.java

[java]  view plain copy
  1. package com.carrey.waterfall;  
  2.   
  3. import com.carrey.waterfall.waterfall.WaterFall;  
  4.   
  5. import android.os.Bundle;  
  6. import android.app.Activity;  
  7.   
  8. public class MainActivity extends Activity {  
  9.   
  10.     @Override  
  11.     protected void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         setContentView(R.layout.activity_main);  
  14.           
  15.         WaterFall waterFall = (WaterFall) findViewById(R.id.waterfall);  
  16.         waterFall.setup();  
  17.     }  
  18.   
  19. }  

activity_main.xml

[html]  view plain copy
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context=".MainActivity" >  
  6.   
  7.     <com.carrey.waterfall.waterfall.WaterFall   
  8.         android:id="@+id/waterfall"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="match_parent"/>  
  11.   
  12. </RelativeLayout>  
源码下载
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值