之前一直想找一個(gè)比較好的文件選擇的第三方庫,可是看了都不太滿意顺囊。于是就自己做了一個(gè)易稠。像這樣的一個(gè)小的功能,做起來也不是什么難事包蓝。但是要做得好看,還是花了一些時(shí)間企量,但這都是值得的测萎。
例圖
不多說,先上圖:
列舉當(dāng)前目錄下的所有文件届巩,如果是選擇目錄硅瞧,則不顯示文件,如果是選擇文件恕汇,則需要顯示文件腕唧。
新建目錄,就是在當(dāng)前路徑下新建目錄瘾英,同時(shí)新建后的目錄要能夠及時(shí)顯示在文件列表中枣接。
實(shí)現(xiàn)的功能
- 文件選擇
- 目錄選擇
- 可顯示隱藏文件
- 顯示上一次打開目錄
- 顯示上一級目錄
- 顯示當(dāng)前路徑
- 文件顯示大小和修改時(shí)間
- 目錄顯示子項(xiàng)數(shù)量和修改日期
- 新建目錄
難點(diǎn)和細(xì)節(jié)
1. android6.0以上版本動(dòng)態(tài)權(quán)限請求
需要讀寫權(quán)限,添加第三方權(quán)限請求庫:
dependencies {
...
implementation "com.yanzhenjie:permission:2.0.0-rc12"
}
使用:
AndPermission.with(this)
.runtime()
.permission(Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE)
.onGranted(data1 -> {
new FileChooserDialog()
.setSelectType(FileProvider.TYPE_DIR)
.setOnFileSelectedListener((path -> {
// todo
}))
.show(getSupportFragmentManager());
}).start();
2.文件選擇 彈窗繼承自DialogFragment缺谴,文件列采用RecyclerView
DialogFragment與Dialog有一些不同的地方但惶,其中show方法需要傳入FragmentManager
另外需在onCreateVie方法初始化布局,以及獲取到控件
public class FileChooserDialog extends DialogFragment{
private final static String TAG = "FileChooserDialog";
private int selectIndex = -1;
private int selectType = FileProvider.TYPE_DIR;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//去掉自帶的標(biāo)題
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.dialog_file_chooser, container);
...
return view;
}
public void show(FragmentManager manager) {
super.show(manager, TAG);
}
...
}
另外就是RecycleView湿蛔,之所以采用RecycleView膀曾,是因?yàn)榘l(fā)現(xiàn)如果用ListView,內(nèi)存會(huì)不斷增加阳啥,很難降下來添谊。
RecyclerView rvFile;
CommonAdapter<FileProvider.FileData> adapter;
...
public void initData() {
rvFile.setLayoutManager(new LinearLayoutManager(this.getContext(), LinearLayoutManager.VERTICAL, false));
mFileProvider = FileProvider.newInstance(getOldPath(), selectType);
adapter = new CommonAdapter<>(getContext(), mFileProvider.list(), R.layout.item_list_file, this::initListItem);
rvFile.setAdapter(adapter);
mTvCurPath.setText("當(dāng)前路徑: " + mFileProvider.getCurPath());
}
其中CommonAdapter繼承自BaseAdapter,是通用的Adapter,兼容ListView:
public abstract class BaseAdapter<T> extends RecyclerView.Adapter<CommonHolder> implements ListAdapter, SpinnerAdapter {
protected final List<T> data;
private final DataSetObservable mDataSetObservable = new DataSetObservable();
@Override
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public void notifyDataChanged() {
mDataSetObservable.notifyChanged();
notifyDataSetChanged();
}
...
}
3. 目錄跳轉(zhuǎn)
這一部分邏輯有FileProvider類完成; 這里需要注意的是察迟,有些手機(jī)不支持讀取根目錄斩狱,所以改為讀取"/mnt/"作為根目錄就行讀取。
另外跳轉(zhuǎn)目錄都是改變當(dāng)前路徑卷拘,然后再刷新數(shù)據(jù)喊废。
public final class FileProvider implements Iterable<FileProvider.FileData> {
public static final int TYPE_DIR = 1;
public static final int TYPE_FILE = 2;
private final String mRootPath;
private final int mType;
private final String mOldPath;
private String curPath;
private boolean mFilter;
private List<FileData> mFileDataList;
public static FileProvider newInstance(String oldPath, int type) {
File rootFile = new File("/");
if (rootFile.exists() && rootFile.list() != null) {
return new FileProvider(type, oldPath, rootFile.getPath());
} else {
rootFile = new File("/mnt/");
if (rootFile.exists() && rootFile.list() != null) {
return new FileProvider(type, oldPath, rootFile.getPath());
} else {
throw new UnsupportedOperationException("");
}
}
}
private FileProvider(int type, String oldPath, String rootPath) {
this.mType = type;
this.mOldPath = oldPath;
this.mRootPath = rootPath;
this.curPath = mRootPath;
this.mFileDataList = new ArrayList<>();
this.mFilter = true;
this.setData();
}
public List<FileData> refresh() {
setData();
return mFileDataList;
}
public List<FileData> setFilter(boolean filter) {
this.mFilter = filter;
setData();
return mFileDataList;
}
public FileData getFileData(File file, FilenameFilter filter, String info) {
boolean isDir = file.isDirectory();
return new FileData(
file.getName(),
isDir,
file.getPath(),
mType == (isDir ? 1 : 2),
info);
}
private String getSizeStr(long size) {
if (size >= 1024 * 1024 * 1024) {
return String.format("%.2f G", (float) size / 1073741824L);
} else if (size >= 1024 * 1024) {
return String.format("%.2f M", (float) size / 1048576L);
} else if (size >= 1024) {
return String.format("%.2f K", (float) size / 1024);
}
return size + "B";
}
@SuppressLint("SimpleDateFormat")
private void setData() {
this.mFileDataList.clear();
FilenameFilter filenameFilter = (dir, name) -> !name.startsWith(".");
File[] files = mFilter ? new File(curPath).listFiles(filenameFilter) : new File(curPath).listFiles();
if (files != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
for (File file : files) {
boolean isDir = file.isDirectory();
String info;
if (isDir) {
int size = 0;
String[] names = mFilter ? file.list(filenameFilter) : file.list();
if (names != null) {
size = names.length;
}
// if (mType == TYPE_FILE && size == 0) continue;
info = size + "項(xiàng) | " + dateFormat.format(new Date(file.lastModified()));
mFileDataList.add(getFileData(file, filenameFilter, info));
} else if (mType == TYPE_FILE) {
info = getSizeStr(file.length()) +
" | " +
dateFormat.format(new Date(file.lastModified()));
mFileDataList.add(getFileData(file, filenameFilter, info));
}
}
}
Collections.sort(mFileDataList, (o1, o2) -> {
if (o1.isDir == o2.isDir) return o1.name.compareTo(o2.name);
return o2.isDir ? 1 : -1;
});
if (isRoot()) {
if (mOldPath != null && !mOldPath.equals(mRootPath)) {
File oldFile = new File(mOldPath);
if (oldFile.exists()) {
mFileDataList.add(0, new FileData(oldFile.getName(), true, oldFile.getPath(), false, "[上次打開目錄] " + oldFile.getPath()));
}
}
} else {
String realPath = new File(curPath).getParent();
mFileDataList.add(0, new FileData("../", true, realPath, false, "[返回上一級] " + realPath));
}
}
public boolean isRoot() {
return curPath.equalsIgnoreCase(mRootPath);
}
public List<FileData> gotoParent() {
if (!isRoot()) {
curPath = new File(curPath).getParent();
setData();
}
return mFileDataList;
}
public List<FileData> gotoChild(int position) {
if (position >= 0 && position < mFileDataList.size() && mFileDataList.get(position).isDir) {
curPath = mFileDataList.get(position).realPath;
}
setData();
return mFileDataList;
}
public FileData getItem(int position) {
return mFileDataList.get(position);
}
public int size() {
return mFileDataList.size();
}
public String getCurPath() {
return curPath;
}
...
}
同時(shí)在其內(nèi)部定義了FileData類:
public static class FileData {
/**
* 文件名稱
*/
public final String name;
/**
* 是否為文件夾
*/
public final boolean isDir;
/**
* 真實(shí)路徑
*/
public final String realPath;
/**
* 是否可選擇
*/
public final boolean selectable;
/**
* 文件信息
*/
public final String info;
public FileData(String name, boolean isDir, String realPath, boolean selectable, String info) {
this.name = name;
this.isDir = isDir;
this.realPath = realPath;
this.selectable = selectable;
this.info = info;
}
...
}
4. 文件選擇
文件選擇,可以通過當(dāng)前路徑路徑以及列表索引來唯一確定路徑栗弟;都是污筷,當(dāng)跳轉(zhuǎn)目錄后,索引應(yīng)該重置。
這里采用WeakReference記錄選擇的控件瓣蛀,但選擇其他目錄或者文件時(shí)陆蟆,之前的控件需要重置一下狀態(tài)。
...
private void initListItem(CommonHolder holder, FileProvider.FileData data, int position) {
holder.setText(R.id.txt_path, data.name);
holder.setItemOnClickListener(v -> {
if (data.name.equals("../")) {
selectIndex = -1;
refreshData(mFileProvider.gotoParent());
} else {
selectIndex = -1;
refreshData(mFileProvider.gotoChild(position));
}
});
holder.setText(R.id.txt_info, data.info);
if (data.isDir) {
holder.setSrc(R.id.img_file, R.drawable.ic_wenjian);
holder.setVisible(R.id.img_back, View.VISIBLE);
} else {
holder.setSrc(R.id.img_file, R.drawable.ic_file);
holder.setVisible(R.id.img_back, View.GONE);
}
CheckBox checkBox = holder.getView(R.id.checkBox3);
if (checkBox != null) {
checkBox.setVisibility(data.selectable ? View.VISIBLE : View.GONE);
checkBox.setTag(position);
checkBox.setChecked(selectIndex == position);
if (selectIndex == position) {
weakCheckBox = new WeakReference<>(checkBox);
}
checkBox.setOnCheckedChangeListener(this);
}
}
...
5. 源碼地址
https://github.com/xiaoyifan6/videocreator
該源碼主要用于圖片合成gif或者視頻惋增,其中文件選擇彈窗是自己寫的叠殷。感覺這個(gè)彈出應(yīng)該有許多地方可以用到,所以寫下這篇文章诈皿,方便以后參考查看林束。