zoukankan      html  css  js  c++  java
  • 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

    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>

    源码下载

  • 相关阅读:
    B.Icebound and Sequence
    Educational Codeforces Round 65 (Rated for Div. 2) D. Bicolored RBS
    Educational Codeforces Round 65 (Rated for Div. 2) C. News Distribution
    Educational Codeforces Round 65 (Rated for Div. 2) B. Lost Numbers
    Educational Codeforces Round 65 (Rated for Div. 2) A. Telephone Number
    Codeforces Round #561 (Div. 2) C. A Tale of Two Lands
    Codeforces Round #561 (Div. 2) B. All the Vowels Please
    Codeforces Round #561 (Div. 2) A. Silent Classroom
    HDU-2119-Matrix(最大匹配)
    读书的感想!
  • 原文地址:https://www.cnblogs.com/james1207/p/3299605.html
Copyright © 2011-2022 走看看