您好,登錄后才能下訂單哦!
大家好!過完年回來到現在差不多一個月沒寫文章了,一是覺得不知道寫哪些方面的文章,沒有好的題材來寫,二是因為自己的一些私事給耽誤了,所以過完年的第一篇文章到現在才發表出來,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 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); } }
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,有問題不明白的同學可以在下面留言!
項目源碼,點擊下載
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。