Android文件挑选器的完结
前语
此项目和之前发布的项目有些不同,之前都是根本的功用,不是基于 Activity 页面完结的,而相似文件挑选,图片挑选,除了功用的完结还需求处理 UI 相关的装备。
在前面的【怎么操作文件的挑选】 一文中我就想把逻辑做一下封装,做成开箱即用的文件挑选器,本来这功用是项目中自用的,UI 等都是自有的,假如要做开源出去,那么就要抽取功用与 UI 逻辑,设置可装备选项。
分化一下完结步骤,怎么自界说一个文件下载器呢?
- 咱们需求装备 Activity 根本的 Theme,动画,状况栏,导航栏等处理。
- 咱们需求装备展现的文本巨细,回来图标,列表与导航栏的文本巨细等等。
- 然后咱们对XML的布局并构建导航列表与文件列表的数据适配器等。
- 然后咱们就能够处理权限以及对文件的操作了。
- 能够运用战略形式不同的版别不同的完结办法。
- 过滤操作是比不可少的,咱们获取文件之后运用过滤操作展现咱们想要的文件。
这样差不多就能完结一个根本的操作文件挑选结构了。
这儿先放完结之后各版别手机的截图,详细作用如下:
Android 7.0 作用(华为):
Android 9.0 作用(谷歌):
Android 12 作用(三星):
Android 13 作用(Vivo):
结构完结基于 target 31,不装备兼容形式 requestLegacyExternalStorage ,支撑 4.4 及以上系统,可保持UI的一致性…
话不多说,赶忙开始吧!
一、文件挑选的页面的装备
咱们运用咱们自界说的theme与动画即可。因为咱们要自己完结可控的标题栏,所以咱们的样式不需求toolbar:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/choose_file_app_blue</item>
<item name="colorPrimaryDark">@color/choose_file_app_blue</item>
<item name="colorAccent">@color/choose_file_app_blue</item>
<item name="android:windowAnimationStyle">@style/My_AnimationActivity</item>
<item name="android:windowIsTranslucent">false</item>
</style>
<style name="My_AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/open_enter</item>
<item name="android:activityCloseExitAnimation">@anim/close_exit</item>
</style>
</resources>
为了可装备的状况栏与导航栏,这儿我用到之前的项目中的 StatusBarHost 结构,详细的完结与细节能够查看之前的文章,【传送门】。
那么咱们创立挑选文件的Activity大致如下:
class ChooseFileActivity : AppCompatActivity(), View.OnClickListener {
private val mViewModel: ChooseFileViewModel by lazy {
ViewModelProvider(this, ChooseFileViewModelFactory()).get(ChooseFileViewModel::class.java)
}
private var mainHandler = Handler(Looper.getMainLooper())
//展现当时页面的UI风格
private val uiConfig = ChooseFile.config?.mUIConfig ?: ChooseFileUIConfig.Builder().build()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_choose_file)
StatusBarHost.inject(this)
.setStatusBarBackground(uiConfig.statusBarColor)
.setStatusBarWhiteText()
.setNavigationBarBackground(uiConfig.navigationBarColor)
.setNavigatiopnBarIconBlack()
}
而为了横竖屏切换的作用,或许说为了适配折叠屏设备,咱们能够运用ViewModel保存一些页面状况:
class ChooseFileViewModel : ViewModel() {
val mNavPathList = arrayListOf<ChooseFileInfo>()
var mNavAdapter: FileNavAdapter? = null
val mFileList = arrayListOf<ChooseFileInfo>()
var mFileListAdapter: FileListAdapter? = null
//根目录
val rootPath = Environment.getExternalStorageDirectory().absolutePath
var rootChoosePos = 0 //根目录文档选中的索引
//当时挑选的途径
var mCurPath = Environment.getExternalStorageDirectory().absolutePath
}
这儿现已用到了一些UI的装备选项,咱们赶忙接下来往下走。
二、页面的UI装备与其他装备
一般咱们都会依据不同的UI作用,设置不同的文本色彩和布景,所以咱们需求把页面上的文本与布景和图标等选项抽取出来,装备成可选的特点:
public class ChooseFileUIConfig {
private int statusBarColor; //状况栏色彩
private int titleBarBgColor; //标题栏的布景色彩
private int titleBarBackRes; //标题栏的回来按钮资源
private int titleBarTitleColor; //标题栏的标题文字色彩
private int titleBarTitleSize; //标题栏的标题文字巨细(sp)
private int navigationBarColor; //底部导航栏色彩
private int fileNavBarTextColor; //文件导航栏的文本色彩
private int fileNavBarTextSize; //文件导航栏的文本巨细
private int fileNavBarArrowIconRes; //文件导航栏的箭头图标资源
private int fileNameTextColor; //文件(夹)称号字体色彩
private int fileNameTextSize; //文件(夹)称号字体巨细(sp)
private int fileInfoTextColor; //文件(夹)提示信息字体巨细
private int fileInfoTextSize; //文件(夹)提示信息字体巨细(sp)
private ChooseFileUIConfig() {
}
...
}
然后咱们运用构建者形式创立可选的装备,假如不挑选那么就能够运用默许的装备,就特别合适此场景:
public static class Builder {
private int statusBarColor = Color.parseColor("#0689FB"); //状况栏色彩
private int titleBarBgColor = Color.parseColor("#0689FB"); //标题栏的布景色彩
private int titleBarBackRes = R.drawable.cf_back; //标题栏的回来按钮资源
private int titleBarTitleColor = Color.parseColor("#FFFFFF"); //标题栏的标题文字色彩
private int titleBarTitleSize = 20; //标题栏的标题文字巨细(sp)
private int navigationBarColor = Color.parseColor("#F7F7FB"); //底部导航栏色彩
private int fileNavBarTextColor = Color.parseColor("#333333"); //文件导航栏的文本色彩
private int fileNavBarTextSize = 15; //文件导航栏的文本巨细
private int fileNavBarArrowIconRes = R.drawable.cf_next; //文件导航栏的箭头图标资源
private int fileNameTextColor = Color.BLACK; //文件(夹)称号字体色彩
private int fileNameTextSize = 16; //文件(夹)称号字体巨细(sp)
private int fileInfoTextColor = Color.parseColor("#A9A9A9"); //文件(夹)提示信息字体巨细
private int fileInfoTextSize = 14; //文件(夹)提示信息字体巨细(sp)
public Builder() {
}
public Builder statusBarColor(int statusBarColor) {
this.statusBarColor = statusBarColor;
return this;
}
...
UI的装备完结之后,咱们还需求对一些常规的装备做一些可选操作,例如线程池的自界说,过滤文件的挑选等等。
class ChooseFileConfig(private val chooseFile: ChooseFile) {
internal var mUIConfig: ChooseFileUIConfig? = null
internal var mIFileTypeFilter: IFileTypeFilter? = null
internal var mExecutor: ExecutorService? = ThreadPoolExecutor(
1, 1, 10L, TimeUnit.MINUTES, LinkedBlockingDeque()
)
fun setUIConfig(uiConfig: ChooseFileUIConfig?): ChooseFileConfig {
mUIConfig = uiConfig
return this
}
fun setExecutor(executor: ExecutorService): ChooseFileConfig {
mExecutor = executor
return this
}
fun getExecutor(): ExecutorService? {
return mExecutor
}
fun setTypeFilter(filter: IFileTypeFilter): ChooseFileConfig {
mIFileTypeFilter = filter
return this
}
fun forResult(listener: IFileChooseListener) {
val activity = chooseFile.activityRef?.get()
activity?.gotoActivityForResult<ChooseFileActivity> {
it?.run {
val info = getSerializableExtra("chooseFile") as ChooseFileInfo
listener.doChoose(info)
}
}
}
//毁掉资源
fun clear() {
mUIConfig = null
mIFileTypeFilter = null
if (mExecutor != null && !mExecutor!!.isShutdown) {
mExecutor!!.shutdown()
}
}
}
因为操作文件是耗时的操作,咱们最好是在线程中进行,咱们一致运用默许的线程池处理,假如用户想自界说运用能够他自己的线程池。
而 forResult 的完结咱们是对 startActivityForResult 的封装,为了兼容低版别内部是 Ghost 完结。
而内部运用到的 ChooseFile 则是咱们的单例运用进口,内部完结如下:
object ChooseFile {
@JvmField
internal var activityRef: WeakReference<FragmentActivity>? = null
@JvmField
internal var config: ChooseFileConfig? = null
@JvmStatic
fun create(activity: FragmentActivity): ChooseFileConfig {
activityRef?.clear()
this.activityRef = WeakReference(activity)
config = ChooseFileConfig(this)
return config!!
}
@JvmStatic
fun create(fragment: Fragment): ChooseFileConfig {
activityRef?.clear()
val activity = fragment.requireActivity()
this.activityRef = WeakReference(activity)
config = ChooseFileConfig(this)
return config!!
}
@JvmStatic
fun release() {
activityRef?.clear()
config?.clear()
config = null
}
}
到处咱们就能够正常的运用结构了:
findViewById<Button>(R.id.btn_get_file).setOnClickListener {
ChooseFile.create(this)
.setUIConfig(ChooseFileUIConfig.Builder().build())
.setTypeFilter { listData ->
return@setTypeFilter ArrayList(listData.filter { item ->
//只需文件夹
item.isDir
//只需文档文件
// item.fileType == ChooseFile.FILE_TYPE_FOLDER ||
// item.fileType == ChooseFile.FILE_TYPE_TEXT ||
// item.fileType == ChooseFile.FILE_TYPE_PDF
})
}
.forResult {
Toast.makeText(this, "选中的文件:" + it?.fileName, Toast.LENGTH_SHORT).show()
val uri = Uri.parse(it?.filePathUri)
val fis = contentResolver.openInputStream(uri)
Log.w("TAG", "文件的Uri:" + it?.filePathUri + " uri:" + uri + " fis:" + fis)
fis?.close()
}
}
这样拉到列表的底部之后就只会显现文件夹类型:
三、导航列表与文件列表的展现
对应文件列表的展现以及文件导航的展现,咱们需求先界说对应的xml:
代码咱们都会,作用如下图:
那么RV的处理如下:
private fun initRV() {
mViewModel.mNavAdapter = FileNavAdapter(mViewModel.mNavPathList, uiConfig)
rvNav.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
rvNav.adapter = mViewModel.mNavAdapter
mViewModel.mNavAdapter?.setOnNavClickListener { position ->
val item = mViewModel.mNavPathList[position]
mViewModel.mCurPath = item.filePath
startRefreshAnim()
obtainByPath(mViewModel.mCurPath)
}
mViewModel.mFileListAdapter = FileListAdapter(mViewModel.mFileList, uiConfig)
rvFiles.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
rvFiles.adapter = mViewModel.mFileListAdapter
mViewModel.mFileListAdapter?.setOnFileListClickListener() { position ->
val item = mViewModel.mFileList[position]
if (item.isDir) {
//设置当时Root的选中
if (mViewModel.mNavPathList.isEmpty()) {
mViewModel.rootChoosePos = (rvFiles.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
//文件夹-直接改写页面
mViewModel.mCurPath = item.filePath
startRefreshAnim()
obtainByPath(mViewModel.mCurPath)
} else {
//选中文件-回调出去
setResult(-1, Intent().putExtra("chooseFile", item))
finish()
}
}
}
Adapter的处理也很简略,咱们把UI的装备挑选传进来,然后做赋值操作即可,咱们最好是只做赋值操作,处理的逻辑都在文件的处理那一边处理,那儿是有子线程一并处理的。
class FileNavAdapter(private val navPathList: MutableList<ChooseFileInfo>, private val uiConfig: ChooseFileUIConfig) :
RecyclerView.Adapter<FileNavAdapter.FileNavViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileNavViewHolder {
val itemView = View.inflate(parent.context, R.layout.item_choose_file_nav, null)
return FileNavViewHolder(itemView)
}
override fun onBindViewHolder(holder: FileNavViewHolder, position: Int) {
holder.curPosition = position
holder.tvPath.text = navPathList[position].fileName
holder.tvPath.setTextColor(uiConfig.fileNavBarTextColor)
holder.tvPath.setTextSize(TypedValue.COMPLEX_UNIT_SP, uiConfig.fileNameTextSize.toFloat())
holder.ivPathSegment.setImageResource(uiConfig.fileNavBarArrowIconRes)
if (position == (itemCount - 1)) holder.ivPathSegment.visibility = View.INVISIBLE
else holder.ivPathSegment.visibility = View.VISIBLE
}
override fun getItemCount(): Int = navPathList.size
inner class FileNavViewHolder(private val itemView: View) : ViewHolder(itemView) {
val tvPath: TextView = itemView.findViewById(R.id.tv_root)
val ivPathSegment: ImageView = itemView.findViewById(R.id.iv_path_segment)
var curPosition: Int = 0
init {
itemView.setOnClickListener {
mListener?.onClick(curPosition)
}
}
}
private var mListener: OnNavClickListener? = null
fun setOnNavClickListener(listener: OnNavClickListener) {
mListener = listener
}
fun interface OnNavClickListener {
fun onClick(position: Int)
}
}
两个 Adapter 的完结作用是相似的,就不多贴代码,有爱好能够去文章末尾找源码。
关于展现的Item的Bean目标,咱们需求运用自界说的 File 封装,作为展现的选项。咱们需求对文件进行读取之后直接封装到这个 Bean 目标中,方便直接展现。
public class ChooseFileInfo implements Serializable {
public String fileName;
public boolean isDir; //是否是文件夹
public String fileSize; //假如是文件夹则表明子目录项数,假如不是文件夹则表明文件巨细,当值为-1的时分不显现
public String fileLastUpdateTime; //最终操作事情
public String filePath; //文件的途径
public String filePathUri; //文件的途径,URI形式
public String fileType; //文件类型
public int fileTypeIconRes; //文件类型对应的图标展现
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChooseFileInfo that = (ChooseFileInfo) o;
return Objects.equals(filePath, that.filePath);
}
@Override
public int hashCode() {
return Objects.hash(filePath);
}
}
需求留意的是咱们需求处理文件夹的选中与顶部文件导航的交互,两个RV选中之后需求有数据的逻辑处理。
底部的 RV 选中文件夹之后需求给顶部的文件导航增加数据,而顶部的文件导航选中之后需求改写底部的 RV 选中:
底部 RV 的选中:
mViewModel.mFileListAdapter?.setOnFileListClickListener() { position ->
val item = mViewModel.mFileList[position]
if (item.isDir) {
//设置当时Root的选中
if (mViewModel.mNavPathList.isEmpty()) {
mViewModel.rootChoosePos = (rvFiles.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
//文件夹-直接改写页面
mViewModel.mCurPath = item.filePath
startRefreshAnim()
obtainByPath(mViewModel.mCurPath)
} else {
//选中文件-回调出去
setResult(-1, Intent().putExtra("chooseFile", item))
finish()
}
}
顶部 RV 的选中:
mViewModel.mNavAdapter?.setOnNavClickListener { position ->
val item = mViewModel.mNavPathList[position]
mViewModel.mCurPath = item.filePath
startRefreshAnim()
obtainByPath(mViewModel.mCurPath)
}
加载数据完结之后的顶部导航展现逻辑:
//顶部文件导航的设置
private fun setTopNavSelect(topInfo: ChooseFileInfo?) {
if (topInfo != null) {
if (mViewModel.mNavPathList.isEmpty()) {
mViewModel.mNavPathList.add(topInfo)
} else {
val index = mViewModel.mNavPathList.indexOf(topInfo)
if (index >= 0) {
mViewModel.mNavPathList.subList(index + 1, mViewModel.mNavPathList.size).clear()
} else {
mViewModel.mNavPathList.add(topInfo)
}
}
} else {
mViewModel.mNavPathList.clear()
}
mViewModel.mNavAdapter?.notifyDataSetChanged()
}
四、权限处理与文件的操作
到此,UI的部分就大致完结了,咱们需求对数据与权限的逻辑做处理,咱们为了演示之前文章中 FilrProvider 与 DocumentsProvider 的运用,这儿用做高版别的作为展现。
首要咱们需求处理动态权限问题,分为不同的版别的权限恳求完结:
public class PermissionUtil {
//一致处理权限
public static boolean isStoragePermissionGranted(Activity activity) {
Context context = activity.getApplicationContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivityForResult(intent, 1);
return false;
} else {
// 有外部存储的权限
return true;
}
} else {
int readPermissionCheck = ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE);
int writePermissionCheck = ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (readPermissionCheck == PackageManager.PERMISSION_GRANTED
&& writePermissionCheck == PackageManager.PERMISSION_GRANTED) {
Log.v("permission", "Permission is granted");
return true;
} else {
Log.v("permission", "Permission is revoked");
ActivityCompat.requestPermissions(activity, new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
return false;
}
}
}
}
那么在 Activity 的权限回调中咱们需求处理成功的回调:
//动态权限授权的回调
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
obtainByPath(mViewModel.rootPath)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
// 用户现已授权,履行需求拜访外部存储的操作
obtainByPath(mViewModel.rootPath)
} else {
// 用户未授权,无法拜访外部存储
Toast.makeText(this, "未授权,无法拜访外部存储", Toast.LENGTH_SHORT).show()
}
}
}
关于文件的类型处理,咱们运用东西类封装一下,大致的逻辑是依据文件的后缀名匹配。而且对文件的 mimeType 做了匹配,大致代码如下:
// Audio
public static final int FILE_TYPE_MP3 = 1;
public static final int FILE_TYPE_M4A = 2;
public static final int FILE_TYPE_WAV = 3;
public static final int FILE_TYPE_AMR = 4;
public static final int FILE_TYPE_AWB = 5;
public static final int FILE_TYPE_WMA = 6;
public static final int FILE_TYPE_OGG = 7;
private static final int FIRST_AUDIO_FILE_TYPE = 0;
private static final int LAST_AUDIO_FILE_TYPE = 10;
// MIDI
public static final int FILE_TYPE_MID = 11;
public static final int FILE_TYPE_SMF = 12;
public static final int FILE_TYPE_IMY = 13;
private static final int FIRST_MIDI_FILE_TYPE = 10;
private static final int LAST_MIDI_FILE_TYPE = 20;
// Video
public static final int FILE_TYPE_MP4 = 21;
public static final int FILE_TYPE_M4V = 22;
public static final int FILE_TYPE_3GPP = 23;
public static final int FILE_TYPE_3GPP2 = 24;
public static final int FILE_TYPE_WMV = 25;
private static final int FIRST_VIDEO_FILE_TYPE = 20;
private static final int LAST_VIDEO_FILE_TYPE = 30;
// Image
public static final int FILE_TYPE_JPEG = 31;
public static final int FILE_TYPE_GIF = 32;
public static final int FILE_TYPE_PNG = 33;
public static final int FILE_TYPE_BMP = 34;
public static final int FILE_TYPE_WBMP = 35;
private static final int FIRST_IMAGE_FILE_TYPE = 30;
private static final int LAST_IMAGE_FILE_TYPE = 40;
// Playlist
public static final int FILE_TYPE_M3U = 41;
public static final int FILE_TYPE_PLS = 42;
public static final int FILE_TYPE_WPL = 43;
private static final int FIRST_PLAYLIST_FILE_TYPE = 40;
private static final int LAST_PLAYLIST_FILE_TYPE = 50;
//TEXT
public static final int FILE_TYPE_TXT = 51;
public static final int FILE_TYPE_DOC = 52;
public static final int FILE_TYPE_RTF = 53;
public static final int FILE_TYPE_LOG = 54;
public static final int FILE_TYPE_CONF = 55;
public static final int FILE_TYPE_SH = 56;
public static final int FILE_TYPE_XML = 57;
public static final int FILE_TYPE_DOCX = 58;
private static final int FIRST_TEXT_FILE_TYPE = 50;
private static final int LAST_TEXT_FILE_TYPE = 60;
//XLS
public static final int FILE_TYPE_XLS = 61;
public static final int FILE_TYPE_XLSX = 62;
private static final int FIRST_XLS_FILE_TYPE = 60;
private static final int LAST_XLS_FILE_TYPE = 70;
//PPT
public static final int FILE_TYPE_PPT = 71;
public static final int FILE_TYPE_PPTX = 72;
private static final int FIRST_PPT_FILE_TYPE = 70;
private static final int LAST_PPT_FILE_TYPE = 80;
//PDF
public static final int FILE_TYPE_PDF = 81;
private static final int FIRST_PDF_FILE_TYPE = 80;
private static final int LAST_PDF_FILE_TYPE = 90;
//静态内部类
static class MediaFileType {
int fileType;
String mimeType;
MediaFileType(int fileType, String mimeType) {
this.fileType = fileType;
this.mimeType = mimeType;
}
}
private static HashMap<String, MediaFileType> sFileTypeMap
= new HashMap<>();
private static HashMap<String, Integer> sMimeTypeMap
= new HashMap<>();
static void addFileType(String extension, int fileType, String mimeType) {
sFileTypeMap.put(extension, new MediaFileType(fileType, mimeType));
sMimeTypeMap.put(mimeType, fileType);
}
static {
//依据文件后缀名匹配
addFileType("MP3", FILE_TYPE_MP3, "audio/mpeg");
addFileType("M4A", FILE_TYPE_M4A, "audio/mp4");
addFileType("WAV", FILE_TYPE_WAV, "audio/x-wav");
addFileType("AMR", FILE_TYPE_AMR, "audio/amr");
addFileType("AWB", FILE_TYPE_AWB, "audio/amr-wb");
addFileType("WMA", FILE_TYPE_WMA, "audio/x-ms-wma");
addFileType("OGG", FILE_TYPE_OGG, "application/ogg");
addFileType("MID", FILE_TYPE_MID, "audio/midi");
addFileType("XMF", FILE_TYPE_MID, "audio/midi");
addFileType("RTTTL", FILE_TYPE_MID, "audio/midi");
addFileType("SMF", FILE_TYPE_SMF, "audio/sp-midi");
addFileType("IMY", FILE_TYPE_IMY, "audio/imelody");
addFileType("MP4", FILE_TYPE_MP4, "video/mp4");
addFileType("M4V", FILE_TYPE_M4V, "video/mp4");
addFileType("3GP", FILE_TYPE_3GPP, "video/3gpp");
addFileType("3GPP", FILE_TYPE_3GPP, "video/3gpp");
addFileType("3G2", FILE_TYPE_3GPP2, "video/3gpp2");
addFileType("3GPP2", FILE_TYPE_3GPP2, "video/3gpp2");
addFileType("WMV", FILE_TYPE_WMV, "video/x-ms-wmv");
addFileType("JPG", FILE_TYPE_JPEG, "image/jpeg");
addFileType("JPEG", FILE_TYPE_JPEG, "image/jpeg");
addFileType("GIF", FILE_TYPE_GIF, "image/gif");
addFileType("PNG", FILE_TYPE_PNG, "image/png");
addFileType("BMP", FILE_TYPE_BMP, "image/x-ms-bmp");
addFileType("WBMP", FILE_TYPE_WBMP, "image/vnd.wap.wbmp");
addFileType("M3U", FILE_TYPE_M3U, "audio/x-mpegurl");
addFileType("PLS", FILE_TYPE_PLS, "audio/x-scpls");
addFileType("WPL", FILE_TYPE_WPL, "application/vnd.ms-wpl");
addFileType("TXT", FILE_TYPE_TXT, "text/plain");
addFileType("RTF", FILE_TYPE_RTF, "application/rtf");
addFileType("LOG", FILE_TYPE_LOG, "text/plain");
addFileType("CONF", FILE_TYPE_CONF, "text/plain");
addFileType("SH", FILE_TYPE_SH, "text/plain");
addFileType("XML", FILE_TYPE_XML, "text/plain");
addFileType("DOC", FILE_TYPE_DOC, "application/msword");
addFileType("DOCX", FILE_TYPE_DOCX, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
addFileType("XLS", FILE_TYPE_XLS, "application/vnd.ms-excel application/x-excel");
addFileType("XLSX", FILE_TYPE_XLSX, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
addFileType("PPT", FILE_TYPE_PPT, "application/vnd.ms-powerpoint");
addFileType("PPTX", FILE_TYPE_PPTX, "application/vnd.openxmlformats-officedocument.presentationml.presentation");
addFileType("PDF", FILE_TYPE_PDF, "application/pdf");
StringBuilder builder = new StringBuilder();
for (String s : sFileTypeMap.keySet()) {
if (builder.length() > 0) {
builder.append(',');
}
builder.append(s);
}
sFileExtensions = builder.toString();
}
public static final String UNKNOWN_STRING = "<unknown>";
public static boolean isAudioFileType(int fileType) {
return ((fileType >= FIRST_AUDIO_FILE_TYPE &&
fileType <= LAST_AUDIO_FILE_TYPE) ||
(fileType >= FIRST_MIDI_FILE_TYPE &&
fileType <= LAST_MIDI_FILE_TYPE));
}
public static boolean isVideoFileType(int fileType) {
return (fileType >= FIRST_VIDEO_FILE_TYPE &&
fileType <= LAST_VIDEO_FILE_TYPE);
}
public static boolean isImageFileType(int fileType) {
return (fileType >= FIRST_IMAGE_FILE_TYPE &&
fileType <= LAST_IMAGE_FILE_TYPE);
}
public static boolean isPlayListFileType(int fileType) {
return (fileType >= FIRST_PLAYLIST_FILE_TYPE &&
fileType <= LAST_PLAYLIST_FILE_TYPE);
}
public static boolean isTextFileType(int fileType) {
return (fileType >= FIRST_TEXT_FILE_TYPE &&
fileType <= LAST_TEXT_FILE_TYPE);
}
public static boolean isXLSFileType(int fileType) {
return (fileType >= FIRST_XLS_FILE_TYPE &&
fileType <= LAST_XLS_FILE_TYPE);
}
public static boolean isPPTFileType(int fileType) {
return (fileType >= FIRST_PPT_FILE_TYPE &&
fileType <= LAST_PPT_FILE_TYPE);
}
public static boolean isPDFFileType(int fileType) {
return (fileType >= FIRST_PDF_FILE_TYPE &&
fileType <= LAST_PDF_FILE_TYPE);
}
public static MediaFileType getFileType(String path) {
int lastDot = path.lastIndexOf(".");
if (lastDot < 0)
return null;
return sFileTypeMap.get(path.substring(lastDot + 1).toUpperCase());
}
//依据视频文件途径判断文件类型
public static boolean isVideoFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isVideoFileType(type.fileType);
}
return false;
}
//依据音频文件途径判断文件类型
public static boolean isAudioFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isAudioFileType(type.fileType);
}
return false;
}
//依据图片文件途径判断文件类型
public static boolean isImageFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isImageFileType(type.fileType);
}
return false;
}
//依据文本文件途径判断文件类型
public static boolean isTextFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isTextFileType(type.fileType);
}
return false;
}
public static boolean isXLSFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isXLSFileType(type.fileType);
}
return false;
}
public static boolean isPPTFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isPPTFileType(type.fileType);
}
return false;
}
public static boolean isPDFFileType(String path) {
MediaFileType type = getFileType(path);
if (null != type) {
return isPDFFileType(type.fileType);
}
return false;
}
接下来咱们就能在获取文件的时分,处理好队友的格局,赋值对应展现的Icon,就能够在数据适配器上面展现了。
五、不同版别的文件获取
其实获取到对应版别权限之后,都运用 File 就能够获取到对应版别的文件信息了,这儿便于演示,所以把 Android10 以上与 Android10 以下区别开来,高版别的运用 DocumentProvider的办法完结:
运用接口+战略的办法,咱们界说不同的完结方案:
internal interface IChooseFilePolicy {
fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit)
}
低版别的直接获取 FileList ,留意咱们处理文件,赋值操作等都是耗时操作,所以咱们最好是在线程池中处理,大致的逻辑如下:
internal class ChooseFileLowPolicy : IChooseFilePolicy {
override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
ChooseFile.config?.mExecutor?.execute {
val listData: ArrayList<ChooseFileInfo> = ArrayList()
val rootFile = File(rootPath)
var topInfo: ChooseFileInfo? = null
val rootExternalPath = Environment.getExternalStorageDirectory().absolutePath
if (rootExternalPath != rootPath) {
//增加一个顶部的导航目标
topInfo = ChooseFileInfo().apply {
fileName = rootFile.name
filePath = rootFile.absolutePath
isDir = true
}
}
val listFiles = rootFile.listFiles()
if (listFiles.isNullOrEmpty()) {
//空数据回调
callback(listData, topInfo)
return@execute
}
for (file in listFiles) {
if (file.isDirectory) {
//假如是文件夹
listData.add(
ChooseFileInfo().apply {
isDir = true
fileName = file.name
filePath = file.absolutePath
fileLastUpdateTime = TimeUtil.getDateInString(Date(file.lastModified()))
fileSize = "共" + FileUtil.getSubfolderNum(file.absolutePath) + "项"
fileType = ChooseFile.FILE_TYPE_FOLDER
fileTypeIconRes = R.drawable.file_folder
}
)
} else {
//依据后缀类型封装自界说文件Bean
val fileInfo = ChooseFileInfo()
if (FileUtil.isAudioFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_AUDIO
fileInfo.fileTypeIconRes = R.drawable.file_audio
} else if (FileUtil.isImageFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_IMAGE
fileInfo.fileTypeIconRes = R.drawable.file_image
} else if (FileUtil.isVideoFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_VIDEO
fileInfo.fileTypeIconRes = R.drawable.file_video
} else if (FileUtil.isTextFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_TEXT
fileInfo.fileTypeIconRes = R.drawable.file_text
} else if (FileUtil.isXLSFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_XLS
fileInfo.fileTypeIconRes = R.drawable.file_excel
} else if (FileUtil.isPPTFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_PPT
fileInfo.fileTypeIconRes = R.drawable.file_ppt
} else if (FileUtil.isPDFFileType(file.absolutePath)) {
fileInfo.fileType = ChooseFile.FILE_TYPE_PDF
fileInfo.fileTypeIconRes = R.drawable.file_pdf
} else {
fileInfo.fileType = ChooseFile.FILE_TYPE_Unknown
fileInfo.fileTypeIconRes = R.drawable.file_unknown
}
fileInfo.apply {
isDir = false
fileName = file.name
filePath = file.absolutePath
filePathUri = getFileUri(ChooseFile.activityRef?.get(), file).toString()
fileLastUpdateTime = TimeUtil.getDateInString(Date(file.lastModified()))
fileSize = FileUtil.getFileSize(file.length())
}
listData.add(fileInfo)
}
}
//满数据回调
callback(filterData, topInfo)
}
}
}
Android 10以上的高版别咱们发动 DocumentProvider 的查询办法:
internal class ChooseFileHighPolicy : IChooseFilePolicy {
@SuppressLint("Range")
override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
val uri = DocumentsContract.buildChildDocumentsUri(
"com.newki.choosefile.authorities",
rootPath
)
ChooseFile.config?.mExecutor?.execute {
val cursor = ChooseFile.activityRef?.get()?.contentResolver?.query(uri, null, null, null, null)
val listData: ArrayList<ChooseFileInfo> = ArrayList()
var topInfo: ChooseFileInfo? = null
if (cursor != null) {
while (cursor.moveToNext()) {
val isTop = cursor.getInt(cursor.getColumnIndex("isTop"))
val isRoot = cursor.getInt(cursor.getColumnIndex("isRoot"))
val fileName = cursor.getString(cursor.getColumnIndex("fileName"))
val isDir = cursor.getInt(cursor.getColumnIndex("isDir"))
val fileSize = cursor.getString(cursor.getColumnIndex("fileSize"))
val fileLastUpdateTime = cursor.getString(cursor.getColumnIndex("fileLastUpdateTime"))
val filePath = cursor.getString(cursor.getColumnIndex("filePath"))
val filePathUri = cursor.getString(cursor.getColumnIndex("filePathUri"))
val fileTypeIconRes = cursor.getInt(cursor.getColumnIndex("fileTypeIconRes"))
if (isTop == 1) {
if (isRoot == 0) {
topInfo = ChooseFileInfo().apply {
this.fileName = fileName
this.isDir = isDir != 0
this.fileSize = fileSize
this.fileLastUpdateTime = fileLastUpdateTime
this.filePath = filePath
this.filePathUri = filePathUri
this.fileTypeIconRes = fileTypeIconRes
}
}
} else {
listData.add(ChooseFileInfo().apply {
this.fileName = fileName
this.isDir = isDir != 0
this.fileSize = fileSize
this.fileLastUpdateTime = fileLastUpdateTime
this.filePath = filePath
this.filePathUri = filePathUri
this.fileTypeIconRes = fileTypeIconRes
})
}
}
cursor.close()
//满数据回调
callback(filterData, topInfo)
} else {
callback(emptyList(), null)
}
}
}
}
而 DocumentProvider 的详细完结如下,咱们只需求要点关注 queryChildDocuments 办法的完结即可:
public class ChooseFileDocumentProvider extends DocumentsProvider {
private final static String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{"isTop", "isRoot", "fileName", "isDir", "fileSize", "fileLastUpdateTime",
"filePath", "filePathUri", "fileType", "fileTypeIconRes"};
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
return null;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
// 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, new File(documentId), false, false);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
// 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项。
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = new File(parentDocumentId);
boolean isDirectory = parent.isDirectory();
boolean canRead = parent.canRead();
File[] files = parent.listFiles();
boolean isRoot = parent.getAbsolutePath().equals(Environment.getExternalStorageDirectory().getAbsolutePath());
includeFile(result, parent, isRoot, true);
//遍历增加处理文件列表
if (isDirectory && canRead && files != null && files.length > 0) {
for (File file : files) {
// 增加文件的名字, 类型, 巨细等特点
includeFile(result, file, isRoot, false);
}
}
return result;
}
private void includeFile(final MatrixCursor result, final File file, boolean isRoot, boolean isTop) {
final MatrixCursor.RowBuilder row = result.newRow();
row.add("isTop", isTop ? "1" : "0");
row.add("isRoot", isRoot ? "1" : "0");
if (file.isDirectory()) {
row.add("fileName", file.getName());
row.add("isDir", 1);
row.add("fileSize", "共" + FileUtil.getSubfolderNum(file.getAbsolutePath()) + "项");
row.add("fileLastUpdateTime", TimeUtil.getDateInString(new Date(file.lastModified())));
row.add("filePath", file.getAbsolutePath());
row.add("filePathUri", file.getAbsolutePath());
row.add("fileType", ChooseFile.FILE_TYPE_FOLDER);
row.add("fileTypeIconRes", R.drawable.file_folder);
} else {
row.add("fileName", file.getName());
row.add("isDir", 0);
row.add("fileSize", FileUtil.getFileSize(file.length()));
row.add("fileLastUpdateTime", TimeUtil.getDateInString(new Date(file.lastModified())));
row.add("filePath", file.getAbsolutePath());
row.add("filePathUri", getFileUri(ChooseFile.activityRef.get(), file).toString());
setFileType(row, file.getAbsolutePath());
}
}
private void setFileType(MatrixCursor.RowBuilder row, String absolutePath) {
if (FileUtil.isAudioFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_AUDIO);
row.add("fileTypeIconRes", R.drawable.file_audio);
} else if (FileUtil.isImageFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_IMAGE);
row.add("fileTypeIconRes", R.drawable.file_image);
} else if (FileUtil.isVideoFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_VIDEO);
row.add("fileTypeIconRes", R.drawable.file_video);
} else if (FileUtil.isTextFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_TEXT);
row.add("fileTypeIconRes", R.drawable.file_text);
} else if (FileUtil.isXLSFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_XLS);
row.add("fileTypeIconRes", R.drawable.file_excel);
} else if (FileUtil.isPPTFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_PPT);
row.add("fileTypeIconRes", R.drawable.file_ppt);
} else if (FileUtil.isPDFFileType(absolutePath)) {
row.add("fileType", ChooseFile.FILE_TYPE_PDF);
row.add("fileTypeIconRes", R.drawable.file_pdf);
} else {
row.add("fileType", ChooseFile.FILE_TYPE_Unknown);
row.add("fileTypeIconRes", R.drawable.file_unknown);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
return null;
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException {
return null;
}
@Override
public boolean onCreate() {
return true;
}
}
记得要在清单文件中注册哦:
<provider
android:name=".provider.ChooseFileDocumentProvider"
android:authorities="com.newki.choosefile.authorities"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
为了地址的可达性,对应 7.0以上的版别咱们最好是供给到 Uri 的资源,所以咱们界说到自己的 FileProvider ,而咱们只用到了外置 SD 卡的资源,所以咱们直接这么装备即可:
<provider
android:name=".provider.ChooseFileProvider"
android:authorities="com.newki.choosefile.file.path.share"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/choose_file_paths" />
</provider>
关于 FileProvider 的细节运用能够看我的这一篇文章【别滥用FileProvider了,Android中FileProvider的各种场景运用】
运用起来的话,就都是这么固定的写法:
private Uri getFileUri(Context context, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return ChooseFileProvider.getUriForFile(context, "com.newki.choosefile.file.path.share", file);
} else {
return Uri.fromFile(file);
}
}
六、过滤的操作
关于咱们的运用来说,咱们只需求选中SD卡中的文档文件,Txt,word,pdf等文件,那么咱们就一定是需求过滤的操作的。
因为在上文咱们获取File,封装自界说的 Bean 目标 ChooseFileInfo 中咱们现已把文件的自界说格局界说好了,所以咱们再回调之前先进行过滤操作,然后在再排序之后回来最终的数据源即可。
而为了对过滤的信息进行更灵活的过滤,咱们能够直接露出 ChooseFileInfo 目标,这样咱们甚至能依据文件类型,文件称号,文件最终操作时刻等等的办法进行过滤了。
先界说一个过滤的笼统接口如下:
public interface IFileTypeFilter {
List<ChooseFileInfo> doFilter(List<ChooseFileInfo> list);
}
在 FileConfig 的装备中,咱们能够加上过滤的接口处理逻辑
class ChooseFileConfig(private val chooseFile: ChooseFile) {
internal var mIFileTypeFilter: IFileTypeFilter? = null
fun setTypeFilter(filter: IFileTypeFilter): ChooseFileConfig {
mIFileTypeFilter = filter
return this
}
...
}
咱们在最终回来的时分就能够这样:
override fun getFileList(rootPath: String, callback: (fileList: List<ChooseFileInfo>, topInfo: ChooseFileInfo?) -> Unit) {
ChooseFile.config?.mExecutor?.execute {
// ... 获取文件操作
//依据Filter过滤数据并排序
val filterData = ChooseFile.config?.mIFileTypeFilter?.doFilter(listData) ?: listData
FileUtil.SortFilesByInfo(filterData)
//满数据回调
callback(filterData, topInfo)
}
}
而排序的逻辑就是先展现文件夹,然后依据文件名排序:
public static void SortFilesByInfo(List<ChooseFileInfo> fileList) {
Collections.sort(fileList, (o1, o2) -> {
if (o1.isDir && (!o2.isDir))
return -1;
if ((!o1.isDir) && o2.isDir)
return 1;
return Collator.getInstance(java.util.Locale.CHINA).compare(o1.fileName, o2.fileName);
});
}
到此咱们的全体的根本结构就完结了。
七、运用与上传
先看看详细的运用办法:
findViewById<Button>(R.id.btn_get_file).setOnClickListener {
ChooseFile.create(this)
.setUIConfig(ChooseFileUIConfig.Builder().build())
.setTypeFilter { listData ->
return@setTypeFilter ArrayList(listData.filter { item ->
//只需文件夹
// item.isDir
//只需文档文件
item.fileType == ChooseFile.FILE_TYPE_FOLDER ||
item.fileType == ChooseFile.FILE_TYPE_TEXT ||
item.fileType == ChooseFile.FILE_TYPE_PDF
})
}
.forResult {
Toast.makeText(this, "选中的文件:" + it?.fileName, Toast.LENGTH_SHORT).show()
val uri = Uri.parse(it?.filePathUri)
val fis = contentResolver.openInputStream(uri)
Log.w("TAG", "文件的Uri:" + it?.filePathUri + " uri:" + uri + " fis:" + fis)
fis?.close()
}
}
这儿咱们能够拿到path或uri,拿到 uri 之后咱们能够直接获取输入流,上传到后端服务器,例如:
public interface ApiService {
@Multipart
@POST("upload")
Call<ResponseBody> upload(@Part("text") String text, @Part("file") RequestBody requestBody);
}
// 创立OkHttpClient实例
OkHttpClient client = new OkHttpClient();
// 构建恳求体
RequestBody fileRequestBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/octet-stream");
}
@Override
public long contentLength() {
try {
// 回来输入流的长度,假如无法确定长度,回来-1
return inputStream.available();
} catch (IOException e) {
return -1;
}
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
// 将输入流中的数据写入到恳求体中
Source source = Okio.source(inputStream);
sink.writeAll(source);
}
};
// 创立Retrofit实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.example.com/")
.client(client)
.build();
// 创立ApiService实例
ApiService apiService = retrofit.create(ApiService.class);
// 构造恳求参数
String text = "Hello World!";
// 发送恳求并获取响应
Call<ResponseBody> call = apiService.upload(text, fileRequestBody);
Response<ResponseBody> response = call.execute();
下面看看视频的演示:
后记
咱们界说的文件挑选结构只是一个简略的轻量级结构,甚至都没有加入多选文件的操作,创立文件、修正文件的操作等。为什么?只因咱们没这方面的需求罢了。
多选文件的操作只需求修正一些UI和一些选中的逻辑罢了并不杂乱,创立修正文件则需求 DocumentFile 配合 SAF 的操作才干兼容高版别,略微杂乱一些,之前的文章也讲过,假如咱们有爱好也能够自行完结。究竟我也只需求一个文件挑选的功用罢了,不想过渡封装。
相似的结构,咱们除了做一些文件挑选的功用,相似的图片挑选也能够采用相似的结构完结,只是获取图片的办法不同罢了。
经过本文咱们能够了解File的运用,权限的恳求,FileProvider的运用,以及要点的DocumentProvider,咱们重写并完整的了解了怎么的运用。
接下来放出源码供咱们参考与指正,【传送门】。
本文发布之时也现已传到 MavenCentral 了,如有要求能够直接依靠,地址如下:
implementation “com.gitee.newki123456:android_choose_file:1.0.0”
内部的依靠库版别并不高,appcompat:1.2.0 ,recyclerview:1.1.0 ,swiperefreshlayout:1.1.0 。最低支撑到 4.4 版别,默许 target 为 31 。aar总巨细为 100K 。假如有需求能够长途依靠去运用,假如有自界说化的需求,也能够自行拉代码修正。
惯例,我如有讲解不到位或错漏的当地,期望同学们能够指出。假如有更好的办法或其他办法,或许你有遇到的坑也都能够在评论区交流一下,咱们互相学习进步。
为了开源项目把公司项目进度都落下了,接下来要赶赶进度了…
假如感觉本文对你有一点点的帮助,还望你能点赞
支撑一下,你的支撑是我最大的动力。
Ok,这一期就此完结。
本文正在参加「金石计划」