Android 使用開源庫StickyGridHeaders來實現帶sections和headers的GridView顯

來源:互聯網
上載者:User

轉載請註明本文出自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() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);callback.scanComplete((Cursor)msg.obj);}};new Thread(new Runnable() {@Overridepublic 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@Overrideprotected 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){//先擷取記憶體中的BitmapBitmap bitmap = getBitmapFromMemCache(path);final Handler mHander = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);mCallBack.onImageLoader((Bitmap)msg.obj, path);}};//若該Bitmap不在記憶體緩衝中,則啟用線程去載入本地的圖片,並將Bitmap加入到mMemoryCache中if(bitmap == null){mImageThreadPool.execute(new Runnable() {@Overridepublic 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 implementsStickyGridHeadersSimpleAdapter {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;}@Overridepublic int getCount() {return hasHeaderIdList.size();}@Overridepublic Object getItem(int position) {return hasHeaderIdList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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() {@Overridepublic 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;}@Overridepublic 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 */@Overridepublic 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>();@Overrideprotected 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, "正在載入...");}@Overridepublic 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的資料產生HeaderIdList<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;}@Overrideprotected 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> {@Overridepublic int compare(GridItem o1, GridItem o2) {return o1.getTime().compareTo(o2.getTime());}}
當然這篇文章不使用YMDComparator也是可以的,因為我在利用ContentProvider擷取圖片的時候,就是根據加入系統的時間排序的,排序只是針對一般的資料來說的。

接下來我們運行下程式看看效果如何


今天的文章就到這裡結束了,感謝大家的觀看,上面還有一個類和一些資源檔沒有貼出來,大家有興趣研究下就直接下載項目源碼,記住採用LruCache緩衝圖片的時候,cacheSize不要設定得過大,不然產生OOM的機率就更大些,我利用上面的程式測試顯示600多張圖片來回滑動,沒有產生OOM,有問題不明白的同學可以在下面留言!

項目源碼,點擊下載


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.