- 浏览: 4107279 次
最新评论
Android 使用开源库StickyGridHeaders来实现带sections和headers的GridView显示本地图片效果
转载请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/20481185),请尊重他人的辛勤劳动成果,谢谢!
大家好!过完年回来到现在差不多一个月没写文章了,一是觉得不知道写哪些方面的文章,没有好的题材来写,二是因为自己的一些私事给耽误了,所以过完年的第一篇文章到现在才发表出来,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); } }ImageScanner是一个图片的扫描器类,该类使用ContentProvider扫描手机中的图片,我们通过调用scanImages()方法就能对手机中的图片进行扫描,将扫描的Cursor回调到ScanCompleteCallBack接口的scanComplete方法中,由于考虑到扫描图片属于耗时操作,所以该操作运行在子线程中,在我们扫描图片之前我们需要先发送广播来扫描外部媒体库,为什么要这么做呢,假如我们新增加一张图片到sd卡,图片确实已经添加了进去,但是我们此时的媒体库还没有同步更新,若不同步媒体库我们就看不到新增加的图片,当然我们可以通过重新启动系统来更新媒体库,但是这样不可取,所以我们直接发送广播就可以同步媒体库了。
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 image 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); } }NativeImageLoader该类是一个单例类,提供了本地图片加载,内存缓存,裁剪等逻辑,该类在加载本地图片的时候采用的是异步加载的方式,对于大图片的加载也是比较耗时的,所以采用子线程的方式去加载,对于图片的缓存机制使用的是LruCache,我们使用手机分配给应用程序内存的1/8用来缓存图片,给图片缓存的内存不宜太大,太大也可能会发生OOM,该类是用我之前写的文章Android 使用ContentProvider扫描手机中的图片,仿微信显示本地图片效果,在这里我就不做过多的介绍,有兴趣的可以去看看那篇文章,不过这里新增了一个方法trimMemCache(),,用来清空LruCache使用的内存
我们看主界面的布局代码,里面只有一个自定义的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; } }除了要继承BaseAdapter之外还需要实现StickyGridHeadersSimpleAdapter接口,继承BaseAdapter需要实现getCount(),getItem(int position),getItemId(int position),getView(int position, View convertView, ViewGroup parent)这四个方法,这几个方法的实现跟我们平常实现的方式一样,主要是看一下getView()方法,我们将每个item的图片路径设置Tag到该ImageView上面,然后利用NativeImageLoader来加载本地图片,在这里使用的ImageView依然是自定义的MyImageView,该自定义ImageView主要实现当MyImageView测量完毕之后,就会将测量的宽和高回调到onMeasureSize()中,然后我们可以根据MyImageView的大小来裁剪图片
另外我们需要实现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()); } }当然这篇文章不使用YMDComparator也是可以的,因为我在利用ContentProvider获取图片的时候,就是根据加入系统的时间排序的,排序只是针对一般的数据来说的。
接下来我们运行下程序看看效果如何
今天的文章就到这里结束了,感谢大家的观看,上面还有一个类和一些资源文件没有贴出来,大家有兴趣研究下就直接下载项目源码,记住采用LruCache缓存图片的时候,cacheSize不要设置得过大,不然产生OOM的概率就更大些,我利用上面的程序测试显示600多张图片来回滑动,没有产生OOM,有问题不明白的同学可以在下面留言!
相关推荐
GridView分组显示StickyGridHeaders,GridView分组显示StickyGridHeaders
前端开源库-markdown-it-lazy-headersMarkdown it Lazy Headers,Lazy ATX Headers插件用于Markdown it
StickyGridHeaders Replacement project at SuperSLiM This repository is abandoned and will no longer see any development or support. The replacement SuperSLiM is an implementation of a layout manager on...
显示总记录数、每页记录数、当前页数、总页数、首页、上一页、下一页、末页和分页按钮 使用方法(设置CustomPagerSettings复合属性): PagingMode - 自定义分页的显示模式 TextFormat - 自定义分页的...
StickyGridHeaders 待头布局的gridview, 可根据头布局类型进行分类
A UI widget that allows for headers and footers on lists backed by RecyclerView, for Android. Download Grab the artifact via JCenter. Include JCenter as a repository in your build.gradle file: ...
在PHP中实现GridView 功能如下:可以实现列搜索过滤的功能,自动分页,还可以列排序功能,非常实用. Features -------- # Filtering and searching capabilities # Ability to change column headers # Capable of ...
nginx扩展工具,nginx的headers_more模块用于 添加、修改或清除 请求/响应头,该模块不是nginx自带的,默认不包含该模块,需要另外安装。幸运的是openresty默认包含了该模块,可以直接使用。 该模块主要有4个指令...
firefox56版本使用插件,已经签名,可以正常使用 File modifyHeaders = new File(pluginPath); profile.addExtension(modifyHeaders); profile.setPreference("modifyheaders.headers.count", 1); profile....
sticky-headers-recyclerview This decorator allows you to easily create section headers for RecyclerViews using a LinearLayoutManager in either vertical or horizontal orientation. Credit to Emil Sjö...
StickyGridHeaders is an Android library that provides a GridView that shows items in sections with headers. By default the section headers stick to the top like the People app in Android 4.x but this ...
PHP实现的带超时功能get_headers函数_.docx
Android StickyListHeaders 实现类似 Ios sectioned list效果, listview 被分成几个组,滑动的时候,每个组滑到顶部的时候,该组的标签会停止滑动,当下一个组滑动到顶部的时候会把上次组的头部顶上去,海豚浏览器,QQ,...
Headers 排插 PCB库DXP
开源项目-carlmjohnson-get-headers.zip,I wrote a simple tool to show the headers from GET-ing a URL in Go
python库,解压后可用。 资源全名:Headers_as_Dependencies-0.1.1-py3-none-any.whl
DSP新建工程所需的common and headers文件,下载后直接复制到新建工程文件夹下即可。
Response-Headers详解,WEB开发不可缺少的帮助文档
live http headers0.17.1火狐老插件,可以直接本地安装在旧版火狐上面,可以查看源码、查看连接等。