Android文件游览选择器(支持选择多个文件、支持选择文件和文件夹,返回文件路径)

一、效果预览

用的图标都是网上到处拷贝的,仅仅做个示例使用。

  1. 截图
  1. gif

二、思路

  1. 首先我们需要一个 FilePickerActivity 去显示页面。里面包含一个标题栏(ToolBar)、路径文本(TextView)和文件列表(RecyclerView)。
  2. RecyclerView 需要使用一个 Adapter 展示内容,内容来自于 File 类的 listFiles() 函数。
  3. 最后我们完善那些返回、单击进入文件夹长按选择文件、空页面显示等逻辑。

三、FileAdapter

1. 通用基类 MyRecyclerAdapter<T>

RecyclerView 使用的适配器需要继承 RecyclerView.Adapter 类,先创建一个通用的父类继承 RecyclerView.Adapter 类,封装一些基本方法,以及设置 Item 的单击和长按事件。

参考这片文章:RecyclerView通用的Adapter封装

完整代码:

import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

import androidx.recyclerview.widget.RecyclerView;

public abstract class MyRecyclerAdapter<T> extends RecyclerView.Adapter<MyRecyclerAdapter.ViewHolder> {

    protected Context mContext;
    protected int mLayoutId;
    protected List<T> mList;

    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;

    public MyRecyclerAdapter() {}

    public MyRecyclerAdapter(Context context, List<T> list, int layoutId) {
        mContext = context;
        mList = list;
        mLayoutId = layoutId;
    }

    @Override
    public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
        ViewHolder viewHolder = ViewHolder.getInstance(mContext, mLayoutId, parent);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        if (mList.size() > 0)
            convert(holder, mList.get(position), position);
        if (mOnItemClickListener != null) {
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int position = holder.getLayoutPosition();
                    mOnItemClickListener.onItemClick(holder.itemView, position);
                }
            });
        }
        if (mOnItemLongClickListener != null) {
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    int position = holder.getLayoutPosition();
                    mOnItemLongClickListener.onItemLongClick(holder.itemView, position);
                    return true; // 返回true 表示消耗了事件 事件不会继续传递
                }
            });
        }
    }

    public abstract void convert(ViewHolder holder, T t, int position);

    @Override
    public int getItemCount() {
        return mList.size();
    }

    public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {
        this.mOnItemClickListener = mOnItemClickListener;
    }

    public void setOnItemLongClickListener(OnItemLongClickListener mOnItemLongClickListener) {
        this.mOnItemLongClickListener = mOnItemLongClickListener;
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }

    public interface OnItemLongClickListener {
        void onItemLongClick(View view, int position);
    }

    protected static class ViewHolder extends RecyclerView.ViewHolder {

        private SparseArray<View> mViews;
        private View mConvertView;

        public ViewHolder(View itemView) {
            super(itemView);
            mConvertView = itemView;
            mViews = new SparseArray<>();
        }

        public static ViewHolder getInstance(Context context, int layoutId, ViewGroup parent) {
            View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
            ViewHolder holder = new ViewHolder(itemView);
            return holder;
        }

        public <T extends View> T getView(int viewId) {
            View view = mViews.get(viewId);
            if (view == null) {
                view = mConvertView.findViewById(viewId);
                mViews.put(viewId, view);
            }
            return (T) view;
        }

    }

}

2. 实现类 FileAdapter

继承 MyRecyclerAdapter<T>,并实现具体的UI、逻辑等。

