編輯:關於Android編程
前言:
加載並顯示gif是App常見的一個功能,像加載普通圖片一樣,大體應該包含以下幾項功能:
1、自動下載GIF到本地文件作為緩存,第二次加載同一個url的圖片不需要下載第二遍
2、由於GIF往往較大,要顯示圓形的進度條提示下載進度
3、在GIF完全下載完之前,先顯示GIF的第一幀圖像進行占位,完全下載完畢之後自動播放動畫。
4、兩個不同的頁面加載同一張GIF,兩個頁面的加載進度應該一致
5、支持ViewPager同時加載多個GIF動圖
效果演示:

實現思路:
1、關於下載和磁盤緩存:
我這裡使用HttpConnection根據url進行下載,在下載之前先將url字符串使用16位MD5進行轉換,讓下載的文件名為url的MD5碼,然後以4096字節為單位,使用ByteStremBuffer進行邊讀邊寫,防止下載過程中內存溢出,而且不時的向磁盤寫入還可以幫助實現GIF第一幀占位的效果。
2、關於進度指示:
我這裡使用了一個圓形的第三方Progress Bar和一個TextView實現,由於在下載過程中以4096為緩沖,所以每下載4096字節就會更新一次進度UI。文件總大小由http返回報文的頭部的Content-length返回,通過已下載大小除以這個length得出下載百分比。
3、關於不同頁面的下載同步:
用戶在首頁會看到一個gif,這時候點擊圖片可以跳進大圖頁繼續這個gif的下載,用戶在首頁的下載進度到帶到大圖頁來,不能讓用戶下載兩遍,也不能在大圖頁打開一個才下載了一半的圖像。
首先在下載開始之前,建立一個MD5.tmp的文件用來存儲下載內容,在下載完畢之後將.tmp文件名後綴去掉,這樣通過文件系統檢索一個GIF是否已被下載的時候,沒有下載完成的圖片就不會被檢索出來。
如果有一個url已經開始了一次下載,這時候又有一個下載請求同一個url,此時會將請求的imageView,textView和progressBar使用一個WeakReference引用起來,防止內存洩漏,然後把這三個空間添加到一個HashMap裡去,這個HashMap的key是url,value就是這些控件的弱引用組成的list。當下載線程更新進度或完成的時候,會從這個HashMap中根據url取出所有和這張gif有關的控件,然後把這些控件統一的更新狀態,這樣就可以保證不同頁面的控件的進度相同,也避免了一個文件下載多次的情況。
4、關於使用GIF的第一幀進行下載占位:
GIF的顯示使用了github上的開源項目:android-gif-drawable,地址:https://github.com/koral--/android-gif-drawable。是一個非常優秀的框架,其內部使用c語言編寫了一些效率非常高的執行代碼。
這個框架的可以直接根據輸入流進行加載,也就是說不用等gif文件完全下載完畢就可以顯示已經下載完畢的內容,甚至可以向浏覽器那樣一行像素一行像素的進行加載,十分好用。
根據框架的這個特性,只需要將還沒有下載好的文件直接傳到Drawable裡,讓道gifImageView中顯示即可,並且在這之前要判斷能否拿到第一幀,然後設置播放選項為暫停。
5、關於VIewPager的使用
在ViewPager的Adapter使用的時候遇到了很多麻煩,主要是由於ViewPager的緩存機制引起的,會引起顯示重復,無控件顯示等等問題,要解決在ViewPager中的使用,並讓GifImageView和普通ImageView一起在ViewPager中和平共處,需要先研究好ViewPager的緩存機制。在這裡我是先根據所有圖片數量生成同等多的imageView放在一個數組裡,然後ViewPager切換到哪張就從數組裡拿出哪張放到ViewPager的Container裡。GIfImageVIew也是這樣,不過是放在另一個數組裡,根據position取得相應的GIFImageView,然後用container來add,這裡對於add過一遍的GIfImageView會報異常,通過catch解決。
具體代碼:
加載工具類:
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import com.imaginato.qravedconsumer.task.AlxMultiTask;
import com.lidroid.xutils.HttpUtils;
import com.pnikosis.materialishprogress.ProgressWheel;
import com.qraved.app.R;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import pl.droidsonroids.gif.GifDrawable;
import pl.droidsonroids.gif.GifImageView;
/**
* Created by Alex on 2016/6/16.
*/
public class AlxGifHelper {
public static class ProgressViews{
public ProgressViews(WeakReference gifImageViewWeakReference, WeakReference progressWheelWeakReference, WeakReference textViewWeakReference,int displayWidth) {
this.gifImageViewWeakReference = gifImageViewWeakReference;
this.progressWheelWeakReference = progressWheelWeakReference;
this.textViewWeakReference = textViewWeakReference;
this.displayWidth = displayWidth;
}
public WeakReference gifImageViewWeakReference;//gif顯示控件
public WeakReference progressWheelWeakReference;//用來裝飾的圓形進度條
public WeakReference textViewWeakReference;//用來顯示當前進度的文本框
public int displayWidth;//imageView的控件寬度
}
public static ConcurrentHashMap> memoryCache;//防止同一個gif文件建立多個下載線程,url和imageView是一對多的關系,如果一個imageView建立了一次下載,那麼其他請求這個url的imageView不需要重新開啟一次新的下載,這幾個imageView同時回調
//為了防止內存洩漏,這個一對多的關系均使用LRU緩存
/**
* 通過本地緩存或聯網加載一張GIF圖片
* @param url
* @param gifView
*/
public static void displayImage(final String url, GifImageView gifView, ProgressWheel progressBar , TextView tvProgress, int displayWidth){
//首先查詢一下這個gif是否已被緩存
String md5Url = getMd5(url);
String path = gifView.getContext().getCacheDir().getAbsolutePath()+"/"+md5Url;//帶.tmp後綴的是沒有下載完成的,用於加載第一幀,不帶tmp後綴是下載完成的,
//這樣做的目的是為了防止一個圖片正在下載的時候,另一個請求相同url的imageView使用未下載完畢的文件顯示一半圖像
JLogUtils.i("AlexGIF","gif圖片的緩存路徑是"+path);
final File cacheFile = new File(path);
if(cacheFile.exists()){//如果本地已經有了這個gif的緩存
JLogUtils.i("AlexGIF","本圖片有緩存");
if(displayImage(cacheFile,gifView,displayWidth)) {//如果本地緩存讀取失敗就重新聯網下載
if (progressBar != null) progressBar.setVisibility(View.GONE);
if (tvProgress!=null)tvProgress.setVisibility(View.GONE);
return;
}
}
//為了防止activity被finish了但是還有很多gif還沒有加載完成,導致activity沒有及時被內存回收導致內存洩漏,這裡使用弱引用
final WeakReference imageViewWait= new WeakReference(gifView);
final WeakReference progressBarWait= new WeakReference(progressBar);
final WeakReference textViewWait= new WeakReference(tvProgress);
if(gifView.getId()!= R.id.gif_photo_view)gifView.setImageResource(R.drawable.qraved_bg_default);//設置沒有下載完成前的默認圖片
if(memoryCache!=null && memoryCache.get(url)!=null){//如果以前有別的imageView加載過
JLogUtils.i("AlexGIF","以前有別的ImageView申請加載過該gif"+url);
//可以借用以前的下載進度,不需要新建一個下載線程了
memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));
return;
}
if(memoryCache==null)memoryCache = new ConcurrentHashMap<>();
if(memoryCache.get(url)==null)memoryCache.put(url,new ArrayList());
//將現在申請加載的這個imageView放到緩存裡,防止重復加載
memoryCache.get(url).add(new ProgressViews(imageViewWait,progressBarWait,textViewWait,displayWidth));
final HttpUtils http = new HttpUtils();
// 下載圖片
startDownLoad(url, new File(cacheFile.getAbsolutePath()+".tmp"), new DownLoadTask() {
@Override
public void onStart() {
JLogUtils.i("AlexGIF","下載GIF開始");
ProgressWheel progressBar = progressBarWait.get();
TextView tvProgress = textViewWait.get();
if(progressBar!=null){
progressBar.setVisibility(View.VISIBLE);
progressBar.setProgress(0);
if(tvProgress==null)return;
tvProgress.setVisibility(View.VISIBLE);
tvProgress.setText("1%");
}
}
@Override
public void onLoading(long total, long current) {
int progress = 0;
//得到要下載文件的大小,是通過http報文的header的Content-Length獲得的,如果獲取不到就是-1
if(total>0)progress = (int)(current*100/total);
JLogUtils.i("AlexGIF","下載gif的進度是"+progress+"%"+" 現在大小"+current+" 總大小"+total);
ArrayList viewses = memoryCache.get(url);
if(viewses ==null)return;
JLogUtils.i("AlexGIF","該gif的請求數量是"+viewses.size());
for(ProgressViews vs : viewses){//遍歷所有的進度條,修改同一個url請求的進度顯示
ProgressWheel progressBar = vs.progressWheelWeakReference.get();
if(progressBar!=null){
progressBar.setProgress((float)progress/100f);
if(total==-1)progressBar.setProgress(20);//如果獲取不到大小,就讓進度條一直轉
}
TextView tvProgress = vs.textViewWeakReference.get();
if(tvProgress != null)tvProgress.setText(progress+"%");
}
//顯示第一幀直到全部下載完之後開始動畫
getFirstPicOfGIF(new File(cacheFile.getAbsolutePath()+".tmp"),vs.gifImageViewWeakReference.get());
}
public void onSuccess(File file) {
if(file==null)return;
String path = file.getAbsolutePath();
if(path==null || path.length()<5)return;
File downloadFile = new File(path);
File renameFile = new File(path.substring(0,path.length()-4));
if(path.endsWith(".tmp"))downloadFile.renameTo(renameFile);//將.tmp後綴去掉
Log.i("AlexGIF","下載GIf成功,文件路徑是"+path+" 重命名之後是"+renameFile.getAbsolutePath());
if(memoryCache==null)return;
ArrayList viewArr = memoryCache.get(url);
if(viewArr==null || viewArr.size()==0)return;
for(ProgressViews ws:viewArr){//遍歷所有的進度條和imageView,同時修改所有請求同一個url的進度
//顯示imageView
GifImageView gifImageView = ws.gifImageViewWeakReference.get();
if (gifImageView!=null)displayImage(renameFile,gifImageView,ws.displayWidth);
//修改進度條
TextView tvProgress = ws.textViewWeakReference.get();
ProgressWheel progressBar = ws.progressWheelWeakReference.get();
if(progressBar!=null)progressBar.setVisibility(View.GONE);
if(tvProgress!=null)tvProgress.setVisibility(View.GONE);
}
JLogUtils.i("AlexGIF",url+"的imageView已經全部加載完畢,共有"+viewArr.size()+"個");
memoryCache.remove(url);//這個url的全部關聯imageView都已經顯示完畢,清除緩存記錄
}
@Override
public void onFailure(Throwable e) {
Log.i("Alex","下載gif圖片出現異常",e);
TextView tvProgress = textViewWait.get();
ProgressWheel progressBar = progressBarWait.get();
if(progressBar!=null)progressBar.setVisibility(View.GONE);
if(tvProgress!=null)tvProgress.setText("image download failed");
if(memoryCache!=null)memoryCache.remove(url);//下載失敗移除所有的弱引用
}
});
}
/**
* 通過本地文件顯示GIF文件
* @param localFile 本地的文件指針
* @param gifImageView
* displayWidth imageView控件的寬度,用於根據gif的實際高度重設控件的高度來保證完整顯示,傳0表示不縮放gif的大小,顯示原始尺寸
*/
public static boolean displayImage(File localFile,GifImageView gifImageView,int displayWidth){
if(localFile==null || gifImageView==null)return false;
JLogUtils.i("AlexGIF","准備加載gif"+localFile.getAbsolutePath()+"顯示寬度為"+displayWidth);
GifDrawable gifFrom;
try {
gifFrom = new GifDrawable(localFile);
int raw_height = gifFrom.getIntrinsicHeight();
int raw_width = gifFrom.getIntrinsicWidth();
JLogUtils.i("AlexGIF","圖片原始height是"+raw_height+" 圖片原始寬是:"+raw_width);
if(gifImageView.getScaleType() != ImageView.ScaleType.CENTER_CROP && gifImageView.getScaleType()!= ImageView.ScaleType.FIT_XY){
//如果大小應該自適應的話進入該方法(也就是wrap content),不然高度不會自動變化
if(raw_width<1 || raw_height<1)return false;
int imageViewWidth = displayWidth;
if(imageViewWidth < 1)imageViewWidth = raw_width;//當傳來的控件寬度不大對的時候,就顯示gif的原始大小
int imageViewHeight = imageViewWidth*raw_height/raw_width;
JLogUtils.i("AlexGIF","縮放完的gif是"+imageViewWidth+" X "+imageViewHeight);
ViewGroup.LayoutParams params = gifImageView.getLayoutParams();
if(params!=null){
params.height = imageViewHeight;
params.width = imageViewWidth;
}
}else {
JLogUtils.i("AlexGIF","按照固定大小進行顯示");
}
gifImageView.setImageDrawable(gifFrom);
return true;
} catch (IOException e) {
JLogUtils.i("AlexGIF","顯示gif出現異常",e);
return false;
}
}
/**
* 用於獲取一個String的md5值
* @param str
* @return
*/
public static String getMd5(String str) {
if(str==null || str.length()<1)return "no_image.gif";
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
byte[] bs = md5.digest(str.getBytes());
StringBuilder sb = new StringBuilder(40);
for(byte x:bs) {
if((x & 0xff)>>4 == 0) {
sb.append("0").append(Integer.toHexString(x & 0xff));
} else {
sb.append(Integer.toHexString(x & 0xff));
}
}
if(sb.length()<24)return sb.toString();
return sb.toString().substring(8,24);//為了提高磁盤的查找文件速度,讓文件名為16位
} catch (NoSuchAlgorithmException e) {
JLogUtils.i("Alex","MD5加密失敗");
return "no_image.gif";
}
}
public static abstract class DownLoadTask{
abstract void onStart();
abstract void onLoading(long total, long current);
abstract void onSuccess(File target);
abstract void onFailure(Throwable e);
boolean isCanceled;
}
/**
* 開啟下載任務到線程池裡,防止多並發線程過多
* @param uri
* @param targetFile
* @param task
*/
public static void startDownLoad(final String uri, final File targetFile, final DownLoadTask task){
final Handler handler = new Handler();
new AlxMultiTask(){//開啟一個多線程池,大小為cpu數量+1
@Override
protected Void doInBackground(Void... params) {
task.onStart();
downloadToStream(uri,targetFile,task,handler);
return null;
}
}.executeDependSDK();
}
/**
* 通過httpconnection下載一個文件,使用普通的IO接口進行讀寫
* @param uri
* @param targetFile
* @param task
* @return
*/
public static long downloadToStream(String uri, final File targetFile, final DownLoadTask task, Handler handler) {
if (task == null || task.isCanceled) return -1;
HttpURLConnection httpURLConnection = null;
BufferedInputStream bis = null;
OutputStream outputStream = null;
long result = -1;
long fileLen = 0;
long currCount = 0;
try {
try {
final URL url = new URL(uri);
outputStream = new FileOutputStream(targetFile);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(20000);
httpURLConnection.setReadTimeout(10000);
final int responseCode = httpURLConnection.getResponseCode();
if (HttpURLConnection.HTTP_OK == responseCode) {
bis = new BufferedInputStream(httpURLConnection.getInputStream());
result = httpURLConnection.getExpiration();
result = result < System.currentTimeMillis() ? System.currentTimeMillis() + 40000 : result;
fileLen = httpURLConnection.getContentLength();//這裡通過http報文的header Content-Length來獲取gif的總大小,需要服務器提前把header寫好
} else {
Log.e("Alex","downloadToStream -> responseCode ==> " + responseCode);
return -1;
}
} catch (final Exception ex) {
handler.post(new Runnable() {
@Override
public void run() {
task.onFailure(ex);
}
});
return -1;
}
if (task.isCanceled) return -1;
byte[] buffer = new byte[4096];//每4k更新進度一次
int len = 0;
BufferedOutputStream out = new BufferedOutputStream(outputStream);
while ((len = bis.read(buffer)) != -1) {
out.write(buffer, 0, len);
currCount += len;
if (task.isCanceled) return -1;
final long finalFileLen = fileLen;
final long finalCurrCount = currCount;
handler.post(new Runnable() {
@Override
public void run() {
task.onLoading(finalFileLen, finalCurrCount);
}
});
}
out.flush();
handler.post(new Runnable() {
@Override
public void run() {
task.onSuccess(targetFile);
}
});
} catch (Throwable e) {
result = -1;
task.onFailure(e);
} finally {
if (bis != null) {
try {
bis.close();
} catch (final Throwable e) {
handler.post(new Runnable() {
@Override
public void run() {
task.onFailure(e);
}
});
}
}
}
return result;
}
/**
* 加載gif的第一幀圖像,用於下載完成前占位
* @param gifFile
* @param imageView
*/
public static void getFirstPicOfGIF(File gifFile,GifImageView imageView){
if(imageView==null)return;
if(imageView.getTag(R.style.AppTheme) instanceof Integer)return;//之前已經顯示過第一幀了,就不用再顯示了
try {
GifDrawable gifFromFile = new GifDrawable(gifFile);
boolean canSeekForward = gifFromFile.canSeekForward();
if(!canSeekForward)return;
JLogUtils.i("AlexGIF","是否能顯示第一幀圖片"+canSeekForward);
//下面是一些其他有用的信息
// int frames = gifFromFile.getNumberOfFrames();
// JLogUtils.i("AlexGIF","已經下載完多少幀"+frames);
// int bytecount = gifFromFile.getFrameByteCount();
// JLogUtils.i("AlexGIF","一幀至少多少字節"+bytecount);
// long memoryCost = gifFromFile.getAllocationByteCount();
// JLogUtils.i("AlexGIF","內存開銷是"+memoryCost);
gifFromFile.seekToFrame(0);
gifFromFile.pause();//靜止在該幀
imageView.setImageDrawable(gifFromFile);
imageView.setTag(R.style.AppTheme,1);//標記該imageView已經顯示過第一幀了
} catch (IOException e) {
JLogUtils.i("AlexGIF","獲取gif信息出現異常",e);
}
}
}
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); } }
public class PhotoImageViewPageAdapter extends PagerAdapter {
@Override
public Object instantiateItem(ViewGroup container, int position) {
String imageUrl = "http://xxx.com/sdf/xxx.gif";
JLogUtils.i("AlexGIF","當前圖片->"+imageUrl);
if(imageUrl.endsWith(".gif")){//如果是gif動圖
JLogUtils.i("AlexGIF","現在是gif大圖");
View rl_gif = LayoutInflater.from(activity).inflate(R.layout.layout_photo_loading_gif_imageview, null);//這種方式容易導致內存洩漏
GifImageView gifImageView = (GifImageView) rl_gif.findViewById(R.id.gif_photo_view);
ProgressWheel progressWheel = (ProgressWheel) rl_gif.findViewById(R.id.progress_wheel);
CustomTextView tv_progress = (CustomTextView) rl_gif.findViewById(R.id.tv_progress);
AlxGifHelper.displayImage(imageUrl,gifImageView,progressWheel,tv_progress,0);//最後一個參數傳0表示不縮放gif的大小,顯示原始尺寸
try {
container.addView(rl_gif);//這裡要注意由於container是一個復用的控件,所以頻繁的addView會導致多張相同的圖片重疊,必須予以處置
}catch (Exception e){
JLogUtils.i("AlexGIF","父控件重復!!!!,這裡出現異常很正常",e);
}
return rl_gif;//這裡有個大坑,千萬不能return container,但是在return之前必須addView
}
}
return container;
}
}
布局文件
dependencies {
compile 'com.pnikosis:materialish-progress:1.7'
}
功能強大的登錄界面Android實現代碼
前言 一個好的應用需要一個有良好的用戶體驗的登錄界面,現如今,許多應用的的登錄界面都有著用戶名,密碼一鍵刪除,用戶名,密碼
Android 6.0的lowmemorykiller機制
最近在處理一些lowmemorykiller相關的問題,於是對lowmemorykiller機制作了一個簡單的了解。在這裡總結一下。首先,是lowmemorykiller
將gradle更好用到應用開發上
將gradle更好應用到你的應用開發上面Gradle深入淺出以下部分可以讓你將一個基於gradle建立的android程序跑起來,並將重點介紹gradle為安卓開發過程中
Android SQLite數據庫基本操作方法
程序的最主要的功能在於對數據進行操作,通過對數據進行操作來實現某個功能。而數據庫就是很重要的一個方面的,Android中內置了小巧輕便,功能卻很強的一個數據庫–SQLit