Android相冊開發(fā)實戰(zhàn)!(附源碼)
安卓進階漲薪訓練營,讓一部分人先進大廠
大家好,我是皇叔,最近開了安卓進階漲薪訓練營第二期,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關,進入心儀的公司。
詳情見文章:皇叔安卓訓練營第二期開啟啦!
作者:呼嘯長風
https://juejin.cn/user/2999123450531982
序
我之前發(fā)布了個圖片加載框架,在JCenter關閉后,“閉關修煉”,想著改好了出個2.0版本。后來覺得僅增加功能和改進實現(xiàn)不夠,得補充一下用例。相冊列表的加載就是很好的用例,然后在Github找了一圈,沒有找到滿意的,有的甚至好幾年沒維護了,于是就自己寫了一個。
代碼鏈接:
https://github.com/BillyWei01/EasyAlbum
相比于圖片加載,相冊加載在Github上要多很多。其原因大概是圖片加載的input/output比較規(guī)范,不涉及UI布局;而相冊則不然,幾乎每個APP都會有自己獨特的需求,有自己的UI風格。因此,相冊庫很難做到通用于大部分APP。我所實現(xiàn)的這個也一樣,并非以實現(xiàn)通用的相冊組件為目的,而是作為一個樣例,以供參考。
需求描述
網(wǎng)上不少相冊的開源庫,都是照微信相冊來搭的界面,我也是跟著這么做吧,要是說涉及侵權(quán)什么的,那些前輩應該先比我收到通知……
主要是自己也不會UI設計,不找個參照對象怕實現(xiàn)的太難看。話說回來,要是真的涉及侵權(quán),請聯(lián)系我處理。相冊所要實現(xiàn)的功能,概括來說,就是顯示相冊列表,點擊縮略圖選中,點擊完成結(jié)束選擇,返回選擇結(jié)果。需求細節(jié),包括但不限于以下列表:
-
實現(xiàn)目錄列表,相冊列表,預覽頁面; -
支持單選/多選; -
支持顯示選擇順序和限定選擇數(shù)量; -
支持自定義篩選條件; -
支持自定義目錄排序; -
支持“原圖”選項; -
支持再次進入相冊時傳入已經(jīng)選中的圖片/視頻; -
支持切換出APP外拍照或刪除照片后,回到相冊時自動刷新;
API設計
EasyAlbum.config()
.setImageLoader(GlideImageLoader)
.setDefaultFolderComparator { o1, o2 -> o1.name.compareTo(o2.name)}
public interface ImageLoader {
void loadPreview(MediaData data, ImageView imageView, boolean asBitmap);
void loadThumbnail(MediaData data, ImageView imageView, boolean asBitmap);
}
private val priorityFolderComparator = Comparator<Folder> { o1, o2 ->
val priorityFolder = "Camera"
if (o1.name == priorityFolder) -1
else if (o2.name == priorityFolder) 1
else o1.name.compareTo(o2.name)
}
EasyAlbum.from(this)
.setFilter(TestMediaFilter(option))
.setSelectedLimit(selectLimit)
.setOverLimitCallback(overLimitCallback)
.setSelectedList(mediaAdapter?.getData())
.setAllString(option.text)
.enableOriginal()
.start { result ->
mediaAdapter?.setData(result.selectedList)
}
public class EasyAlbum {
public static AlbumRequest from(@NonNull Context context) {
return new AlbumRequest(context);
}
}
public final class AlbumRequest {
private WeakReference<Context> contextRef;
AlbumRequest(Context context) {
this.contextRef = new WeakReference<>(context);
}
// ...其他參數(shù)..
public void start(ResultCallback callback) {
Session.init(this, callback, selectedList);
if (contextRef != null) {
Context context = contextRef.get();
if (context != null) {
context.startActivity(new Intent(context, AlbumActivity.class));
}
contextRef = null;
}
}
}
-
通過intent傳參數(shù)到AlbumActivity, 用startActivityForResult啟動,通過onActivityResult接收。 -
通過靜態(tài)變量傳遞參數(shù),通過Callback回調(diào)結(jié)果。
final class Session {
static AlbumRequest request;
static AlbumResult result;
private static ResultCallback resultCallback;
static void init(AlbumRequest req, ResultCallback callback, List<MediaData> selectedList) {
request = req;
resultCallback = callback;
result = new AlbumResult();
if (selectedList != null) {
result.selectedList.addAll(selectedList);
}
}
static void clear() {
if (request != null) {
request.clear();
request = null;
resultCallback = null;
result = null;
}
}
}
媒體文件加載
public final Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder,
CancellationSignal cancellationSignal) {
}
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
private static final Uri CONTENT_URI = MediaStore.Files.getContentUri("external");
private static final String TYPE_SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
+ " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
+ ")";
private static final String[] PROJECTIONS = new String[]{
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.Images.Media.ORIENTATION
};
public final class MediaData implements Comparable<MediaData> {
private static final String BASE_VIDEO_URI = "content://media/external/video/media/";
private static final String BASE_IMAGE_URI = "content://media/external/images/media/";
static final byte ROTATE_UNKNOWN = -1;
static final byte ROTATE_NO = 0;
static final byte ROTATE_YES = 1;
public final boolean isVideo;
public final int mediaId;
public final String parent;
public final String name;
public final long modifiedTime; // in seconds
public String mime;
long fileSize;
int duration;
int width;
int height;
byte rotate = ROTATE_UNKNOWN;
public String getPath() {
return parent + name;
}
public Uri getUri() {
String baseUri = isVideo ? BASE_VIDEO_URI : BASE_IMAGE_URI;
return Uri.parse(baseUri + mediaId);
}
public int getRealWidth() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? width : height;
}
public int getRealHeight() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? height : width;
}
// ......
}
int count = cursor.getCount();
List<MediaData> list = new ArrayList<>(count);
while (cursor.moveToNext()) {
String path = cursor.getString(IDX_DATA);
String parent = parentPool.getOrAdd(Utils.getParentPath(path));
String name = Utils.getFileName(path);
String mime = mimePool.getOrAdd(cursor.getString(IDX_MIME_TYPE));
// ......
}
public int getWidth() {
return rotate != ROTATE_YES ? width : height;
}
public int getHeight() {
return rotate != ROTATE_YES ? height : width;
}
public int getDuration() {
if (isVideo && duration == 0) {
checkData();
}
return duration;
}
void checkData() {
if (!hadFillData) {
FutureTask<Boolean> future = new FutureTask<>(this::fillData);
try {
// Limit the time for filling extra info, in case of ANR.
AlbumConfig.getExecutor().execute(future);
future.get(300, TimeUnit.MILLISECONDS);
} catch (Throwable ignore) {
}
}
}
private static List<Folder> makeResult(AlbumRequest request) {
AlbumRequest.MediaFilter filter = request.filter;
ArrayList<MediaData> totalList = new ArrayList<>(mediaCache.size());
if (filter == null) {
totalList.addAll(mediaCache.values());
} else {
// 根據(jù)filter過濾MediaData
for (MediaData item : mediaCache.values()) {
if (filter.accept(item)) {
totalList.add(item);
}
}
}
// 先對所有MediaData排序,后面分組后就不需要繼續(xù)在分組內(nèi)排序了
// 因為分組時是按順序放到分組列表的。
Collections.sort(totalList);
Map<String, ArrayList<MediaData>> groupMap = new HashMap<>();
for (MediaData item : totalList) {
String parent = item.parent;
ArrayList<MediaData> subList = groupMap.get(parent);
if (subList == null) {
subList = new ArrayList<>();
groupMap.put(parent, subList);
}
subList.add(item);
}
final List<Folder> result = new ArrayList<>(groupMap.size() + 1);
for (Map.Entry<String, ArrayList<MediaData>> entry : groupMap.entrySet()) {
String folderName = Utils.getFileName(entry.getKey());
result.add(new Folder(folderName, entry.getValue()));
}
// 對目錄排序
Collections.sort(result, request.folderComparator);
// 最后,總列表放在最前
result.add(0, new Folder(request.getAllString(), totalList));
return result;
}
public interface MediaFilter {
boolean accept(MediaData media);
// To identify the filter
String tag();
}
相冊列表
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private final int n; // 列的數(shù)量
private final int space; // 列與列之間的間隔
private final int part; // 每一列應該分攤多少間隔
public GridItemDecoration(int n, int space) {
this.n = n;
this.space = space;
// 總間隔:space * (n - 1) ,等分n份
part = space * (n - 1) / n;
}
@Override
public void getItemOffsets(
@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
int position = parent.getChildLayoutPosition(view);
int i = position % n;
// 第i列(0開始)的左邊部分的間隔的計算公式:space * i / n
outRect.left = Math.round(part * i / (float) (n - 1));
outRect.right = part - outRect.left;
outRect.top = 0;
outRect.bottom = space;
}
}
-
第1個item的left=0px, right = 3px; -
第2個item的left=1px, right = 2px; -
第3個item的left=2px, right =1px; -
第4個item的left=3px, right =0px。
outRect.left = column == 0 ? 0 : space / 2;
outRect.right = column == (n - 1) ? 0 : space / 2;
后序
相冊的實現(xiàn)可簡單可復雜,我見過的最簡單的實現(xiàn)是直接在主線程查詢媒體數(shù)據(jù)庫的……本文從各個方面分享了一些相冊實現(xiàn)的經(jīng)驗,尤其是相冊加載部分。目前這個時代,手機存幾千上萬張圖片是很常見的,優(yōu)化好相冊的加載,能提升不少用戶體驗。項目已發(fā)布到Github和Maven Central。
https://github.com/BillyWei01/EasyAlbum
implementation 'io.github.billywei01:easyalbum:1.0.6'

為了防止失聯(lián),歡迎關注我防備的小號
微信改了推送機制,真愛請星標本公號??
