大家好!过完年回来到现在差不多一个月没写文章了,一是觉得不知道写哪些方面的文章,没有好的题材来写,二是因为自己的一些私事给耽误了,所以过完年的第一篇文章到现在才发表出来,2014年我还是会继续在CSDN上面更新我的博客,欢迎大家关注一下,今天这篇文章主要的是介绍下开源库StickyGridHeaders的使用,StickyGridHeaders是一个自定义GridView带sections和headers的Android库,sections就是GridView item之间的分隔,headers就是固定在GridView顶部的标题,类似一些Android手机联系人的效果,StickyGridHeaders的介绍在https://github.com/TonicArtos/StickyGridHeaders,与此对应也有一个相同效果的自定义ListView带sections和headers的开源库https://github.com/emilsjolander/StickyListHeaders,大家有兴趣的可以去看下,我这里介绍的是StickyGridHeaders的使用,我在Android应用方面看到使用StickyGridHeaders的不是很多,而是在Iphone上看到相册采用的是这种效果,于是我就使用StickyGridHeaders来仿照Iphone按照日期分隔显示本地图片
我们先新建一个Android项目StickyHeaderGridView,去https://github.com/TonicArtos/StickyGridHeaders下载开源库,为了方便浏览源码我直接将源码拷到我的工程中了
com.tonicartos.widget.stickygridheaders这个包就是我放StickyGridHeaders开源库的源码,com.example.stickyheadergridview这个包是我实现此功能的代码,类看起来还蛮多的,下面我就一一来介绍了
GridItem用来封装StickyGridHeadersGridView 每个Item的数据,里面有本地图片的路径,图片加入手机系统的时间和headerId
package com.example.stickyheadergridview;
/**
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
public class GridItem {
/**
* 图片的路径
*/
private String path;
/**
* 图片加入手机中的时间,只取了年月日
*/
private String time;
/**
* 每个Item对应的HeaderId
*/
private int headerId;
public GridItem(String path, String time) {
super();
this.path = path;
this.time = time;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public int getHeaderId() {
return headerId;
}
public void setHeaderId(int headerId) {
this.headerId = headerId;
}
}
图片的路径path和图片加入的时间time 我们直接可以通过ContentProvider获取,但是headerId需要我们根据逻辑来生成。
package com.example.stickyheadergridview;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
/**
* 图片扫描器
*
* @author xiaanming
*
*/
public class ImageScanner {
private Context mContext;
public ImageScanner(Context context){
this.mContext = context;
}
/**
* 利用ContentProvider扫描手机中的图片,将扫描的Cursor回调到ScanCompleteCallBack
* 接口的scanComplete方法中,此方法在运行在子线程中
*/
public void scanImages(final ScanCompleteCallBack callback) {
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
callback.scanComplete((Cursor)msg.obj);
}
};
new Thread(new Runnable() {
@Override
public void run() {
//先发送广播扫描下整个sd卡
mContext.sendBroadcast(new Intent(
Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + Environment.getExternalStorageDirectory())));
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = mContext.getContentResolver();
Cursor mCursor = mContentResolver.query(mImageUri, null, null, null, MediaStore.Images.Media.DATE_ADDED);
//利用Handler通知调用线程
Message msg = mHandler.obtainMessage();
msg.obj = mCursor;
mHandler.sendMessage(msg);
}
}).start();
}
/**
* 扫描完成之后的回调接口
*
*/
public static interface ScanCompleteCallBack{
public void scanComplete(Cursor cursor);
}
}
package com.example.stickyheadergridview;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.os.Handler;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.util.Log;
/**
* 本地图片加载器,采用的是异步解析本地图片,单例模式利用getInstance()获取NativeImageLoader实例
* 调用loadNativeImage()方法加载本地图片,此类可作为一个加载本地图片的工具类
*
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
public class NativeImageLoader {
private static final String TAG = NativeImageLoader.class.getSimpleName();
private static NativeImageLoader mInstance = new NativeImageLoader();
private static LruCache<String, Bitmap> mMemoryCache;
private ExecutorService mImageThreadPool = Executors.newFixedThreadPool(1);
private NativeImageLoader(){
//获取应用程序的最大内存
final int maxMemory = (int) (Runtime.getRuntime().maxMemory());
//用最大内存的1/8来存储图片
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
//获取每张图片的bytes
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
/**
* 通过此方法来获取NativeImageLoader的实例
* @return
*/
public static NativeImageLoader getInstance(){
return mInstance;
}
/**
* 加载本地图片,对图片不进行裁剪
* @param path
* @param mCallBack
* @return
*/
public Bitmap loadNativeImage(final String path, final NativeImageCallBack mCallBack){
return this.loadNativeImage(path, null, mCallBack);
}
/**
* 此方法来加载本地图片,这里的mPoint是用来封装ImageView的宽和高,我们会根据ImageView控件的大小来裁剪Bitmap
* 如果你不想裁剪图片,调用loadNativeImage(final String path, final NativeImageCallBack mCallBack)来加载
* @param path
* @param mPoint
* @param mCallBack
* @return
*/
public Bitmap loadNativeImage(final String path, final Point mPoint, final NativeImageCallBack mCallBack){
//先获取内存中的Bitmap
Bitmap bitmap = getBitmapFromMemCache(path);
final Handler mHander = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mCallBack.onImageLoader((Bitmap)msg.obj, path);
}
};
//若该Bitmap不在内存缓存中,则启用线程去加载本地的图片,并将Bitmap加入到mMemoryCache中
if(bitmap == null){
mImageThreadPool.execute(new Runnable() {
@Override
public void run() {
//先获取图片的缩略图
Bitmap mBitmap = decodeThumbBitmapForFile(path, mPoint == null ? 0: mPoint.x, mPoint == null ? 0: mPoint.y);
Message msg = mHander.obtainMessage();
msg.obj = mBitmap;
mHander.sendMessage(msg);
//将图片加入到内存缓存
addBitmapToMemoryCache(path, mBitmap);
}
});
}
return bitmap;
}
/**
* 往内存缓存中添加Bitmap
*
* @param key
* @param bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null && bitmap != null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 根据key来获取内存中的图片
* @param key
* @return
*/
private Bitmap getBitmapFromMemCache(String key) {
Bitmap bitmap = mMemoryCache.get(key);
if(bitmap != null){
Log.i(TAG, "get p_w_picpath for LRUCache , path = " + key);
}
return bitmap;
}
/**
* 清除LruCache中的bitmap
*/
public void trimMemCache(){
mMemoryCache.evictAll();
}
/**
* 根据View(主要是ImageView)的宽和高来获取图片的缩略图
* @param path
* @param viewWidth
* @param viewHeight
* @return
*/
private Bitmap decodeThumbBitmapForFile(String path, int viewWidth, int viewHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//设置为true,表示解析Bitmap对象,该对象不占内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
//设置缩放比例
options.inSampleSize = computeScale(options, viewWidth, viewHeight);
//设置为false,解析Bitmap对象加入到内存中
options.inJustDecodeBounds = false;
Log.e(TAG, "get Iamge form file, path = " + path);
return BitmapFactory.decodeFile(path, options);
}
/**
* 根据View(主要是ImageView)的宽和高来计算Bitmap缩放比例。默认不缩放
* @param options
* @param width
* @param height
*/
private int computeScale(BitmapFactory.Options options, int viewWidth, int viewHeight){
int inSampleSize = 1;
if(viewWidth == 0 || viewWidth == 0){
return inSampleSize;
}
int bitmapWidth = options.outWidth;
int bitmapHeight = options.outHeight;
//假如Bitmap的宽度或高度大于我们设定图片的View的宽高,则计算缩放比例
if(bitmapWidth > viewWidth || bitmapHeight > viewWidth){
int widthScale = Math.round((float) bitmapWidth / (float) viewWidth);
int heightScale = Math.round((float) bitmapHeight / (float) viewWidth);
//为了保证图片不缩放变形,我们取宽高比例最小的那个
inSampleSize = widthScale < heightScale ? widthScale : heightScale;
}
return inSampleSize;
}
/**
* 加载本地图片的回调接口
*
* @author xiaanming
*
*/
public interface NativeImageCallBack{
/**
* 当子线程加载完了本地的图片,将Bitmap和图片路径回调在此方法中
* @param bitmap
* @param path
*/
public void onImageLoader(Bitmap bitmap, String path);
}
}
我们看主界面的布局代码,里面只有一个自定义的StickyGridHeadersGridView控件
<?xml version="1.0" encoding="utf-8"?>
<com.tonicartos.widget.stickygridheaders.StickyGridHeadersGridView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/asset_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:columnWidth="90dip"
android:horizontalSpacing="3dip"
android:numColumns="auto_fit"
android:verticalSpacing="3dip" />
在看主界面的代码之前我们先看StickyGridAdapter的代码
package com.example.stickyheadergridview;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.stickyheadergridview.MyImageView.OnMeasureListener;
import com.example.stickyheadergridview.NativeImageLoader.NativeImageCallBack;
import com.tonicartos.widget.stickygridheaders.StickyGridHeadersSimpleAdapter;
/**
* StickyHeaderGridView的适配器,除了要继承BaseAdapter之外还需要
* 实现StickyGridHeadersSimpleAdapter接口
*
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
public class StickyGridAdapter extends BaseAdapter implements
StickyGridHeadersSimpleAdapter {
private List<GridItem> hasHeaderIdList;
private LayoutInflater mInflater;
private GridView mGridView;
private Point mPoint = new Point(0, 0);//用来封装ImageView的宽和高的对象
public StickyGridAdapter(Context context, List<GridItem> hasHeaderIdList,
GridView mGridView) {
mInflater = LayoutInflater.from(context);
this.mGridView = mGridView;
this.hasHeaderIdList = hasHeaderIdList;
}
@Override
public int getCount() {
return hasHeaderIdList.size();
}
@Override
public Object getItem(int position) {
return hasHeaderIdList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder mViewHolder;
if (convertView == null) {
mViewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.grid_item, parent, false);
mViewHolder.mImageView = (MyImageView) convertView
.findViewById(R.id.grid_item);
convertView.setTag(mViewHolder);
//用来监听ImageView的宽和高
mViewHolder.mImageView.setOnMeasureListener(new OnMeasureListener() {
@Override
public void onMeasureSize(int width, int height) {
mPoint.set(width, height);
}
});
} else {
mViewHolder = (ViewHolder) convertView.getTag();
}
String path = hasHeaderIdList.get(position).getPath();
mViewHolder.mImageView.setTag(path);
Bitmap bitmap = NativeImageLoader.getInstance().loadNativeImage(path, mPoint,
new NativeImageCallBack() {
@Override
public void onImageLoader(Bitmap bitmap, String path) {
ImageView mImageView = (ImageView) mGridView
.findViewWithTag(path);
if (bitmap != null && mImageView != null) {
mImageView.setImageBitmap(bitmap);
}
}
});
if (bitmap != null) {
mViewHolder.mImageView.setImageBitmap(bitmap);
} else {
mViewHolder.mImageView.setImageResource(R.drawable.friends_sends_pictures_no);
}
return convertView;
}
@Override
public View getHeaderView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder mHeaderHolder;
if (convertView == null) {
mHeaderHolder = new HeaderViewHolder();
convertView = mInflater.inflate(R.layout.header, parent, false);
mHeaderHolder.mTextView = (TextView) convertView
.findViewById(R.id.header);
convertView.setTag(mHeaderHolder);
} else {
mHeaderHolder = (HeaderViewHolder) convertView.getTag();
}
mHeaderHolder.mTextView.setText(hasHeaderIdList.get(position).getTime());
return convertView;
}
/**
* 获取HeaderId, 只要HeaderId不相等就添加一个Header
*/
@Override
public long getHeaderId(int position) {
return hasHeaderIdList.get(position).getHeaderId();
}
public static class ViewHolder {
public MyImageView mImageView;
}
public static class HeaderViewHolder {
public TextView mTextView;
}
}
另外我们需要实现StickyGridHeadersSimpleAdapter接口的getHeaderId(int position)和getHeaderView(int position, View convertView, ViewGroup parent),getHeaderId(int position)方法返回每个Item的headerId,getHeaderView()方法是生成sections和headers的,如果某个item的headerId跟他下一个item的HeaderId不同,则会调用getHeaderView方法生成一个sections用来区分不同的组,还会根据firstVisibleItem的headerId来生成一个位于顶部的headers,所以如何生成每个Item的headerId才是关键,生成headerId的方法在MainActivity中
package com.example.stickyheadergridview;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.TimeZone;
import android.app.Activity;
import android.app.ProgressDialog;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.MediaStore;
import android.widget.GridView;
import com.example.stickyheadergridview.ImageScanner.ScanCompleteCallBack;
public class MainActivity extends Activity {
private ProgressDialog mProgressDialog;
/**
* 图片扫描器
*/
private ImageScanner mScanner;
private GridView mGridView;
/**
* 没有HeaderId的List
*/
private List<GridItem> nonHeaderIdList = new ArrayList<GridItem>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGridView = (GridView) findViewById(R.id.asset_grid);
mScanner = new ImageScanner(this);
mScanner.scanImages(new ScanCompleteCallBack() {
{
mProgressDialog = ProgressDialog.show(MainActivity.this, null, "正在加载...");
}
@Override
public void scanComplete(Cursor cursor) {
// 关闭进度条
mProgressDialog.dismiss();
if(cursor == null){
return;
}
while (cursor.moveToNext()) {
// 获取图片的路径
String path = cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media.DATA));
//获取图片的添加到系统的毫秒数
long times = cursor.getLong(cursor
.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
GridItem mGridItem = new GridItem(path, paserTimeToYMD(times, "yyyy年MM月dd日"));
nonHeaderIdList.add(mGridItem);
}
cursor.close();
//给GridView的item的数据生成HeaderId
List<GridItem> hasHeaderIdList = generateHeaderId(nonHeaderIdList);
//排序
Collections.sort(hasHeaderIdList, new YMDComparator());
mGridView.setAdapter(new StickyGridAdapter(MainActivity.this, hasHeaderIdList, mGridView));
}
});
}
/**
* 对GridView的Item生成HeaderId, 根据图片的添加时间的年、月、日来生成HeaderId
* 年、月、日相等HeaderId就相同
* @param nonHeaderIdList
* @return
*/
private List<GridItem> generateHeaderId(List<GridItem> nonHeaderIdList) {
Map<String, Integer> mHeaderIdMap = new HashMap<String, Integer>();
int mHeaderId = 1;
List<GridItem> hasHeaderIdList;
for(ListIterator<GridItem> it = nonHeaderIdList.listIterator(); it.hasNext();){
GridItem mGridItem = it.next();
String ymd = mGridItem.getTime();
if(!mHeaderIdMap.containsKey(ymd)){
mGridItem.setHeaderId(mHeaderId);
mHeaderIdMap.put(ymd, mHeaderId);
mHeaderId ++;
}else{
mGridItem.setHeaderId(mHeaderIdMap.get(ymd));
}
}
hasHeaderIdList = nonHeaderIdList;
return hasHeaderIdList;
}
@Override
protected void onDestroy() {
super.onDestroy();
//退出页面清除LRUCache中的Bitmap占用的内存
NativeImageLoader.getInstance().trimMemCache();
}
/**
* 将毫秒数装换成pattern这个格式,我这里是转换成年月日
* @param time
* @param pattern
* @return
*/
public static String paserTimeToYMD(long time, String pattern ) {
System.setProperty("user.timezone", "Asia/Shanghai");
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone.setDefault(tz);
SimpleDateFormat format = new SimpleDateFormat(pattern);
return format.format(new Date(time * 1000L));
}
}
主界面的代码主要是组装StickyGridHeadersGridView的数据,我们将扫描出来的图片的路径,时间的毫秒数解析成年月日的格式封装到GridItem中,然后将GridItem加入到List中,此时每个Item还没有生成headerId,我们需要调用generateHeaderId(),该方法主要是将同一天加入的系统的图片生成相同的HeaderId,这样子同一天加入的图片就在一个组中,当然你要改成同一个月的图片在一起,修改paserTimeToYMD()方法的第二个参数就行了,当Activity finish之后,我们利用NativeImageLoader.getInstance().trimMemCache()释放内存,当然我们还需要对GridView的数据进行排序,比如说headerId相同的item不连续,headerId相同的item就会生成多个sections(即多个分组),所以我们要利用YMDComparator使得在同一天加入的图片在一起,YMDComparator的代码如下
package com.example.stickyheadergridview;
import java.util.Comparator;
public class YMDComparator implements Comparator<GridItem> {
@Override
public int compare(GridItem o1, GridItem o2) {
return o1.getTime().compareTo(o2.getTime());
}
}
接下来我们运行下程序看看效果如何
今天的文章就到这里结束了,感谢大家的观看,上面还有一个类和一些资源文件没有贴出来,大家有兴趣研究下就直接下载项目源码,记住采用LruCache缓存图片的时候,cacheSize不要设置得过大,不然产生OOM的概率就更大些,我利用上面的程序测试显示600多张图片来回滑动,没有产生OOM,有问题不明白的同学可以在下面留言!
项目源码,点击下载
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。