import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class FileAdapter extends MyRecyclerAdapter<File> implements MyRecyclerAdapter.OnItemClickListener,
        MyRecyclerAdapter.OnItemLongClickListener {

    private static final String TAG = "FileAdapter";
    private File mRootDir;
    private File mCurrentDir;
    private boolean[] mCheckedFlags;
    private boolean mChooseMode;
    private OnDirChangeListener mOnDirChangeListener;

    /* 文件名排序规则:文件夹在前,文件在后,按字母顺序*/
    private Comparator<File> mFileComparator = new Comparator<File>() {
        @Override
        public int compare(File file1, File file2) {
            if (file1.isDirectory() && file2.isFile()) {
                return -1;
            }
            if (file1.isFile() && file2.isDirectory()) {
                return 1;
            }
            return file1.getName().toLowerCase().compareTo(file2.getName().toLowerCase());
        }
    };

    /* 文件名过滤规则:不显示点号开头的文件*/
    private FilenameFilter mFilenameFilter = new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
            if (name.startsWith(".")) { //点号开头的文件一般都不需要
                return false;
            }
            return true;
        }
    };

    public FileAdapter(Context context, String rootPath) {
        mContext = context;
        mList = new ArrayList<>();
        mLayoutId = R.layout.item_file_list;
        setOnItemClickListener(this);
        setOnItemLongClickListener(this);

        mRootDir = new File(rootPath);
        mCurrentDir = mRootDir;
        updateList();
    }

    @Override
    public void convert(ViewHolder holder, File file, final int position) {
        ImageView fileIcon = holder.getView(R.id.item_file_icon);
        TextView filename = holder.getView(R.id.item_file_name);
        CheckBox checkBox = holder.getView(R.id.item_check_box);
        ImageView arrowIcon = holder.getView(R.id.item_arrow);

        int resId = file.isDirectory() ? R.mipmap.folder_style_yellow : R.mipmap.file_style_yellow;
        fileIcon.setImageResource(resId);
        filename.setText(file.getName());
        checkBox.setVisibility(mChooseMode ? View.VISIBLE : View.GONE);
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                mCheckedFlags[position] = isChecked;
            }
        });
        checkBox.setChecked(mCheckedFlags[position]);
        if (file.isDirectory() && !mChooseMode) {
            arrowIcon.setVisibility(View.VISIBLE);
        } else {
            arrowIcon.setVisibility(View.GONE);
        }
    }

    public boolean isChooseMode() {
        return mChooseMode;
    }

    public boolean isRootDir () {
        if (mRootDir == null || mCurrentDir == null) {
            return true;
        }
        return mRootDir.getAbsolutePath().equals(mCurrentDir.getAbsolutePath());
    }

    public void quitMode() {
        mChooseMode = false;
        Arrays.fill(mCheckedFlags, false);
        notifyDataSetChanged();
    }

    public void backParent() {
        if (isRootDir()) {
            Log.w(TAG, "can't back to parent dir, is root dir!");
            return;
        }
        mCurrentDir = mCurrentDir.getParentFile();
        updateList();
        if (mOnDirChangeListener != null) {
            mOnDirChangeListener.onDirChangeListener(mCurrentDir);
        }
    }

    public ArrayList<String> getChoosePaths() {
        if (!mChooseMode) {
            return null;
        }
        ArrayList<String> result = new ArrayList<>();
        for (int i = 0; i < mCheckedFlags.length; i++) {
            if (mCheckedFlags[i]) {
                result.add(mList.get(i).getAbsolutePath());
                Log.d(TAG, "add path: " + mList.get(i).getAbsolutePath());
            }
        }
        return result.size() > 0 ? result : null;
    }

    public void setOnDirChangeListener(OnDirChangeListener onDirChangeListener) {
        mOnDirChangeListener = onDirChangeListener;
    }

    @Override
    public void onItemClick(View view, int position) {
        if (mChooseMode) {
            mCheckedFlags[position] = !mCheckedFlags[position];
            CheckBox checkBox = view.findViewById(R.id.item_check_box);
            checkBox.setChecked(mCheckedFlags[position]);
        } else {
            File file = mList.get(position);
            if (file.isDirectory()) {
                mCurrentDir = new File(file.getAbsolutePath());
                updateList();
                if (mOnDirChangeListener != null) {
                    mOnDirChangeListener.onDirChangeListener(mCurrentDir);
                }
            }
        }
    }

    public void updateList() {
        File[] files = mCurrentDir.listFiles(mFilenameFilter);
        mList.clear();
        if (files != null && files.length > 0) {
            Arrays.sort(files, mFileComparator);
            mList.addAll(Arrays.asList(files));
            mCheckedFlags = new boolean[mList.size()];
        }
        notifyDataSetChanged();
    }

    @Override
    public void onItemLongClick(View view, int position) {
        if (!mChooseMode) {
            mChooseMode = true;
            mCheckedFlags[position] = true;
            notifyDataSetChanged();
        }
    }

    interface OnDirChangeListener {

        void onDirChangeListener(File currentDirectory);
    }

}

