android自定义控件之文件选择

之前一直想找一个比较好的文件选择的第三方库,可是看了都不太满意。于是就自己做了一个。像这样的一个小的功能,做起来也不是什么难事。但是要做得好看,还是花了一些时间,但这都是值得的。

例图

不多说,先上图:

选择文件

列举当前目录下的所有文件,如果是选择目录,则不显示文件,如果是选择文件,则需要显示文件。

新建目录

新建目录,就是在当前路径下新建目录,同时新建后的目录要能够及时显示在文件列表中。

实现的功能

  • 文件选择
  • 目录选择
  • 可显示隐藏文件
  • 显示上一次打开目录
  • 显示上一级目录
  • 显示当前路径
  • 文件显示大小和修改时间
  • 目录显示子项数量和修改日期
  • 新建目录

难点和细节

1. android6.0以上版本动态权限请求

需要读写权限,添加第三方权限请求库:

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) {

        //去掉自带的标题
        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,是因为发现如果用ListView,内存会不断增加,很难降下来。

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("当前路径: " + 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. 目录跳转

这一部分逻辑有FileProvider类完成; 这里需要注意的是,有些手机不支持读取根目录,所以改为读取"/mnt/"作为根目录就行读取。

另外跳转目录都是改变当前路径,然后再刷新数据。

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 + "项 | " + 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;
    }

    ...

}

同时在其内部定义了FileData类:

public static class FileData {
        /**
         * 文件名称
         */
        public final String name;
        /**
         * 是否为文件夹
         */
        public final boolean isDir;
        /**
         * 真实路径
         */
        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. 文件选择

文件选择,可以通过当前路径路径以及列表索引来唯一确定路径;都是,当跳转目录后,索引应该重置。

这里采用WeakReference记录选择的控件,但选择其他目录或者文件时,之前的控件需要重置一下状态。

...
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或者视频,其中文件选择弹窗是自己写的。感觉这个弹出应该有许多地方可以用到,所以写下这篇文章,方便以后参考查看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,358评论 0 17
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 1、窗体 1、常用属性 (1)Name属性:用来获取或设置窗体的名称,在应用程序中可通过Name属性来引用窗体。 ...
    Moment__格调阅读 4,500评论 0 11
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,720评论 0 10
  • 个人学习批处理的初衷来源于实际工作;在某个迭代版本有个BS(安卓手游模拟器)大需求,从而在测试过程中就重复涉及到...
    Luckykailiu阅读 4,685评论 0 11