編輯:關於Android編程
在移動應用中,很多時候都會用到圖片選擇、圖片裁剪等功能。最近我也在准備一個開源的相冊項目,以方便以後開發應用的時候使用,也盡可能的方便需要的人。一個完整的相冊,應該包含相冊列表、圖片列表、圖片的單選和多選、圖片的裁剪、拍照、多選圖片的大圖預覽等功能。這也是我這個項目將要包含的功能。在本篇博客中,將會講述下我在這個項目中相冊列表和圖片列表的大致實現。
實現效果
結合幾個常用的APP中的相冊效果,當前項目中已經實現了一些基本的功能和UI,在後續完善的過程中還會有所變動。項目在Github上開源,歡迎fork和star。先展示實現的效果(後面會增加拍照功能):
單選效果 單選未選擇時的效果 單選已選擇的效果


功能分析
在實現相冊功能之前,我們先需要明確它的邏輯。參照QQ、新浪、微博這中巨頭級的APP,當我們需要用選擇圖片時,會先打開相冊,獲取到最新的照片列表。然後點擊一個按鈕可以展開相冊列表,點擊列表內容,可以切換相冊,刷新當前照片列表中的內容。而且選擇這篇的時候,會有單選、多選、單選並裁剪等情況,多選的時候還要出現選擇效果和指示器等,單選的時候如果需要裁剪則進入裁剪頁,不裁剪則默認確定選擇,(拍照功能在後續博客中再說明)。
這樣,我們就可以明確我們需要實現的功能有:
1.獲取手機中的最新圖片
2.獲取手機中的相冊列表
3.獲取制定相冊中的所有圖片
4.展示圖片和相冊
5.多圖選擇時需要有選擇效果和指示器
6.單選裁剪時需要用到裁剪功能
另外,掃描手機中的圖片也是一個相對耗時的工作,所以這個工作還需要主要避免放到主線程中。
准備數據
為了使用方便,我們可以將相冊列表的查詢、制定相冊的查詢、最新圖片的查詢都放到一個工具類中,主要工具類代碼如下:
public class AlbumTool {
private Handler handler;
//private Semaphore semaphore;
private Callback callback;
private Context context;
private final int TYPE_FOLDER=1;
private final int TYPE_ALBUM=2;
public AlbumTool(Context context){
this.context=context;
handler=new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
if(callback!=null){
switch (msg.what){
case TYPE_FOLDER:
callback.onFolderFinish((ImageFolder) msg.obj);
break;
case TYPE_ALBUM:
callback.onAlbumFinish((ArrayList<ImageFolder>) msg.obj);
break;
}
}
super.handleMessage(msg);
}
};
}
public void setCallback(Callback callback){
this.callback=callback;
}
public void findAlbumsAsync(){
new Thread(new Runnable() {
@Override
public void run() {
getAlbums(context);
}
}).start();
}
public void findFolderAsync(final ImageFolder folder){
new Thread(new Runnable() {
@Override
public void run() {
getFolder(context,folder);
}
}).start();
}
//獲取所有圖片集
private ArrayList<ImageFolder> getAlbums(Context context) {
ArrayList<ImageFolder> albums=new ArrayList<>();
albums.add(getNewestPhotos(context));
//利用ContentResolver查詢數據庫,找出所有包含圖片的文件夾,保存到相冊列表中
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{
MediaStore.Images.Media.DATA,
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.Media.DATE_MODIFIED,
"count(*) as count"
},
MediaStore.Images.Media.MIME_TYPE + "=? or " +
MediaStore.Images.Media.MIME_TYPE + "=? or " +
MediaStore.Images.Media.MIME_TYPE + "=?) " +
"group by (" + MediaStore.Images.ImageColumns.BUCKET_ID,
new String[]{"image/jpeg", "image/png", "image/jpg"},
MediaStore.Images.Media.DATE_MODIFIED + " desc");
if (cursor != null) {
while (cursor.moveToNext()) {
final File file = new File(cursor.getString(0));
ImageFolder imageFolder = new ImageFolder();
imageFolder.setDir(file.getParent());
imageFolder.setId(cursor.getString(1));
imageFolder.setFirstImagePath(cursor.getString(0));
String[] all=file.getParentFile().list(new FilenameFilter() {
private boolean e(String filename,String ends){
return filename.toLowerCase().endsWith(ends);
}
@Override
public boolean accept(File dir, String filename) {
return e(filename,".png") || e(filename,".jpg") || e(filename,"jpeg");
}
});
if(all!=null&&all.length>0){
imageFolder.setCount(all.length);
albums.add(imageFolder);
}
}
cursor.close();
}
sendMessage(TYPE_ALBUM,albums);
return albums;
}
//獲取《最新圖片》集
private ImageFolder getNewestPhotos(Context context) {
ImageFolder newestFolder=new ImageFolder();
newestFolder.setName(ChooserSetting.newestAlbumName);
ArrayList<ImageInfo> imageBeans = new ArrayList<>();
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED,
},
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=?",
new String[]{"image/jpeg", "image/png", "image/jpg"},
MediaStore.Images.Media.DATE_MODIFIED + " desc"
+ (ChooserSetting.newestAlbumSize < 0 ? ""
: (" limit " + ChooserSetting.newestAlbumSize)));
if (cursor != null){
while (cursor.moveToNext()) {
ImageInfo info=new ImageInfo();
info.path=cursor.getString(0);
info.displayName=cursor.getString(1);
info.time=cursor.getLong(2);
imageBeans.add(info);
}
cursor.close();
newestFolder.setFirstImagePath(imageBeans.get(0).path);
newestFolder.setDatas(imageBeans);
newestFolder.setCount(imageBeans.size());
}
sendMessage(TYPE_FOLDER,newestFolder);
return newestFolder;
}
//獲取具體圖片集,確保圖片數據已被查詢
private ImageFolder getFolder(Context context,ImageFolder folder) {
ContentResolver resolver = context.getContentResolver();
Cursor cursor;
if(folder!=null&&folder.getDatas()!=null&&folder.getDatas().size()>0){
sendMessage(TYPE_FOLDER,folder);
return folder;
}
if (folder == null) {
return getNewestPhotos(context);
} else {
cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED
},
MediaStore.Images.ImageColumns.BUCKET_ID + "=? and (" +
MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=? or "
+ MediaStore.Images.Media.MIME_TYPE + "=?) ",
new String[]{folder.getId(), "image/jpeg", "image/png", "image/jpg"},
MediaStore.Images.Media.DATE_MODIFIED + " desc");
}
ArrayList<ImageInfo> datas=new ArrayList<>();
folder.setDatas(datas);
if (cursor != null){
while (cursor.moveToNext()) {
ImageInfo info=new ImageInfo();
info.path=cursor.getString(0);
info.displayName=cursor.getString(1);
info.time=cursor.getLong(2);
datas.add(info);
}
cursor.close();
}
sendMessage(TYPE_FOLDER,folder);
return folder;
}
private void sendMessage(int what,Object obj){
Message msg=new Message();
msg.what=what;
msg.obj=obj;
handler.sendMessage(msg);
}
public interface Callback{
//文件夾查找完畢
void onFolderFinish(ImageFolder folder);
//成功搜索出所有的圖片集
void onAlbumFinish(ArrayList<ImageFolder> albums);
}
}
這樣,我們就可以利用這個工具類方便的獲取相冊列表、獲取制定相冊的圖片了(最新照片合集當做是一個相冊)。裡面主要就是使用ContentResolver來做查詢,Android入門級問題,四大組件——Activity、Service、ContentProvider和BroadcastReceiver,中的ContentProvider和ContentResolver就是一對CP了,ContentProvider用來提供數據,ContentResolver用來獲取數據。
展示相冊和相冊列表
有了獲取相冊列表和獲取指定相冊的方法,展示相冊和相冊列表就容易了,按照通常的方式,我們直接使用GridView來展示相冊,用ListView來展示相冊列表。當然,你也可以選擇使用RecyclerView來替代掉GridView和ListView,其實也都一樣。
顯示圖片直接使用成熟的第三方框架即可,我使用的是Glide。
值得注意的是,在相冊中,我們展示出來的圖片都是正方塊、並且需要三個(你也可以設置四個或者五個,只要你高興)鋪滿寬度。在這裡我使用的是比較懶的方式,直接用一個自定義的布局作為Item的跟布局,這個自定義布局繼承RelativeLayout,然後將復寫它的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
心有多懶,人就能有多懶。這樣它的高度就被強制保持為何寬度一致了。
選擇指示器
像QQ中,選擇圖片時,圖片會根據選擇的順序,在圖片上的那個圈圈裡面顯示出1234……等數字,然後取消選擇時,被選的數字會順序補位,比如你選了七張圖片、然後取消了顯示數字3的那張,這時4就變成3了、5變成了4、6變成了5。
像新浪微博中的圖片選擇,不會出現數字,而是出現一個勾,選中的時候這個勾還有動畫效果。
這樣的功能怎麼實現呢?
我實現的方式是,在每個Item中都有一個固定大小的View,根據圖片是否被選中,加載不同的Drawable。當然,寫這個項目既然是為了以後在不同的項目中使用,這個自然要方便被使用者自行設置。所以我寫一個抽象類:
public abstract class IChooseDrawable{
private Paint paint;
protected int width=0;
protected int height=0;
private SparseArray<Drawable> drawables;
public IChooseDrawable(){
paint=new Paint();
paint.setAntiAlias(true);
paint.setColor(0x88000000);
drawables=new SparseArray<>();
}
public Drawable get(int state){
if(drawables.indexOfKey(state)>=0){
return drawables.get(state);
}else{
InDrawable drawable=new InDrawable(state);
drawables.put(state,drawable);
return drawable;
}
}
public void clear(){
drawables.clear();
}
public int getBaseline(Paint paint,int top,int bottom){
Paint.FontMetrics i=paint.getFontMetrics();
return (int) ((bottom+top-i.top-i.bottom)/2);
}
//state表示第幾個被選擇,0表示未選中
public abstract void draw(Canvas canvas,Paint paint,int state);
private class InDrawable extends Drawable{
private int state=0;
InDrawable(int state){
this.state=state;
}
@Override
public void draw(@NonNull Canvas canvas) {
IChooseDrawable.this.draw(canvas,paint,state);
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
}
}
在相冊的Adapter的構造函數中會傳入一個IChooseDrawable實體,在顯示每個Item時,會根據當前狀態通過drawable.get(int state)取得指定的Drawable,設置為指示器View的背景。
上面效果圖中的指示器(也可配置為只顯示對號)實現為:
public class CircleChooseDrawable extends IChooseDrawable {
private boolean isShowNum=true;
private int chooseBgColor=0xFFFF6600;
private Path path;
public CircleChooseDrawable(){
super();
}
public CircleChooseDrawable(boolean isShowNum,int chooseBgColor){
super();
this.isShowNum=isShowNum;
this.chooseBgColor=chooseBgColor;
}
@Override
public void draw(Canvas canvas, Paint paint, int state) {
width=canvas.getWidth();
height=canvas.getHeight();
if(state==0){ //未選擇狀態
paint.setColor(0x55000000);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2,height/2,width/2-2,paint);
paint.setColor(0xDDFFFFFF);
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(width/2,height/2,width/2-2,paint);
}else{ //選中狀態
paint.setColor(chooseBgColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(width/2,height/2,width/2-2,paint);
paint.setColor(0xDDFFFFFF);
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(width/2,height/2,width/2-2,paint);
paint.setColor(0xDDFFFFFF);
if(isShowNum){ //顯示數字
paint.setStyle(Paint.Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(width*0.53f);
canvas.drawText(state+"",width/2,getBaseline(paint,0,height),paint);
}else{ //顯示一個√號
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paint.setStrokeCap(Paint.Cap.ROUND);
if(path==null){
path=new Path();
path.moveTo(width/4f,height/2f);
path.lineTo(width*2/5f,height*5/7f);
path.lineTo(width*3/4f,height/3f);
}
canvas.drawPath(path,paint);
}
}
}
}
裁剪、單選和多選
單選和多選的區別在於單選的時候,沒有選擇指示器,選中直接攜帶數據返回。而多選時,有選擇指示器,選擇完成後,需要確定後攜帶數據返回,在確定前可以取消之前所選的內容。
所以實現的時候,只需要判斷用戶傳入的選擇意圖,做出相應的處理。如果是裁剪,則選擇一張圖片後,進入到裁剪頁面,裁剪結束後攜帶裁剪結果返回到進入到相冊前的頁面。如果是單選,則選擇一張圖片後,直接攜帶數據返回到進入相冊前的頁面。如果是多選,則要在點擊確認按鈕後,攜帶數據返回到進入相冊前的頁面。裁剪的實現見上一篇博客——Android 圖片裁剪。
其他
其他的一些功能,主要是拍照的功能、和大圖切換預覽現在還未添加進項目中,目前准備是利用OpenGl做拍照預覽和拍照(也許會添加些許常用濾鏡),實現的相關細節也會在後續單獨寫博客來介紹。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
android 項目實戰——超級課程表課表一鍵提取功能
如果你是在校大學生,或許你用多了各種課程表,比如課程格子,超級課程表。它們都有一個共同點就是可以一鍵導入教務處的課程。那麼一直都是用戶的我們,沒有考慮過它是
Android中ListView綁定CheckBox實現全選增加和刪除功能(DEMO)
ListView控件還是挺復雜的,也是項目中應該算是比較常用的了,所以寫了一個小Demo來講講,主要是自定義adapter的用法,加了很多的判斷等等等等….我們先來看看實
Android 手勢鎖的實現 讓自己的應用更加安全吧
今天偶遇以github上gesturelock關於手勢鎖的一個例子(有興趣的去搜索下看看),於是下載下來研究,無奈基本沒有注釋,代碼上存在一些問題(當設置gravity=
Android:軟鍵盤擋住輸入框問題的終極解決方案
前言開發做得久了,總免不了會遇到各種坑。而在Android開發的路上,『軟鍵盤擋住了輸入框』這個坑,可謂是一個曠日持久的巨坑——來來來,我們慢慢看