android自定義控件之文件選擇

之前一直想找一個(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)該有許多地方可以用到,所以寫下這篇文章诈皿,方便以后參考查看林束。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市稽亏,隨后出現(xiàn)的幾起案子壶冒,更是在濱河造成了極大的恐慌,老刑警劉巖截歉,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胖腾,死亡現(xiàn)場離奇詭異,居然都是意外死亡瘪松,警方通過查閱死者的電腦和手機(jī)咸作,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宵睦,“玉大人记罚,你說我怎么就攤上這事】呛浚” “怎么了毫胜?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長诬辈。 經(jīng)常有香客問我酵使,道長,這世上最難降的妖魔是什么焙糟? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任口渔,我火速辦了婚禮,結(jié)果婚禮上穿撮,老公的妹妹穿的比我還像新娘缺脉。我一直安慰自己,他們只是感情好悦穿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布攻礼。 她就那樣靜靜地躺著,像睡著了一般栗柒。 火紅的嫁衣襯著肌膚如雪礁扮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機(jī)與錄音太伊,去河邊找鬼雇锡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛僚焦,可吹牛的內(nèi)容都是我干的锰提。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼芳悲,長吁一口氣:“原來是場噩夢啊……” “哼立肘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起名扛,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赛不,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后罢洲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡文黎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年惹苗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耸峭。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡桩蓉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出劳闹,到底是詐尸還是另有隱情院究,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布本涕,位于F島的核電站业汰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏菩颖。R本人自食惡果不足惜样漆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晦闰。 院中可真熱鬧放祟,春花似錦、人聲如沸呻右。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽声滥。三九已至眉撵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背执桌。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工鄙皇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仰挣。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓伴逸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親膘壶。 傳聞我的和親對象是個(gè)殘疾皇子错蝴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 6,365評論 0 17
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,090評論 1 32
  • 1颓芭、窗體 1顷锰、常用屬性 (1)Name屬性:用來獲取或設(shè)置窗體的名稱,在應(yīng)用程序中可通過Name屬性來引用窗體亡问。 ...
    Moment__格調(diào)閱讀 4,522評論 0 11
  • 一官紫、Python簡介和環(huán)境搭建以及pip的安裝 4課時(shí)實(shí)驗(yàn)課主要內(nèi)容 【Python簡介】: Python 是一個(gè)...
    _小老虎_閱讀 5,723評論 0 10
  • 個(gè)人學(xué)習(xí)批處理的初衷來源于實(shí)際工作;在某個(gè)迭代版本有個(gè)BS(安卓手游模擬器)大需求州藕,從而在測試過程中就重復(fù)涉及到...
    Luckykailiu閱讀 4,702評論 0 11