編輯:關於Android編程
總結一下微信的本地圖片加載有以下幾個特點,也是提高用戶體驗的關鍵點
1、縮略圖挨個加載,一個一個加載完畢,直到屏幕所有縮略圖都加載完成
2、不等當前屏的所有縮略圖加載完,迅速向下滑,滑動停止時立即加載停止頁面的圖片
3、已經加載成功的縮略圖,不管滑出去多遠,滑回來的時候不需要重新加載
4、在相冊以外的環境中,需要讓imageView的寬高比例隨圖片的寬高比例自動伸縮,而且要在圖片加載完畢之前就要預留占位空間
為了滿足上面幾個要求,主要采用以下幾個方法:
0、為了防止圖片加載出來OOM,需要對分辨率和顏色的位數進行縮小到合適范圍,同時采用LRU緩存
1、采用一個定長線程池,線程池的大小等於CPU的數量+1,把所有縮略圖加載任務都交給線程池執行,以獲得最快的加載效率。
2、在用戶快速滑動的時候,沒有加載完畢的劃走了的圖片立即停止加載,將所占線程讓出來,讓新的加載任務執行。
3、已經加載成功的縮略圖,保存到sd卡中,下次再滑動回來的時候,直接從sd卡加載以前保存好的小圖,不經過線程池。
4、對於三星這樣的手機,其圖片全都是寬度大於高度,方向用exif進行記錄,圖片加載器要讀出exif的方向信息,然後通過矩陣進行旋轉
下面就是具體代碼了
import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.media.ExifInterface; import android.util.Log; import android.view.ViewGroup; import android.widget.ImageView; import com.imaginato.qravedconsumer.task.AlxMultiTask; import com.qraved.app.R; /** * Created by Administrator on 2016/4/8. */ public class AlxImageLoader { private Context mcontext; private HashMap> imageCache = new HashMap >(); private ConcurrentHashMap currentUrls = new ConcurrentHashMap<>();//記錄一個imageView應該顯示哪個Url,用於中斷子線程 private Bitmap defbitmap; public AlxImageLoader(Context context) { this.mcontext = context; defbitmap = BitmapFactory.decodeResource(mcontext.getResources(), R.drawable.upload_photo4x);//沒加載到圖片的默認顯示 } /** * 從本地加載一張圖片並使用imageView進行顯示,可以設置是否根據圖片的大小動態修改imageView的高度,寬度必須傳入來控制顯示圖片的清晰度防止oom * @param uri * @param imageView * @param imageViewWidth * @param resizeImageView * @param autoRotate * @param imageCallback * @return */ private Bitmap loadBitmapFromSD(final String uri, final ImageView imageView, final int imageViewWidth, final boolean resizeImageView, final boolean autoRotate,final boolean storeThumbnail ,final ImageCallback imageCallback) { if (imageCache.containsKey(uri)) {//如果之前已經加載過這個圖片,那麼就從LRU緩存裡加載 SoftReference SoftReference = imageCache.get(uri); Bitmap bitmap = SoftReference.get(); if (bitmap != null) { Log.i("Alex","現在是從LRU中拿出來的bitmap"); return bitmap;//從系統內存裡直接拿出來 } } final int[] imageSize = {0,0}; if(uri ==null)return null; if(storeThumbnail) { File file = new File(imageView.getContext().getCacheDir().getAbsolutePath().concat("/" + new File(uri).getName())); if (file.exists() && file.length()>1000) { //因為從file中獲取圖片的寬高存在IO操作,所以把每個圖片的寬高緩存起來 Log.i("Alex", "現在是從cache目錄中拿出來的縮略圖"); return BitmapFactory.decodeFile(file.getAbsolutePath()); } } //如果沒有緩存options,那麼就先獲取options new AlxMultiTask (){ @Override protected BitmapFactory.Options doInBackground(Void... params) {//這一塊主要是用來拿寬高,確定要加載圖片的大小的 //線程開啟之後,由於滾動太快,已經過了一段時間,可能imageView要顯示的圖片已經換了,就沒有必要執行下面的東西了 String targetUrl = currentUrls.get(imageView);//滑動的非常快的時候會在此處中斷 if(!uri.equals(targetUrl)) { Log.i("Alex","這個圖片已經過時了0"); return null; } final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 不讀取像素數組到內存中,僅讀取圖片的信息,非常重要 BitmapFactory.decodeFile(uri, options);//讀取文件信息,存放到Options對象中 String targetUrl1 = currentUrls.get(imageView);//滑動的非常快的時候會在此處中斷 if(!uri.equals(targetUrl1)) { Log.i("Alex","這個圖片已經過時了1"); return null; } // 從Options中獲取圖片的分辨率 imageSize[0] = options.outWidth; imageSize[1] = options.outHeight; Log.i("Alex","原圖的分辨率是"+imageSize[0]+" "+imageSize[1]); Log.i("Alex","目標寬度是"+imageViewWidth); if(imageSize[0]<1)return null; int destWidth = imageViewWidth; if (imageViewWidth > 200) destWidth /= 2;//如果imageView太大的話,不需要加載那麼大的圖片,就縮小一下 float compressRatio = imageSize[0] / (float) destWidth;//使用圖片源寬度除以目標imageView的寬度計算出一個壓縮比 int compressRatioInt = Math.round(compressRatio);//四捨五入 if (compressRatioInt % 2 != 0 && compressRatioInt != 1) compressRatioInt++;//如果是奇數的話,就給弄成偶數 Log.i("Alex", "長寬壓縮比是" + compressRatio + " 偶數化後" + compressRatioInt); options.inSampleSize = compressRatioInt; options.inPurgeable = true; options.inJustDecodeBounds = false; options.inPreferredConfig = Bitmap.Config.RGB_565; return options; } @Override protected void onPostExecute(final BitmapFactory.Options options) { super.onPostExecute(options); //在線程終止回調的時候會產生巨大的延遲 String targetUrl = currentUrls.get(imageView); if(!uri.equals(targetUrl)) { Log.i("Alex","這個圖片已經過時了haha"); return; } if (options == null) return; asynGetBitmap(options,uri,imageView,imageViewWidth,resizeImageView,storeThumbnail,autoRotate,imageCallback); } }.executeDependSDK(); return defbitmap;//在子線程執行結束之前先用默認bitmap頂著 } /** * 一個在子線程裡從文件獲取bitmap,並存到LRU緩存的方法 * @param catchedOptions * @param uri * @param imageView * @param autoRotate * @param imageCallback */ private void asynGetBitmap(final BitmapFactory.Options catchedOptions, final String uri, final ImageView imageView, final int imageViewWidth, final boolean resizeImageView, final boolean autoRotate, final boolean storeThumbnail, final ImageCallback imageCallback){ //如果不需要重置imageView的大小,那麼底下這部分先不執行 if (resizeImageView && imageViewWidth > 0) {//如果給出了imageView的寬度,就修改imageView的寬高以自適應圖片的寬高 int imageViewHeight; int degree = readPictureDegree(uri); if (autoRotate && (degree == 90 || degree == 270)) {//如果原來是豎著的,且需要自動擺正那麼寬和高要互換 imageViewHeight = catchedOptions.outWidth * imageViewWidth / catchedOptions.outHeight; } else { imageViewHeight = catchedOptions.outHeight * imageViewWidth / catchedOptions.outWidth; } JLogUtils.i("Alex", "准備重設高度" + imageViewHeight); ViewGroup.LayoutParams params = imageView.getLayoutParams(); if (params != null) {//如果是旋轉90度的圖片,那麼寬和高應該互換 params.height = imageViewHeight; imageView.setLayoutParams(params); } } new AlxMultiTask () { @Override protected Bitmap doInBackground(Void... params) { String targetUrl = currentUrls.get(imageView); if(!uri.equals(targetUrl)) { Log.i("Alex","這個圖片已經過時了2");//滑動的比較快的時候會在此處中斷 return null; } Bitmap bitmap = null; //首先獲取完整的bitmap存到內存裡,此處有可能oom bitmap = getBitmapFromFile(uri, catchedOptions); // String targetUrl3 = currentUrls.get(imageView); // if(!uri.equals(targetUrl3)) { // Log.i("Alex","這個圖片已經過時了3");//在此處經常會中斷 // return null; // } //獲取完bitmap之後,因為已經過了一段時間,可能imageView要顯示的圖片已經換了,就沒有必要執行下面的東西了 if (autoRotate) {//如果需要自動旋轉 int degree = readPictureDegree(uri); //獲取完角度之後,因為已經過了一段時間,可能imageView要顯示的圖片已經換了,就沒有必要執行下面的東西了 if (degree != 0) bitmap = rotateBitmap(bitmap, degree, true); } if (bitmap == null) bitmap = BitmapFactory.decodeResource(mcontext.getResources(), R.drawable.upload_photo4x);//如果出現異常,就用默認的bitmap return bitmap; } @Override protected void onPostExecute(final Bitmap bitmap) { super.onPostExecute(bitmap); String targetUrl = currentUrls.get(imageView); if(!uri.equals(targetUrl)) { Log.i("Alex","這個圖片已經過時了5");//在此處經常會中斷 return; } if (bitmap == null) return; imageCallback.imageLoaded(bitmap, imageView, uri); //顯示完圖片之後將縮略圖緩存到本地 final Context context = imageView.getContext(); if(!storeThumbnail)return; new AlxMultiTask (){ @Override protected Void doInBackground(Void... params) { imageCache.put(uri, new SoftReference (bitmap));//將bitmap存到LRU緩存裡 storeThumbnail(context,new File(uri).getName(),bitmap); return null; } }.executeDependSDK(); } }.executeDependSDK(); } private interface ImageCallback { void imageLoaded(Bitmap imageBitmap, ImageView imageView, String uri); } /** * 從本地根據相應的options獲取完整的bitmap存到內存裡,有可能會出現oom異常 * @param uri * @param options * @return */ private static Bitmap getBitmapFromFile(String uri, BitmapFactory.Options options) { if(uri==null || uri.length()<4 || options==null)return null; try{ if(!new File(uri).isFile())return null;//如果文件不存在 Bitmap bitmap = BitmapFactory.decodeFile(uri, options);// 這裡還是會出現oom?? return bitmap; }catch (Exception e){ Log.i("Alex","從圖片中獲取bitmap出現異常",e); }catch (OutOfMemoryError e) { Log.i("Alex","從文件中獲取圖片 OOM了",e); } return null; } /** * 讀取一個jpg文件的exif中的旋轉信息 * @param path * @return */ public static int readPictureDegree(String path) { int degree = 0; try { ExifInterface exifInterface = new ExifInterface(path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270; break; } } catch (IOException e) { Log.i("Alex","獲取圖片旋轉角度出現異常",e); return degree; } Log.i("Alex","本張圖片的旋轉角度是"+path+" 角度是"+degree); return degree; } /** * 旋轉一個bitmap,注意這個操作會銷毀傳入的bitmap,並且會占用源bitmap兩倍的內存,所以要把一個已經壓縮好的bitmap放進去,如果沒有轉換成功就返回原來的bitmap * @param bitmap * @param degrees * @return */ public static Bitmap rotateBitmap(Bitmap bitmap, int degrees,boolean destroySource) { if (degrees == 0) return bitmap; Log.i("Alex","准備旋轉bitmap,內存占用是"+AlxBitmapUtils.getSize(bitmap)+" 寬度是"+bitmap.getHeight()+" 高度是"+bitmap.getHeight()+" 角度是"+degrees); try { Matrix matrix = new Matrix(); matrix.setRotate(degrees, bitmap.getWidth() / 2, bitmap.getHeight() / 2); Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (null != bitmap && destroySource) bitmap.recycle(); return bmp; } catch (Exception e) { e.printStackTrace(); Log.i("Alex","旋轉bitmap出現異常",e); return bitmap; } catch (OutOfMemoryError e) { e.printStackTrace(); Log.i("Alex","旋轉bitmap出現oom異常",e); return bitmap; } } /** * 異步加載本地圖片的暴露方法 * @param uri * @param imageView * @param imageViewWidth 如果想要imageView大小隨圖片文件自適應全顯示的話,需要給一個imageView的目標寬度 */ public void setAsyncBitmapFromSD(String uri, ImageView imageView,int imageViewWidth,boolean resizeImageView,boolean autoRotate,boolean storeThumbnail) { //從LRU緩存裡獲取bitmap if(uri!=null) currentUrls.put(imageView,uri);//把url綁定在imageView上,用來防止顯示緩存錯誤 else currentUrls.put(imageView,""); Bitmap cacheBitmap = loadBitmapFromSD(uri, imageView,imageViewWidth,resizeImageView,autoRotate,storeThumbnail, new ImageCallback() { public void imageLoaded(Bitmap imageBitmap, ImageView imageView, String imageUrl) { Log.i("Alex","加載成功的bitmap寬高是"+imageBitmap.getWidth()+" x "+imageBitmap.getHeight()); imageView.setImageBitmap(imageBitmap); } }); if(cacheBitmap!=null) { if(uri!=null)imageView.setImageBitmap(cacheBitmap); Log.i("Alex","緩存的bitmap是"+cacheBitmap.getWidth()+" ::"+cacheBitmap.getHeight()); ViewGroup.LayoutParams params = imageView.getLayoutParams(); if(resizeImageView && params!=null && imageViewWidth>0 && cacheBitmap!=defbitmap) {//只有當現在緩存裡的的bitmap不是默認bitmap的時候才重新修改大小,因為根據默認bitmap重設大小是沒有意義的 int height = cacheBitmap.getHeight()* imageViewWidth / cacheBitmap.getWidth() ; Log.i("Alex","准備重設高度haha"+height); params.height = height; imageView.setLayoutParams(params); } }else { Log.i("Alex","緩存的bitmap為空"); } } /** * 保存一個縮略圖到sd卡,這樣在selectPhoto的時候,第二次加載同一張圖片就會變快 * @param bitmap * @return */ public static boolean storeThumbnail(Context context, String fileName, Bitmap bitmap){ if(bitmap==null)return false; File file = new File(context.getCacheDir().getAbsolutePath().concat("/"+fileName)); if(!file.exists()) try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); return false; } OutputStream out = null; try { out = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); return true; } catch (FileNotFoundException e) { e.printStackTrace(); return false; }finally { if(out!=null) try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }
import android.os.AsyncTask; import android.os.Build; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Created by Alex on 2016/4/19. * 用於替換系統自帶的AsynTask,使用自己的多線程池,執行一些比較復雜的工作,比如select photos,這裡用的是緩存線程池,也可以用和cpu數相等的定長線程池以提高性能 */ public abstract class AlxMultiTaskextends AsyncTask { private static ExecutorService photosThreadPool;//用於加載大圖和評論的線程池 private final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private final int CORE_POOL_SIZE = CPU_COUNT + 1; public void executeDependSDK(Params...params){ if(photosThreadPool==null)photosThreadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE); if(Build.VERSION.SDK_INT<11) super.execute(params); else super.executeOnExecutor(photosThreadPool,params); } }
調用方法
第一個參數是文件url路徑,第一個是imageView對象,第二個是加載imageView的寬度,第三個參數是是否讓imageView的大小隨圖片的寬高比例自動伸縮,第四個參數是是否讓圖片的方向隨exif記錄的角度旋轉,最後一個參數是是否在sd卡保存加載成功縮略圖
this.alxImageLoader = new AlxImageLoader(activity); alxImageLoader.setAsyncBitmapFromSD(filePath,viewHolder.iv_photo,getScreenWidth()/3,false,true,true);
ProgressBar進度條,分為旋轉進度條和水平進度條,進度條的樣式根據需要自定義,之前一直不明白進度條如何在實際項目中使用,網上演示進度條的案例大多都是通過Butto
今天給大家講講android開發中比較常見的listView的下拉加載,其實也可以叫做分頁加載。為什麼會有這個叫法呢?說說我的理解吧!從字面上很好理解。當你滑動一個列表到
ViewPager做導航想不想有這樣的效果: 比如說有四張圖片,下面有四個圓點,當頁面滑動的時候一個點變大一個點變小(或者是一個點變小一個點變大),等於說同時在執行兩個動
昨天看了下RenderScript的官方文檔,發現RenderScript這厮有點牛逼。無意中發現ScriptIntrinsic這個抽象類,有些很有用的子類。其中有個子類