因此如何高效的加载Bitmap是一个很重要也很容易被开发者或忽视的问题。
为了更好地介绍上述三个主题,本章提供了一个示例程序,该程序会尝试从网络加载大量图片,并在GridView中现实,可以发现这个程序具有很强的使用性,并且技术细节完全覆盖了本章的三个主题:图片加载、缓存策略、列表的滑动流程性,通过这个示例程序读者可以很好地理解本章地全部内容并能够在实际中灵活应用。
一 Bitmap的高效加载
- (1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
- (2)从BitmapFactory.Options中取出图片的原始宽高,它们对应于outWidth和outHeight参数。
- (3)根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize.
- (4)将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重写加载图片。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int rewHeight){ final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res,resId,options); options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight); options.inJustDecodeBounds = true; return BitmapFactory.decodeResource(res,resId,options); } (BitmapFactory.Options options, int reqWidth, int rewHeight){ final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > rewHeight || width > reqWidth) { final int halfHeight = height/2; final int haleWidth = width/2; while ((halfHeight / inSampleSize) >= rewHeight && (haleWidth/inSampleSize) >= reqWidth){ inSampleSize *= 2; } } return inSampleSize; }
有了上面的两个方法,实际使用的时候就很简单了,比如ImageView所期望的图片大小为100*100像素,这个时候就可以通过如下方式高效地加载并显示图片:
mImageView.setImageBitmap(BitmapUtil.
decodeSampledBitmapFromResource(
getResources(),R.mipmap.ic_launcher,100,100));
二 Android中的缓存策略
如何避免过多的流量消耗呢?那就是本节所要讨论的主题:缓存。当程序第一次网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会把图片放在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取。如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。上述的缓存策略不仅仅适用于图片,也适用于其他文件类型。
2.1LruCache
- 强引用:直接的对象引用。
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
- 弱引用:当一个对象只有软引用存在时,此对象会随时被gc回收。
int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024); int cacheSize = maxMemory /8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } };
mMemoryCache.get(key);
向LruCache中添加一个缓存对象,如下所示。
mMemoryCache.put(key,bitmap);
2.2 DiskLruCache
- 1.DiskLruCache的创建
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException
File diskCacheDir = getDiskCacheDir(mContext, "bitmap"); if (!diskCacheDir.exists()){ diskCacheDir.mkdirs(); } try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); } catch (IOException e) { e.printStackTrace(); }
- 2.DiskLruCache的缓存添加
private String hashKeyFromUrl(String url){ String cacheKey; try { final MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(url.getBytes()); cacheKey = bytesToHexString(digest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(url.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes){ StringBuilder sb = new StringBuilder(); for (int i=0; i< bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1){ sb.append("0"); } sb.append(hex); } return sb.toString(); }
将图片的url转成key以后,就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。需要注意的是前面在DiskLruCache的open方法中设置了一个节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量直接设置为0即可,如下所示。
String key = hashKeyFromUrl(url); try { DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); } } catch (IOException e) { e.printStackTrace(); }
有了文件输出流,接下来要怎么做呢?其实是这样的,当从网络下载图片时,图片即可以通过这个文件输出流写入到文件系统上,这个过程的实现如下所示。
private boolean downloadUrlToStream(String urlString, OutputStream outputStream){ HttpURLConnection urlConnection = null; BufferedInputStream in = null; BufferedOutputStream out = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection)url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream()); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read())!=-1){ out.write(b); } return true; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (urlConnection != null){ urlConnection.disconnect(); } MyUtils.close(out); MyUtils.close(in); } return false; }
经过上面的步骤,其实并没有真正的将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作,这个过程如下所示。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(url, outputStream)){ editor.commit(); } else { editor.abort(); } mDiskLruCache.flush();
- 3.DiskLruCache的缓存查找
Bitmap bitmap = null; String key = hashKeyFromUrl(url); try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX); FileDescriptor descriptor = fileInputStream.getFD(); bitmap = BitmapUtil.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight); if (bitmap != null){ addBitmapToMemoryCache(key,bitmap); } } } catch (IOException e) { e.printStackTrace(); } return bitmap;
2.3 ImageLoader的实现
- 图片的同步加载
- 图片的异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取。
内存缓存和磁盘缓存时ImageLoader的核心,也是ImageLoader的意义所在,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户造成地流量消耗,只有当这两级缓存都不可以时才需要从网络中拉取图片。
- 1.图片的压缩功能的实现
public class ImageResizer { public ImageResizer() { } public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor descriptor, int reqWidth, int rewHeight ){ final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(descriptor, null,options); options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeFileDescriptor(descriptor, null,options); } public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int rewHeight){ final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res,resId,options); options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res,resId,options); } public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int rewHeight){ if (reqWidth == 0 || rewHeight == 0) { return 1; } final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > rewHeight || width > reqWidth) { final int halfHeight = height/2; final int haleWidth = width/2; while ((halfHeight / inSampleSize) >= rewHeight && (haleWidth/inSampleSize) >= reqWidth){ inSampleSize *= 2; } } return inSampleSize; } }
- 2.内存缓存和磁盘缓存的实现
private LruCache<String, Bitmap> mMemoryCache; private DiskLruCache mDiskLruCache; public ImageLoader(Context context) { mContext = context.getApplicationContext(); int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024); int cacheSize = maxMemory /8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; File diskCacheDir = getDiskCacheDir(mContext, "bitmap"); if (!diskCacheDir.exists()){ diskCacheDir.mkdirs(); } if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); mIsDiskLruCacheCreated = true; } catch (IOException e) { e.printStackTrace(); } } }
private Bitmap getBitmapFromMemoryCache(String key){ return mMemoryCache.get(key); } private void addBitmapToMemoryCache(String key, Bitmap bitmap){ if (getBitmapFromMemoryCache(key) == null) { mMemoryCache.put(key, bitmap); } }
而磁盘缓存和读取功能稍微复杂一些,具体内容已经在2.2节中进行了详细的介绍,这里再简单说明一下。磁盘缓存的添加需要通过Editor来完成,Editor提高了commit和abort方法来提交和撤销对文件系统的写操作,具体实现请参看下面的loadBitmapFromHttp方法。磁盘缓存的读取需要通过Snapshot来完成,通过Snapshot可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷地进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存中,具体实现请参考下面的loadBitmapFromDiskCache方法。
private Bitmap loadBitmapFromDiskCache(String url,int reqWidth, int rewHeight) throws IOException{ if (Looper.myLooper() == Looper.getMainLooper()){ throw new RuntimeException("can not visit network from UI thread."); } if (mDiskLruCache == null){ return null; } Bitmap bitmap = null; String key = hashKeyFromUrl(url); DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX); FileDescriptor descriptor = fileInputStream.getFD(); bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight); if (bitmap != null){ addBitmapToMemoryCache(key,bitmap); } } return bitmap; } private Bitmap loadBitmapFromHttp(String url,int reqWidth, int rewHeight)throws IOException{ if (Looper.myLooper() == Looper.getMainLooper()){ throw new RuntimeException("can not visit network from UI thread."); } if (mDiskLruCache == null){ return null; } String key = hashKeyFromUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(url, outputStream)){ editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } return loadBitmapFromDiskCache(url, reqWidth, rewHeight); }
public Bitmap loadBitmap(String url,int reqWidth, int rewHeight){ Bitmap bitmap = loadBitmapFromMemoryCache(url); if (bitmap != null) { return bitmap; } try { bitmap = loadBitmapFromDiskCache(url,reqWidth,rewHeight); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromHttp(url,reqWidth,rewHeight); } catch (IOException e) { e.printStackTrace(); } if (bitmap == null && !mIsDiskLruCacheCreated) { bitmap = downloadFromUrl(url); } return bitmap; }
从loadBitmap的实现可以看出,其工作过程遵循如下几步:首先尝试从内存中读取图片,接着尝试从磁盘缓存中读取图片,最后才从网络中拉取图片。另外,这个方法不能在主线程中调用,否则就会抛出异常。这个执行换下的检查时在loadBitmapFromHttp中实现的,通过检测当前线程的Looper是否为主线的Looper来判断当前线程是否是主线程,如果不是主线程就直接抛出异常终止程序,如下所示。
if (Looper.myLooper() == Looper.getMainLooper()){ throw new RuntimeException("can not visit network from UI thread."); }
接着看异步加载接口的设计,如下所示。
public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){ imageView.setTag(TAG_KEY_URI, uri); final Bitmap bitmap = loadBitmapFromMemoryCache(uri); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } final Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap1 = loadBitmap(uri,reqWidth,reqHeight); if (bitmap1 != null) { LoaderResult result = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); }
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAX_POOL_SIZE = CPU_COUNT*2 + 1; private static final long KEEP_ALIVE = 10L; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @Override public Thread newThread(@NonNull Runnable r) { return new Thread(r, "ImageLoader#" + mCount.getAndIncrement()); } }; public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()){ @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; String uri = (String) imageView.getTag(); if (uri.equals(result.uri)){ imageView.setImageBitmap(result.bitmap); } else { Log.d(TAG, "set image bitmap, but uri has changed,ignored!"); } } };
到此为止,ImageLoader的细节都已经做了全面的分析,下面是ImageLoader的完整代码。
public class ImageLoader { private static final String TAG= "ImageLoader"; private static final int MESSAGE_POST_RESULT = 1; private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAX_POOL_SIZE = CPU_COUNT*2 + 1; private static final long KEEP_ALIVE = 10L; private static final int IO_BUFFER_SIZE = 8 * 1024; private static final int TAG_KEY_URI = R.id.imageloader_url; private static final long DISK_CACHE_SIZE = 1024 * 1024 *50; private static final int DISK_CACHE_INDEX = 0; private boolean mIsDiskLruCacheCreated = false; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @Override public Thread newThread(@NonNull Runnable r) { return new Thread(r, "ImageLoader#" + mCount.getAndIncrement()); } }; public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory); private Handler mMainHandler = new Handler(Looper.getMainLooper()){ @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; String uri = (String) imageView.getTag(); if (uri.equals(result.uri)){ imageView.setImageBitmap(result.bitmap); } else { Log.d(TAG, "set image bitmap, but uri has changed,ignored!"); } } }; private Context mContext; private LruCache<String, Bitmap> mMemoryCache; private DiskLruCache mDiskLruCache; private ImageResizer mImageResizer; private ImageLoader(Context context) { mImageResizer = new ImageResizer(); mContext = context.getApplicationContext(); int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024); int cacheSize = maxMemory /8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; File diskCacheDir = getDiskCacheDir(mContext, "bitmap"); if (!diskCacheDir.exists()){ diskCacheDir.mkdirs(); } if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); mIsDiskLruCacheCreated = true; } catch (IOException e) { e.printStackTrace(); } } } public static ImageLoader build(Context context){ return new ImageLoader(context); } private Bitmap getBitmapFromMemoryCache(String key){ return mMemoryCache.get(key); } private void addBitmapToMemoryCache(String key, Bitmap bitmap){ if (getBitmapFromMemoryCache(key) == null) { mMemoryCache.put(key, bitmap); } } public void bindBitmap(final String uri, final ImageView imageView){ bindBitmap(uri,imageView,0,0); } public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){ imageView.setTag(TAG_KEY_URI, uri); final Bitmap bitmap = loadBitmapFromMemoryCache(uri); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } final Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap1 = loadBitmap(uri,reqWidth,reqHeight); if (bitmap1 != null) { LoaderResult result = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); } public Bitmap loadBitmap(String url,int reqWidth, int rewHeight){ Bitmap bitmap = loadBitmapFromMemoryCache(url); if (bitmap != null) { return bitmap; } try { bitmap = loadBitmapFromDiskCache(url,reqWidth,rewHeight); if (bitmap != null) { return bitmap; } bitmap = loadBitmapFromHttp(url,reqWidth,rewHeight); } catch (IOException e) { e.printStackTrace(); } if (bitmap == null && !mIsDiskLruCacheCreated) { bitmap = downloadFromUrl(url); } return bitmap; } private Bitmap loadBitmapFromMemoryCache(String url){ final String key = hashKeyFromUrl(url); return getBitmapFromMemoryCache(key); } private Bitmap loadBitmapFromHttp(String url,int reqWidth, int rewHeight)throws IOException{ if (Looper.myLooper() == Looper.getMainLooper()){ throw new RuntimeException("can not visit network from UI thread."); } if (mDiskLruCache == null){ return null; } String key = hashKeyFromUrl(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(url, outputStream)){ editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } return loadBitmapFromDiskCache(url, reqWidth, rewHeight); } private Bitmap loadBitmapFromDiskCache(String url,int reqWidth, int rewHeight) throws IOException{ if (Looper.myLooper() == Looper.getMainLooper()){ throw new RuntimeException("can not visit network from UI thread."); } if (mDiskLruCache == null){ return null; } Bitmap bitmap = null; String key = hashKeyFromUrl(url); DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX); FileDescriptor descriptor = fileInputStream.getFD(); bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight); if (bitmap != null){ addBitmapToMemoryCache(key,bitmap); } } return bitmap; } private boolean downloadUrlToStream(String urlString, OutputStream outputStream){ HttpURLConnection urlConnection = null; BufferedInputStream in = null; BufferedOutputStream out = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection)url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream()); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read())!=-1){ out.write(b); } return true; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (urlConnection != null){ urlConnection.disconnect(); } MyUtils.close(out); MyUtils.close(in); } return false; } private Bitmap downloadFromUrl(String urlString){ Bitmap bitmap = null; HttpURLConnection urlConnection = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection)url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream()); bitmap = BitmapFactory.decodeStream(in); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } MyUtils.close(in); } return bitmap; } private String hashKeyFromUrl(String url){ String cacheKey; try { final MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(url.getBytes()); cacheKey = bytesToHexString(digest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(url.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes){ StringBuilder sb = new StringBuilder(); for (int i=0; i< bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1){ sb.append("0"); } sb.append(hex); } return sb.toString(); } private File getDiskCacheDir(Context context, String name) { boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); final String cachePath; if (externalStorageAvailable){ cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + name); } @TargetApi(Build.VERSION_CODES.GINGERBREAD) private long getUsableSpace(File path){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){ return path.getUsableSpace(); } final StatFs stats = new StatFs(path.getPath()); return (long)stats.getBlockSize() * (long)stats.getAvailableBlocks(); } private static class LoaderResult { public ImageView imageView; public String uri; public Bitmap bitmap; public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) { this.imageView = imageView; this.uri = uri; this.bitmap = bitmap; } } }
3.ImageLoader的使用
3.1照片强效果
#GridView的的布局文件 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="study.chenj.chapter_9.TestActivity"> <GridView android:id="@+id/gridView1" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:horizontalSpacing="5dp" android:verticalSpacing="5dp" android:listSelector="@android:color/transparent" android:numColumns="3" android:stretchMode="columnWidth"/> </LinearLayout> #GridView的item的布局文件 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical"> <study.chenj.chapter_9.SquareImageView android:id="@+id/image" android:scaleType="centerCrop" android:src="@mipmap/ic_launcher" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
GridView的item的布局文件中并没有采用ImageView,而是采用了一个叫SquareImageView的自定义控件。顾名思义,它的作用就是打造一个正方形的ImageView,这样整个照片墙看起来会比较整齐美观。需要实现一个宽高相等的ImageView是非常简单的是一件事,只需要在onMeasure方法中稍微处理一下,如下所示。
public class SquareImageView extends android.support.v7.widget.AppCompatImageView { public SquareImageView(Context context) { super(context); } public SquareImageView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public SquareImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); } }
public class ImageAdapter extends BaseAdapter { ... @Override public int getCount() { return mUrList.size(); } @Override public Object getItem(int position) { return mUrList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null){ convertView = LayoutInflater.from(mContext).inflate(R.layout.grid_item,parent,false); holder = new ViewHolder(); holder.imageView = (ImageView)convertView.findViewById(R.id.image); convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } ImageView imageView = holder.imageView; final String tag = (String)imageView.getTag(); final String url = (String)getItem(position); if (!url.equals(tag)){ imageView.setImageDrawable(mDefaultBitmapDrawable); } if (mIsGridViewIdle && mCanGetBitmapFromNetWork){ imageView.setTag(url); mImageLoader.bindBitmap(url,imageView,mImageWidth, mImageWidth); } return convertView; } }
GridView gridView = (GridView)findViewById(R.id.gridView1); gridView.setAdapter(new ImageAdapter(this,getUrls()));
3.2 优化列表的卡顿现象
public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){ mIsGridViewIdle = true; ImageAdapter.this.notifyDataSetChanged(); } else { mIsGridViewIdle = false; } }
然后在getView方法中,仅当列表静止时才能加载图片,如下所示。
if (mIsGridViewIdle){ imageView.setTag(url); mImageLoader.bindBitmap(url,imageView,mImageWidth, mImageWidth); }
一般来说经过上面两个步骤,列表都不会有卡顿现象,但是在某些特殊情况下,列表还是会有偶尔卡顿线程,这个时候还可以开启硬件加速。绝大多说情况下,硬件加速可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated="true"即可为Activity开启硬件加速。