3. 布局文件 item_file_list.xml

布局整体还是很简单,item_file_icon 显示文件夹或者文件的图标,item_file_name 显示文件名,item_check_box 为选择框,仅在文件选择模式下可见,item_arrow 为文件夹后面的箭头。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:background="?attr/selectableItemBackground">

    <ImageView
        android:id="@+id/item_file_icon"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_margin="10dp"
        android:src="@mipmap/file_style_yellow" />

    <TextView
        android:id="@+id/item_file_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginRight="50dp" />

    <CheckBox
        android:id="@+id/item_check_box"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_marginRight="20dp"
        android:visibility="gone"/>

    <ImageView
        android:id="@+id/item_arrow"
        android:layout_width="12dp"
        android:layout_height="16dp"
        android:layout_marginRight="20dp"
        android:src="@mipmap/ic_arrow_right"/>
</LinearLayout>

四、EmptyRecyclerView

通常的 RecyclerView 当没有数据时是什么都不显示的,我们希望在内容为空时,有一个 Empty 页面提示用户。

我们可以通过实现 AdapterDataObserver 类,这是一个适配器数据的观察者,在它的回调里检查数据是否为空。

由于适配器默认已经有了一个 AdapterDataObserver 实现,所有我们需要在 setAdapter() 的时候,先注销掉旧的 AdapterDataObserver 实现,再注册我们自己实现的 AdapterDataObserver 子类。

最后,我们通过一个 setEmptyView() 来设置我们需要显示的 Empty 页面。

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.recyclerview.widget.RecyclerView;

public class EmptyRecyclerView extends RecyclerView {

    private View emptyView;
    private static final String TAG = "EmptyRecyclerView";

    final private AdapterDataObserver observer = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            checkIfEmpty();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            Log.i(TAG, "onItemRangeInserted" + itemCount);
            checkIfEmpty();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            checkIfEmpty();
        }
    };

    public EmptyRecyclerView(Context context) {
        super(context);
    }

    public EmptyRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public EmptyRecyclerView(Context context, AttributeSet attrs,
                             int defStyle) {
        super(context, attrs, defStyle);
    }

    private void checkIfEmpty() {
        if (emptyView != null && getAdapter() != null) {
            final boolean emptyViewVisible = getAdapter().getItemCount() == 0;
            emptyView.setVisibility(emptyViewVisible ? VISIBLE : GONE);
            setVisibility(emptyViewVisible ? GONE : VISIBLE);
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        final Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(observer);
        }
        super.setAdapter(adapter);
        if (adapter != null) {
            adapter.registerAdapterDataObserver(observer);
        }
        checkIfEmpty();
    }

    //设置没有内容时,提示用户的空布局
    public void setEmptyView(View emptyView) {
        this.emptyView = emptyView;
        checkIfEmpty();
    }
}

五、FilePickerActivity

显示我们的 RecyclerView,以及实现一些相应的事件控制。

import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import java.io.File;
import java.util.ArrayList;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;

public class FilePickerActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "FilePickerActivity";
    public static final String INTENT_EXTRA_CHOOSE_PATHS = "paths";

    private TextView mFilepathTv;
    private EmptyRecyclerView mRecyclerView;
    private FileAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_picker);
        initView();
    }

    private void initView() {
        findViewById(R.id.toolbar_left_iv).setOnClickListener(this);
        findViewById(R.id.toolbar_right_iv).setOnClickListener(this);
        findViewById(R.id.back_iv).setOnClickListener(this);

        mFilepathTv = findViewById(R.id.path_tv);
        mRecyclerView = findViewById(R.id.file_recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setItemAnimator(null);
        String path = Environment.getExternalStorageDirectory().getAbsolutePath();
        mFilepathTv.setText(path);
        mAdapter = new FileAdapter(this, path);
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setEmptyView(findViewById(R.id.empty_view));
        mAdapter.setOnDirChangeListener(new FileAdapter.OnDirChangeListener() {
            @Override
            public void onDirChangeListener(File currentDirectory) {
                mFilepathTv.setText(currentDirectory.getAbsolutePath());
            }
        });
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.toolbar_left_iv:
                chooseCancel();
                break;
            case R.id.toolbar_right_iv:
                chooseDone();
                break;
            case R.id.back_iv:
                onBackPressed();
                break;
        }
    }

    private void chooseCancel() {
        setResult(RESULT_CANCELED, null);
        finish();
    }

    private void chooseDone() {
        ArrayList<String> choosePaths = mAdapter.getChoosePaths();
        if (choosePaths == null || choosePaths.size() <= 0) {
            chooseCancel();
        }
        Log.d(TAG, "choosePaths length: " + choosePaths.size());
        Intent intent = new Intent();
        intent.putStringArrayListExtra(INTENT_EXTRA_CHOOSE_PATHS, choosePaths);
        setResult(RESULT_OK, intent);
        finish();
    }

    @Override
    public void onBackPressed() {
        if (mAdapter.isChooseMode()) {
            mAdapter.quitMode();
            return;
        }
        if (!mAdapter.isRootDir()) {
            mAdapter.backParent();
            return;
        }
        chooseCancel();
    }
}

