Android 自定義本地圖片載入庫,仿微信相簿
總結一下微信的本地圖片載入有以下幾個特點,也是提高使用者體驗的關鍵點
1、縮圖挨個載入,一個一個載入完畢,直到螢幕所有縮圖都載入完成
2、不等當前屏的所有縮圖載入完,迅速向下滑,滑動停止時立即載入停止頁面的圖片
3、已經載入成功的縮圖,不管滑出去多遠,滑回來的時候不需要重新載入
4、在相簿以外的環境中,需要讓imageView的寬高比例隨圖片的寬高比例自動伸縮,而且要在圖片載入完畢之前就要預留佔位空間
為了滿足上面幾個要求,主要採用以下幾個方法:
0、為了防止圖片載入出來OOM,需要對解析度和顏色的位數進行縮小到合適範圍,同時採用LRU快取
1、採用一個定長執行緒池,執行緒池的大小等於CPU的數量+1,把所有縮圖載入任務都交給執行緒池執行,以獲得最快的載入效率。
2、在使用者快速滑動的時候,沒有載入完畢的划走了的圖片立即停止載入,將所佔執行緒讓出來,讓新的載入任務執行。
3、已經載入成功的縮圖,儲存到sd卡中,下次再滑動回來的時候,直接從sd卡載入以前儲存好的小圖,不經過執行緒池。
4、對於三星這樣的手機,其圖片全都是寬度大於高度,方向用exif進行記錄,圖片載入器要讀出exif的方向資訊,然後通過矩陣進行旋轉
效果展示:
github專案地址:https://github.com/AlexZhuo/AlxImageLoader
下面就是載入器主要部分的註釋和程式碼
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.demo.task.AlxMultiTask;
/**
* Created by Administrator on 2016/4/8.
*/
public class AlxImageLoader {
private Context mcontext;
private HashMap<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
private ConcurrentHashMap<ImageView,String> 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<Bitmap> 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<Void,Void,BitmapFactory.Options>(){
@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,autoRotate,storeThumbnail,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<Void, Void, Bitmap>() {
@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<Void,Void,Void>(){
@Override
protected Void doInBackground(Void... params) {
imageCache.put(uri, new SoftReference<Bitmap>(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 AlxMultiTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
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);
相關文章
- Android本地圖片上傳(拍照+相簿)Android地圖
- html5仿微信朋友圈相簿圖片放大程式碼HTML
- Android 仿微信的圖片選擇器ImageSelector的使用Android
- iOS-對圖片操作---新增到自定義相簿iOS
- flutter 圖片檢視,仿微信Flutter
- Android 自定義圓形旋轉進度條,仿微博頭像載入效果Android
- 仿微信iOS相簿選擇 MTImagePickeriOS
- Flutter 自定義列表以及本地圖片引用Flutter地圖
- 微信小程式(canvas)畫圖儲存到本地相簿(wepy)微信小程式Canvas
- Flutter仿微信,支付寶密碼輸入框+自定義鍵盤Flutter密碼
- 教你一鍵下載微博相簿的所有圖片,自動批量採集微博相簿所有圖片
- Android生成圖片並放入相簿Android
- Android仿微信圖片編輯——塗鴉框架Doodle(多功能畫板)Android框架
- Android 圖片載入框架Android框架
- 載入本地圖片模糊,Glide載入網路圖片卻很清晰地圖IDE
- 微信小程式--實現圖片懶載入(lazyload)微信小程式
- Android 仿微信, QQ 裁剪Android
- Android 高效安全載入圖片Android
- iOS 仿微信相簿選擇照片imagePicker(Swift) 序號 預覽縮圖iOSSwift
- Android常用圖片載入庫介紹及對比Android
- Android 圖片載入庫Glide知其然知其所以然之載入AndroidIDE
- 『自定義View實戰』—— 仿ios圖示下載viewViewiOS
- vue如何動態載入本地圖片Vue地圖
- RecorderManager安卓仿微信自定義音視訊錄製第三方庫安卓
- android仿微信表情雨下落!Android
- 解耦圖片載入庫解耦
- 仿微信圖片選取、相機拍照—PhotoPicker(已整合GalleryView)View
- Android偽圖片載入進度效果Android
- Android 基礎之圖片載入(二)Android
- Android圖解建立外部lib庫及自定義ViewAndroid圖解View
- 微信小程式 實現網路圖片本地快取微信小程式快取
- Android自定義View–仿QQ音樂歌詞AndroidView
- 自定義View-27 仿58同城載入資料動畫View動畫
- Vue富文字帶圖片修改圖片大小自定義選擇項自定義字型Vue自定義字型
- 微信小程式 自定義tabbar微信小程式tabBar
- 微信小程式自定義tabBar微信小程式tabBar
- Android圖片載入庫Glide 知其然知其所以然 開篇AndroidIDE
- 6.自定義圖片剪下
- 一個仿微信朋友圈的圖片檢視框架 - PhotoViewer框架View