Kotlin - 處理Android-WebView文件上傳的工具類

最近開發上遇到需要處理WebView進行文件上傳的問題,但是由于原生的WebView并不支持文件上傳,只能我們重寫WebChromeClient類中的onShowFileChooser方法,下面我們就來實現這個操作。

效果圖,文件已選擇
選擇文件
切換存儲設備

注意:該代碼不兼容低版本設備。

object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView?,
                filePathCallback: ValueCallback<Array<Uri>>?,
                fileChooserParams: FileChooserParams?
            ): Boolean {
                /// 此次處理文件上傳
                return true
            }
        }
  • 準備工作,處理圖片上傳和文件上傳

    1. 圖片選擇對話框,這里我們使用第三方庫

      implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.3.4'
      
    2. 文件選擇對話框,我們手動實現一個簡單的文件選擇頁面

      -- 后面貼出具體代碼。

  • WebViewFileUploader,處理WebView文件上傳的工具類

    1. 由于需要處理Activity的返回值,因此需要接收onActivityResult的數據。

    2. 需要處理接收到的文件的返回或者取消文件選擇的操作(置空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
              }
          }
       }
      
      }
      
  • 文件管理器的簡單實現

    需求:

    • 支持文件選擇,因此需要展示文件夾、文件(支持記憶上一次目錄的位置)
    • 支持切換存儲設備選擇(有內存卡的)
    • 基本的額文件圖標展示
    • 支持展示路徑面包屑,面包屑支持點擊操作

    實現步驟:

    1. 獲取手機中內存設備

      /// 獲取內存儲備地址,由于方法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)
          }
      
    2. 列出目錄中的文件夾及文件

      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
          }
      }
      
    3. 展示文件及文件夾的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
          }
      
      }
      
    4. 面包屑的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
                  )
              }
          }
      
      }
      
    5. 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
        }
    
    }
    

項目下載地址:查看,轉載請聲明出處,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377