布局页面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.appcompat.widget.Toolbar
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="?android:actionBarSize"
        android:background="@color/colorPrimary"
        app:contentInsetStart="0dp">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/toolbar_left_iv"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="10dp"
                android:padding="10dp"
                android:src="@mipmap/ic_close"
                android:scaleType="fitCenter"/>

            <TextView
                android:id="@+id/toolbar_title_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="File Picker"
                android:textColor="@android:color/white"
                android:textSize="22sp"
                android:textStyle="bold"/>

            <ImageView
                android:id="@+id/toolbar_right_iv"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="10dp"
                android:padding="10dp"
                android:src="@mipmap/ic_ok"
                android:scaleType="fitCenter"/>
        </RelativeLayout>
    </androidx.appcompat.widget.Toolbar>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:background="@android:color/white" >

        <ImageView
            android:id="@+id/back_iv"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:padding="10dp"
            android:scaleType="fitCenter"
            android:src="@mipmap/ic_back"/>

        <TextView
            android:id="@+id/path_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:textColor="@android:color/black"
            android:textSize="16sp" />
    </LinearLayout>

    <com.afei.filepicker.EmptyRecyclerView
        android:id="@+id/file_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <FrameLayout
        android:id="@+id/empty_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">

        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_gravity="center"
            android:scaleType="fitCenter"
            android:src="@mipmap/emptyimg"/>
    </FrameLayout>
</LinearLayout>

六、调用示例

首先我们得确保获取sdcard的权限,其次我们启动 FilePickerActivity,并在 onActivityResult() 方法中通过 Intent 获取我们选择的路径。

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.TextView;

import java.util.ArrayList;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_PERMISSION = 1000;
    private static final int REQUEST_FILE_PICKER = 1001;

    private final String[] PERMISSIONS = new String[] {
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        checkPermission();
        findViewById(R.id.file_picker_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityForResult(new Intent(MainActivity.this, FilePickerActivity.class), REQUEST_FILE_PICKER);
            }
        });
    }

    private boolean checkPermission() {
        for (int i = 0; i < PERMISSIONS.length; i++) {
            int state = ContextCompat.checkSelfPermission(this, PERMISSIONS[i]);
            if (state != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, PERMISSIONS, REQUEST_PERMISSION);
                return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION) {
            if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivityForResult(intent, REQUEST_PERMISSION);
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_PERMISSION && resultCode == RESULT_OK) {
            checkPermission();
        }
        if (requestCode == REQUEST_FILE_PICKER && resultCode == RESULT_OK && data != null) {
            ArrayList<String> paths = data.getStringArrayListExtra(FilePickerActivity.INTENT_EXTRA_CHOOSE_PATHS);
            if (paths != null || paths.size() > 0) {
                StringBuilder sb = new StringBuilder("Selected Files:\n\n");
                for (String path : paths) {
                    sb.append(path);
                    sb.append("\n");
                }
                TextView textView = findViewById(R.id.filepath_tv);
                textView.setText(sb.toString());
            }
        }
    }
}

七、工程地址

如果效果存在一些差异的地方,可能是系统的一些风格导致的。

完整的项目代码地址为:

https://gitee.com/afei_/FilePicker.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值