最近开发上遇到需要处理WebView进行文件上传的问题,但是由于原生的WebView并不支持文件上传,只能我们重写WebChromeClient类中的onShowFileChooser方法,下面我们就来实现这个操作。
效果图,文件已选择
选择文件
切换存储设备
注意:该代码不兼容低版本设备。
object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
/// 此次处理文件上传
return true
}
}
-
准备工作,处理图片上传和文件上传
-
图片选择对话框,这里我们使用第三方库
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.3.4'
-
文件选择对话框,我们手动实现一个简单的文件选择页面
-- 后面贴出具体代码。
-
-
WebViewFileUploader,处理WebView文件上传的工具类
由于需要处理Activity的返回值,因此需要接收onActivityResult的数据。
-
需要处理接收到的文件的返回或者取消文件选择的操作(置空ValueCallback<Array<Uri>>的对象)
class WebViewFileUploader( private val activity: Activity, filePathCallback: ValueCallback<Array<Uri>>?, acceptType: String? ) { private var fileUploadCallback: ValueCallback<Array<Uri>>? = filePathCallback init { when { acceptType?.toLowerCase(Locale.getDefault())?.startsWith("image/*") == true -> pickPicture() else -> activity.startActivityForResult( Intent( activity, FileManagerActivity::class.java ), FileManagerActivity.CHOOSE_REQUEST ) } } /** 从相册获取图片 **/ private fun pickPicture(): Unit = PictureSelector.create(activity) .openGallery(PictureMimeType.ofImage())//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio() .loadImageEngine(GlideEngine.createGlideEngine()) .maxSelectNum(1)// 最大图片选择数量 int .minSelectNum(1)// 最小选择数量 int .imageSpanCount(4)// 每行显示个数 int .selectionMode(PictureConfig.SINGLE)// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE .previewImage(true)// 是否可预览图片 true or false .isCamera(true)// 是否显示拍照按钮 true or false .imageFormat(PictureMimeType.JPEG)// 拍照保存图片格式后缀,默认jpeg .isZoomAnim(true)// 图片列表点击 缩放效果 默认true .compress(true)// 是否压缩 true or false .enableCrop(false) .isGif(true)// 是否显示gif图片 true or false .openClickSound(false)// 是否开启点击声音 true or false .previewEggs(true)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false .minimumCompressSize(100)// 小于100kb的图片不压缩 .synOrAsy(true)//同步true或异步false 压缩 默认同步 .forResult(PictureConfig.CHOOSE_REQUEST)//结果回调onActivityResult code /** * 处理Activity返回结果 * @param requestCode 请求码 * @param resultCode 结果码 * @param data 数据包 */ fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { PictureConfig.CHOOSE_REQUEST -> { //图片选择 when (resultCode) { Activity.RESULT_OK -> { // 图片、视频、音频选择结果回调 val selectList = PictureSelector.obtainMultipleResult(data) val imgPath = with(selectList.firstOrNull()) { when { this == null -> null else -> compressPath ?: path } } if (!imgPath.isNullOrEmpty()) { val imgFile = File(imgPath) if (imgFile.exists()) { val uris = arrayOf(activity.getUriFromFileProvider(imgFile)) fileUploadCallback?.onReceiveValue(uris) fileUploadCallback = null return } } } } fileUploadCallback?.onReceiveValue(null) fileUploadCallback = null } FileManagerActivity.CHOOSE_REQUEST -> { //文件选择,具体实现请看下面的内容 when (resultCode) { Activity.RESULT_OK -> { val filePath = data?.getStringExtra("file") if (!filePath.isNullOrEmpty()) { val file = File(filePath) if (file.exists()) { val uris = arrayOf(activity.getUriFromFileProvider(file)) fileUploadCallback?.onReceiveValue(uris) fileUploadCallback = null return } } } } //没有接收到文件时,需要返回为空,不然不支持第二次文件的选择【切记切记】 fileUploadCallback?.onReceiveValue(null) fileUploadCallback = null } } } }
-
文件管理器的简单实现
需求:
- 支持文件选择,因此需要展示文件夹、文件(支持记忆上一次目录的位置)
- 支持切换存储设备选择(有内存卡的)
- 基本的额文件图标展示
- 支持展示路径面包屑,面包屑支持点击操作
实现步骤:
-
获取手机中内存设备
/// 获取内存储备地址,由于方法getVolumePaths被隐藏,因此需要通过反射获取。 @Suppress("unchecked_cast") fun Context.getStoragePaths(): Array<String>? = try { with(StorageManager::class.java.getMethod("getVolumePaths")) { isAccessible = true invoke((getSystemService(Context.STORAGE_SERVICE) as StorageManager)) as? Array<String> } } catch (e: Exception) { e.printStackTrace() null } /** * 获取文件得Uri * @param file 文件对象 */ fun Context.getUriFromFileProvider(file: File): Uri = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { true -> FileProvider.getUriForFile(this, "$packageName.FileProvider", file) else -> Uri.fromFile(file) }
-
列出目录中的文件夹及文件
object FileScannerUtils { /** * 列出某个目录下的文件目录和文件夹 * @param folderPath 文件夹路径 * @param fileFilter 要匹配的文件类型 */ @JvmStatic fun list( folderPath: String, fileFilter: FileFilter, onCallback: (folders: List<File>, files: List<File>) -> Unit, onError: (message: String) -> Unit ) { val f = File(folderPath) if (!f.canRead()) { onError("文件不可读取!") return } val files = mutableListOf<File>() val folders = mutableListOf<File>() f.listFiles(fileFilter)?.forEach { when { it.isFile -> files.add(it) it.isDirectory -> folders.add(it) } } files.sortBy { it.name.toLowerCase(Locale.getDefault()) } folders.sortBy { it.name.toLowerCase(Locale.getDefault()) } onCallback(folders, files) } } /** 文件筛选类型 **/ enum class FileFilterType { None, Picture, Document } /** 文件筛选器工厂类 **/ object FileFilterFactory { @JvmStatic fun getFileFilter(type: FileFilterType = FileFilterType.None): MyFileFilter = when (type) { FileFilterType.None -> noneFileFilter FileFilterType.Picture -> pictureFileFilter FileFilterType.Document -> documentFileFilter } /** 不筛选 **/ private val noneFileFilter get() = MyFileFilter() /** 图片筛选 **/ private val pictureFileFilter get() = MyFileFilter( mutableListOf( "jpg", "jpeg", "png", "bmp", "webp" ) ) /** 文档筛选 **/ private val documentFileFilter get() = MyFileFilter( mutableListOf( "txt", "doc", "docx", "xls", "xlsx", "pdf", "ppt", "wps", "java", "cs", "kt", "sql", "cpp", "c", "h" ) ) } /** * 我的文件筛选器 * @param fileExtensions 文件扩展集合 */ class MyFileFilter(private val fileExtensions: List<String>? = null) : FileFilter { override fun accept(f: File?): Boolean { if (fileExtensions?.isNotEmpty() != true || f?.isDirectory == true) return true if (f?.isFile == true) return f.extension.toLowerCase(Locale.getDefault()) in fileExtensions return false } }
-
展示文件及文件夹的Adapter类的实现
abstract class BaseListAdapter<E : Any> @JvmOverloads constructor( val context: Context, @LayoutRes private val layoutId: Int, var dataSource: MutableList<E>? = mutableListOf() ) : BaseAdapter() { /** 获取适配器数据源 **/ fun getAdapterDataSource(): MutableList<E>? = dataSource /** * 添加数据 * * @param e 数据项 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun add(e: E, isRefresh: Boolean = false) { dataSource?.add(e) if (isRefresh) notifyDataSetChanged() } /** * 添加数据 * * @param e 数据项 * @param index 要插入的索引位置 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun add(e: E, index: Int, isRefresh: Boolean = false) { dataSource?.add(index, e) if (isRefresh) notifyDataSetChanged() } /** * 添加多个数据 * * @param elements 数据项集合 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun add(elements: MutableList<E>, isRefresh: Boolean = false) { dataSource?.addAll(elements) if (isRefresh) notifyDataSetChanged() } /** * 添加数据集合 * @param es 数据集合 * @param isRefresh 是否刷新 */ @Synchronized @JvmOverloads fun add(vararg es: E, isRefresh: Boolean = false) { dataSource?.addAll(es.toMutableList()) if (isRefresh) notifyDataSetChanged() } /** * 删除数据项 * * @param e 要删除的数据项 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun remove(e: E, isRefresh: Boolean = false) { dataSource?.remove(e) if (isRefresh) notifyDataSetChanged() } /** * 删除某一索引位置的数据 * * @param index 数据索引位置 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun remove(index: Int, isRefresh: Boolean = false) { dataSource?.removeAt(index) if (isRefresh) notifyDataSetChanged() } /** * 删除数据集合 * * @param elements 数据集合 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun remove(elements: MutableList<E>, isRefresh: Boolean = false) { dataSource?.removeAll(elements) if (isRefresh) notifyDataSetChanged() } /** * 更新某处的数据 * * @param index 要更新的数据的索引 * @param e 数据内容 * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun update(index: Int, e: E, isRefresh: Boolean = false) { dataSource?.set(index, e) if (isRefresh) notifyDataSetChanged() } /** * 清空所有数据 * * @param isRefresh 是否刷新数据,默认为false */ @Synchronized @JvmOverloads fun clear(isRefresh: Boolean = false) { dataSource?.clear() if (isRefresh) notifyDataSetChanged() } override fun getCount(): Int = dataSource?.size ?: 0 override fun getItem(position: Int): Any? = dataSource?.get(position) override fun getItemId(position: Int): Long = position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? { val viewHolder = ViewHolder.getInstance(context, position, convertView, parent, layoutId) bindValues(viewHolder, position, dataSource?.get(position)!!) return viewHolder.convertView } /** * 绑定数据 * * @param holder * @param position 数据索引 * @param itemData 数据项 */ abstract fun bindValues(holder: ViewHolder, position: Int, itemData: E) /** * ViewHolder对象 * * @param context 上下文对象 * @param position 数据索引 * @param convertView itemView * @param parent Group-Root * @param layoutId item布局ID */ @Suppress("unused") class ViewHolder( val context: Context, val position: Int, convertView: View?, parent: ViewGroup?, @LayoutRes val layoutId: Int ) { companion object { /** * 获取ViewHolder实例 * * @param context 上下文对象 * @param position 数据索引 * @param convertView itemView对象 * @param parent RootParent * @param layoutId Item布局ID */ @JvmStatic fun getInstance( context: Context, position: Int, convertView: View?, parent: ViewGroup?, layoutId: Int ): ViewHolder = if (convertView != null) convertView.tag as ViewHolder else ViewHolder(context, position, convertView, parent, layoutId) } var convertView: View? private set init { this.convertView = convertView ?: View.inflate(context, layoutId, null) this.convertView?.tag = this } /** * 根据ID获取控件对象 * * @param id 控件ID */ @Suppress("UNCHECKED_CAST") fun <T : View> getViewById(@IdRes id: Int): T? { val result: View? = convertView?.findViewById(id) return result as? T? } /** * 设置文本控件的文本 * * @param id 控件ID * @param text 文本内容 */ fun setText(@IdRes id: Int, text: CharSequence): ViewHolder { getViewById<TextView>(id)?.text = text return this } /** * 设置文本控件的文本 * * @param id 控件ID * @param textId 数据String ID */ fun setText(@IdRes id: Int, @StringRes textId: Int): ViewHolder { setText(id, context.getString(textId)) return this } /** * 设置控件图片 * * @param id 图片控件ID * @param url 图片数据URL对象 */ fun setImage(@IdRes id: Int, url: Any): ViewHolder { val imageViewInstance = getViewById<ImageView>(id) imageViewInstance?.let { ImageLoader.displayImage(imageViewInstance, uri = url, target = imageViewInstance) } return this } } } class FileManagerAdapter(context: Context) : BaseListAdapter<File>(context, R.layout.item_for_file_folder) { override fun bindValues(holder: ViewHolder, position: Int, itemData: File) { val tv = holder.getViewById<TextView>(android.R.id.text1) tv?.text = itemData.name ContextCompat.getDrawable( context, when { itemData.isDirectory -> R.drawable.ic_folder itemData.isFile -> FileIconProvider.getDrawableId(itemData.extension) else -> R.drawable.ic_unknown_file } )?.apply { setBounds(0, 0, intrinsicWidth, intrinsicHeight) holder.getViewById<ImageView>(R.id.imgIcon)?.setImageDrawable(this) } } } /// 文件图标提供类 object FileIconProvider { @JvmStatic fun getDrawableId(extension: String): Int = when (extension.toLowerCase(Locale.getDefault())) { "txt" -> R.drawable.ic_file_txt "ppt" -> R.drawable.ic_file_ppt "doc" -> R.drawable.ic_file_doc "docx" -> R.drawable.ic_file_docx "xls" -> R.drawable.ic_file_xls "xlsx" -> R.drawable.ic_file_xls "png" -> R.drawable.ic_file_png "jpg", "jpeg" -> R.drawable.ic_file_jpg "java" -> R.drawable.ic_file_java "xml" -> R.drawable.ic_file_xml "html", "htm" -> R.drawable.ic_file_html "js" -> R.drawable.ic_file_js "mp3" -> R.drawable.ic_file_mp3 "mp4" -> R.drawable.ic_file_mp4 "dat" -> R.drawable.ic_file_dat "rmvb" -> R.drawable.ic_file_rmvb "avi" -> R.drawable.ic_file_avi "log" -> R.drawable.ic_file_log else -> R.drawable.ic_unknown_file } }
-
面包屑的Adapter的实现
/** * Created by Jbtm on 2017/4/24. * RecyclerAdapter通用数据适配器 */ abstract class BaseRecyclerAdapter<E> /** * 构造函数 * @param context 上下文对象 * * * @param itemId 布局ItemId * * * @param dataSource 数据源 */ @JvmOverloads constructor( val context: Context, @LayoutRes val layoutId: Int, dataSource: MutableList<E>? = null ) : RecyclerView.Adapter<BaseRecyclerAdapter.GenericViewHolder>() { protected var inflater: LayoutInflater = LayoutInflater.from(context) protected var dataSource: MutableList<E>? = null var onItemClickListener: OnItemClickListener<E>? = null init { this.dataSource = dataSource ?: mutableListOf<E>() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenericViewHolder { val itemView = inflater.inflate(layoutId, parent, false) return GenericViewHolder(itemView) } override fun onBindViewHolder(holder: GenericViewHolder, position: Int) { val item = dataSource!![position] if (onItemClickListener != null) holder.itemView.setOnClickListener { onItemClickListener!!.onItemClick( holder, holder.adapterPosition, position, item ) } bindValues(holder, holder.adapterPosition, position, item) } abstract fun bindValues( holder: GenericViewHolder, viewPosition: Int, dataPosition: Int, item: E ) override fun getItemCount(): Int = dataSource?.size ?: 0 /** * 添加一条数据 * @param item 数据项 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun add(item: E, isRefresh: Boolean = false) { dataSource?.add(item) if (isRefresh) notifyDataSetChanged() } /** * 指定位置添加一条数据 * @param item 数据项 * @param position 要添加数据的位置 * @param isRefresh 是否刷新 */ @JvmOverloads @Synchronized fun add(item: E, position: Int, isRefresh: Boolean = false) { dataSource?.add(position, item) if (isRefresh) notifyDataSetChanged() } /** * 添加一条数据 * @param items 数据源 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun add(items: MutableList<E>, isRefresh: Boolean = false) { dataSource?.addAll(items) if (isRefresh) notifyDataSetChanged() } /** * 添加一些数据 * @param items 数据集合 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun add(vararg items: E, isRefresh: Boolean = false) { dataSource?.addAll(items) if (isRefresh) notifyDataSetChanged() } /** * 删除一条数据 * @param item 数据项 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun remove(item: E, isRefresh: Boolean = false) { dataSource?.remove(item) if (isRefresh) notifyDataSetChanged() } /** * 删除一条指定位置的数据 * @param position 数据位置 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun remove(position: Int, isRefresh: Boolean = false) { dataSource?.removeAt(position) if (isRefresh) notifyItemChanged(position) } /** * 删除一些数据 * @param items 要删除的数据 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun remove(items: MutableList<E>, isRefresh: Boolean = false) { dataSource?.removeAll(items) if (isRefresh) notifyDataSetChanged() } /** * 更新数据源 * @param item 数据Item * @param position 要更新的位置 * @param isRefresh 是否刷新数据 */ @JvmOverloads @Synchronized fun update(item: E, position: Int, isRefresh: Boolean = false) { dataSource?.set(position, item) if (isRefresh) notifyItemChanged(position) } /** * 清空数据源 * @param isRefresh 是否刷新数据,默认:false */ @JvmOverloads @Synchronized fun clear(isRefresh: Boolean = false) { dataSource?.clear() if (isRefresh) notifyDataSetChanged() } /** * 获取数据项 * @param position 数据position */ fun getItem(position: Int) = dataSource?.get(position) class GenericViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun setText(@IdRes id: Int, text: String) { val txt: TextView? = itemView.findViewById(id) txt?.text = text } fun setText(@IdRes id: Int, @StringRes textId: Int, vararg params: Any?) { val txt: TextView? = itemView.findViewById(id) txt?.text = itemView.context.getString(textId, params) } } interface OnItemClickListener<E> { fun onItemClick(holder: GenericViewHolder, viewPosition: Int, dataPosition: Int, item: E) } } /// 面包屑类的实现 class BreadCrumbsAdapter(context: Context) : BaseRecyclerAdapter<Map<String, Any>>(context, R.layout.item_for_breadcrumbs) { var onItemClick: ((data: Map<String, Any>, relativePath: String, position: Int) -> Unit)? = null override fun bindValues( holder: GenericViewHolder, viewPosition: Int, dataPosition: Int, item: Map<String, Any> ) { val tv = holder.itemView.findViewById<TextView>(android.R.id.text1) tv.text = item["text"].toString() if (item["hasNavigation"] != null && item["hasNavigation"] is Boolean && item["hasNavigation"] == true) { ContextCompat.getDrawable( context, R.drawable.ic_navigation_breadcrumbs_forward )?.apply { setBounds(0, 0, intrinsicWidth, intrinsicHeight) tv.setCompoundDrawables(this, null, null, null) tv.compoundDrawablePadding = DensityUtils.dip2px(context, 1f) } } else { tv.compoundDrawablePadding = 0 tv.setCompoundDrawables(null, null, null, null) } val padding = DensityUtils.dip2px(context, 5f) tv.setPadding(padding, 0, padding, 0) tv.setOnClickListener { onItemClick?.invoke( item, if (dataPosition == 0) "" else dataSource!!.subList( 1, dataPosition + 1 ).joinToString(File.separator) { it["text"].toString() }, dataPosition ) } } }
-
FileManagerActivity的实现
class FileManagerActivity : AppCompatActivity(), AdapterView.OnItemClickListener { companion object { /** 选择请求 **/ const val CHOOSE_REQUEST = 19999 } private val fileManagerAdapter by lazy { FileManagerAdapter(this) } private val breadCrumbsAdapter by lazy { initializerBreadCrumbsAdapter() } private val sdcardPaths = mutableListOf<String>() private val lsPositionCache = mutableMapOf<String, Int>() private lateinit var currentSDCardPath: String private var currentFolder: File? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_file_manager) toolbar.setNavigationOnClickListener { if (!isFolderCanBack()) return@setNavigationOnClickListener else finish() } lvFiles.apply { onItemClickListener = this@FileManagerActivity adapter = fileManagerAdapter } rvBreadCrumbs.apply { layoutManager = LinearLayoutManager(this@FileManagerActivity, LinearLayoutManager.HORIZONTAL, false) adapter = breadCrumbsAdapter } sdcardPaths.apply { clear() addAll(getStoragePaths()?.toMutableList() ?: mutableListOf()) } if (!sdcardPaths.isNullOrEmpty()) { currentSDCardPath = sdcardPaths.first() currentFolder = File(currentSDCardPath) currentSDCardPath.listFoldersAndFiles() } } override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val item = fileManagerAdapter.getItem(position) as File if (item.isDirectory) { if (!item.canRead()) { Toast.makeText(this, "该文件夹不可读取", Toast.LENGTH_SHORT).show() return } if (item.parentFile?.exists() == true) lsPositionCache[item.parentFile!!.absolutePath] = lvFiles.firstVisiblePosition currentFolder = item item.absolutePath.listFoldersAndFiles() } if (item.isFile) { setResult(Activity.RESULT_OK, Intent().apply { putExtra("file", item.absolutePath) }) finish() } } override fun onBackPressed() { if (!isFolderCanBack()) return super.onBackPressed() } /** 是否是顶级目录 **/ private fun String.isTopLevelFolder(): Boolean = isNullOrEmpty() || !File(this).exists() || this in sdcardPaths /** 是否是顶级目录 **/ private fun File.isTopLevelFolder(): Boolean { if (!exists() || absolutePath.isNullOrEmpty()) return true return absolutePath in sdcardPaths } /** 文件夹是否支持返回 **/ private fun isFolderCanBack(): Boolean { if (currentFolder?.isTopLevelFolder() != false) return true val pf = currentFolder?.parentFile if (pf?.exists() == true) { currentFolder = pf pf.absolutePath.listFoldersAndFiles() return false } return true } /** 执行文件搜索 **/ private fun String.listFoldersAndFiles() = Thread { runOnUiThread { pbLoading.isVisible = true } FileScannerUtils.list( this, FileFilterFactory.getFileFilter(), { folders, files -> runOnUiThread { setCurrentBreadCrumbs() fileManagerAdapter.apply { clear() add(folders.toMutableList()) add(files.toMutableList()) notifyDataSetChanged() lvFiles.setSelection(lsPositionCache[this@listFoldersAndFiles] ?: 0) } pbLoading.isVisible = false } }) { runOnUiThread { pbLoading.isVisible = false } } }.start() /** 设置文件夹当前的面包屑 **/ private fun String.setCurrentBreadCrumbs() { val sdcardOrderIndex = sdcardPaths.indexOf(currentSDCardPath) + 1 val deviceName = "存储设备${if (sdcardPaths.size > 1) sdcardOrderIndex.toString() else ""}" breadCrumbsAdapter.apply { clear() add(mapOf("text" to deviceName, "hasNavigation" to false)) val pathParts = this@setCurrentBreadCrumbs.replaceFirst(Regex.fromLiteral(currentSDCardPath), "") .split(File.separator).filter { v -> !TextUtils.isEmpty(v) } pathParts.forEach { item -> add(mapOf("text" to item, "hasNavigation" to true)) } notifyDataSetChanged() } } /** 初始化面包屑适配器 **/ private fun initializerBreadCrumbsAdapter() = BreadCrumbsAdapter(this).apply { onItemClick = here@{ _, relativePath, _ -> if (relativePath.isEmpty()) { if ((currentFolder?.isTopLevelFolder() != false)) { if (sdcardPaths.size <= 1) return@here showSDCardChooseDialog() } else { currentFolder = File(currentSDCardPath) } } else { currentFolder = File("$currentSDCardPath/$relativePath") } currentFolder?.absolutePath?.listFoldersAndFiles() } } /** 显示SDCard的选择对话框 **/ private fun showSDCardChooseDialog() = AlertDialog.Builder(this) .setTitle("请选择存储设备") .setItems(mutableListOf(*sdcardPaths.toTypedArray()).mapIndexed { index, item -> "存储设备${index + 1}${if (item == currentSDCardPath) "(当前)" else ""}" }.toTypedArray()) { dialog, which -> currentSDCardPath = sdcardPaths[which] currentFolder = File(currentSDCardPath) currentSDCardPath.listFoldersAndFiles() dialog.dismiss() } .create() .show() }
-
在WebView中调用示例
class MainActivity : AppCompatActivity() { private var fileUploader: WebViewFileUploader? = null @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) browser.settings.apply { javaScriptEnabled = true cacheMode = WebSettings.LOAD_NO_CACHE } browser.webChromeClient = object : WebChromeClient() { override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams? ): Boolean { fileUploader = WebViewFileUploader( this@MainActivity, filePathCallback, fileChooserParams?.acceptTypes?.firstOrNull() ) return true } } browser.loadUrl("http://192.168.0.102:3000/") } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) fileUploader?.onActivityResult(requestCode, resultCode, data) fileUploader = null } }
项目下载地址:查看,转载请声明出处,谢谢!