一、效果预览
用的图标都是网上到处拷贝的,仅仅做个示例使用。
- 截图
|
|
- gif
二、思路
- 首先我们需要一个 FilePickerActivity 去显示页面。里面包含一个标题栏(ToolBar)、路径文本(TextView)和文件列表(RecyclerView)。
- RecyclerView 需要使用一个 Adapter 展示内容,内容来自于 File 类的 listFiles() 函数。
- 最后我们完善那些返回、单击进入文件夹、长按选择文件、空页面显示等逻辑。
三、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());
}
}
}
}
七、工程地址
如果效果存在一些差异的地方,可能是系统的一些风格导致的。
完整的项目代码地址为: