Switch explorer from ListView to RecyclerView

This commit is contained in:
Matéo Duparc 2021-11-10 19:45:10 +01:00
parent 65ecdd19ca
commit e3df7be3b5
Signed by untrusted user: hardcoresushi
GPG Key ID: 007F84120107191E
11 changed files with 219 additions and 181 deletions

View File

@ -1,14 +1,13 @@
package sushi.hardcore.droidfs.adapters package sushi.hardcore.droidfs.adapters
import android.content.Context import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.ConstValues.Companion.getAssociatedDrawable import sushi.hardcore.droidfs.ConstValues.Companion.getAssociatedDrawable
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
@ -16,79 +15,47 @@ import sushi.hardcore.droidfs.util.PathUtils
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
class ExplorerElementAdapter(private val context: Context) : BaseAdapter() { class ExplorerElementAdapter(
private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault()) context: Context,
private var explorerElements = listOf<ExplorerElement>() private val onExplorerElementClick: (Int) -> Unit,
private val onExplorerElementLongClick: (Int) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault())
var explorerElements = listOf<ExplorerElement>()
set(value) {
field = value
unSelectAll()
}
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
val selectedItems: MutableList<Int> = ArrayList() val selectedItems: MutableList<Int> = ArrayList()
override fun getCount(): Int {
override fun getItemCount(): Int {
return explorerElements.size return explorerElements.size
} }
override fun getItem(position: Int): ExplorerElement { private fun toggleSelection(position: Int): Boolean {
return explorerElements[position] if (!explorerElements[position].isParentFolder) {
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View = convertView ?: inflater.inflate(R.layout.adapter_explorer_element, parent, false)
val currentElement = getItem(position)
val textElementName = view.findViewById<TextView>(R.id.text_element_name)
textElementName.text = currentElement.name
val textElementMtime = view.findViewById<TextView>(R.id.text_element_mtime)
val textElementSize = view.findViewById<TextView>(R.id.text_element_size)
if (!currentElement.isParentFolder){
textElementSize.text = PathUtils.formatSize(currentElement.size)
} else {
textElementSize.text = ""
}
var drawableId = R.drawable.icon_folder
when {
currentElement.isDirectory -> {
textElementMtime.text = dateFormat.format(currentElement.mTime)
}
currentElement.isParentFolder -> {
textElementMtime.setText(R.string.parent_folder)
}
else -> {
textElementMtime.text = dateFormat.format(currentElement.mTime)
drawableId = getAssociatedDrawable(currentElement.name)
}
}
view.findViewById<ImageView>(R.id.icon_element).setImageResource(drawableId)
if (selectedItems.contains(position)) { if (selectedItems.contains(position)) {
view.setBackgroundColor(ContextCompat.getColor(context, R.color.itemSelected)) selectedItems.remove(position)
} else { } else {
view.setBackgroundColor(Color.alpha(0)) selectedItems.add(position)
return true
} }
return view }
return false
} }
fun onItemClick(position: Int) { private fun onItemClick(position: Int): Boolean {
onExplorerElementClick(position)
if (selectedItems.isNotEmpty()) { if (selectedItems.isNotEmpty()) {
if (!explorerElements[position].isParentFolder) { return toggleSelection(position)
if (selectedItems.contains(position)) {
selectedItems.remove(position)
} else {
selectedItems.add(position)
}
notifyDataSetChanged()
}
} }
return false
} }
fun onItemLongClick(position: Int) { private fun onItemLongClick(position: Int): Boolean {
if (!explorerElements[position].isParentFolder) { onExplorerElementLongClick(position)
if (!selectedItems.contains(position)) { return toggleSelection(position)
selectedItems.add(position)
} else {
selectedItems.remove(position)
}
notifyDataSetChanged()
}
} }
fun selectAll() { fun selectAll() {
@ -105,8 +72,112 @@ class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
notifyDataSetChanged() notifyDataSetChanged()
} }
fun setExplorerElements(explorer_elements: List<ExplorerElement>) { open class ExplorerElementViewHolder(
this.explorerElements = explorer_elements itemView: View,
unSelectAll() private val onClick: (Int) -> Boolean,
private val onLongClick: (Int) -> Boolean,
) : RecyclerView.ViewHolder(itemView) {
private val textElementName by lazy {
itemView.findViewById<TextView>(R.id.text_element_name)
}
protected val textElementSize: TextView by lazy {
itemView.findViewById(R.id.text_element_size)
}
protected val textElementMtime: TextView by lazy {
itemView.findViewById(R.id.text_element_mtime)
}
protected val icon: ImageView by lazy {
itemView.findViewById(R.id.icon_element)
}
private val selectableContainer: LinearLayout by lazy {
itemView.findViewById(R.id.selectable_container)
}
protected fun setBackground(isSelected: Boolean) {
itemView.setBackgroundResource(if (isSelected) { R.color.itemSelected } else { 0 })
}
open fun bind(explorerElement: ExplorerElement, position: Int) {
textElementName.text = explorerElement.name
selectableContainer.setOnClickListener {
setBackground(onClick(position))
}
selectableContainer.setOnLongClickListener {
setBackground(onLongClick(position))
true
}
}
}
open class RegularElementViewHolder(
itemView: View,
private val dateFormat: DateFormat,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
) : ExplorerElementViewHolder(itemView, onClick, onLongClick) {
open fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position)
textElementSize.text = PathUtils.formatSize(explorerElement.size)
textElementMtime.text = dateFormat.format(explorerElement.mTime)
setBackground(isSelected)
}
}
class FileViewHolder(
itemView: View,
dateFormat: DateFormat,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) {
override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position, isSelected)
icon.setImageResource(getAssociatedDrawable(explorerElement.name))
}
}
class DirectoryViewHolder(
itemView: View,
dateFormat: DateFormat,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
) : RegularElementViewHolder(itemView, dateFormat, onClick, onLongClick) {
override fun bind(explorerElement: ExplorerElement, position: Int, isSelected: Boolean) {
super.bind(explorerElement, position, isSelected)
icon.setImageResource(R.drawable.icon_folder)
}
}
class ParentFolderViewHolder(
itemView: View,
onClick: (Int) -> Boolean,
onLongClick: (Int) -> Boolean,
): ExplorerElementViewHolder(itemView, onClick, onLongClick) {
override fun bind(explorerElement: ExplorerElement, position: Int) {
super.bind(explorerElement, position)
textElementSize.text = ""
textElementMtime.setText(R.string.parent_folder)
icon.setImageResource(R.drawable.icon_folder)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(R.layout.adapter_explorer_element, parent, false)
return when (viewType) {
ExplorerElement.REGULAR_FILE_TYPE -> FileViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick)
ExplorerElement.DIRECTORY_TYPE -> DirectoryViewHolder(view, dateFormat, ::onItemClick, ::onItemLongClick)
ExplorerElement.PARENT_FOLDER_TYPE -> ParentFolderViewHolder(view, ::onItemClick, ::onItemLongClick)
else -> throw IllegalArgumentException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val element = explorerElements[position]
if (element.isParentFolder) {
(holder as ParentFolderViewHolder).bind(element, position)
} else {
(holder as RegularElementViewHolder).bind(element, position, selectedItems.contains(position))
}
}
override fun getItemViewType(position: Int): Int {
return explorerElements[position].elementType.toInt()
} }
} }

View File

@ -11,16 +11,15 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.AdapterView.OnItemClickListener
import android.widget.AdapterView.OnItemLongClickListener
import android.widget.EditText import android.widget.EditText
import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.ConstValues
@ -67,7 +66,7 @@ open class BaseExplorerActivity : BaseActivity() {
protected var usf_keep_open = false protected var usf_keep_open = false
private lateinit var toolbar: androidx.appcompat.widget.Toolbar private lateinit var toolbar: androidx.appcompat.widget.Toolbar
private lateinit var titleText: TextView private lateinit var titleText: TextView
private lateinit var listExplorer: ListView private lateinit var recycler_view_explorer: RecyclerView
private lateinit var refresher: SwipeRefreshLayout private lateinit var refresher: SwipeRefreshLayout
private lateinit var textDirEmpty: TextView private lateinit var textDirEmpty: TextView
private lateinit var currentPathText: TextView private lateinit var currentPathText: TextView
@ -88,7 +87,7 @@ open class BaseExplorerActivity : BaseActivity() {
init() init()
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
titleText = findViewById(R.id.title_text) titleText = findViewById(R.id.title_text)
listExplorer = findViewById(R.id.list_explorer) recycler_view_explorer = findViewById(R.id.recycler_view_explorer)
refresher = findViewById(R.id.refresher) refresher = findViewById(R.id.refresher)
textDirEmpty = findViewById(R.id.text_dir_empty) textDirEmpty = findViewById(R.id.text_dir_empty)
currentPathText = findViewById(R.id.current_path_text) currentPathText = findViewById(R.id.current_path_text)
@ -96,14 +95,13 @@ open class BaseExplorerActivity : BaseActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
title = "" title = ""
titleText.text = getString(R.string.volume, volumeName) titleText.text = getString(R.string.volume, volumeName)
explorerAdapter = ExplorerElementAdapter(this) explorerAdapter = ExplorerElementAdapter(this, ::onExplorerItemClick, ::onExplorerItemLongClick)
explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java) explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java)
currentDirectoryPath = explorerViewModel.currentDirectoryPath currentDirectoryPath = explorerViewModel.currentDirectoryPath
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
listExplorer.apply { recycler_view_explorer.apply {
adapter = explorerAdapter adapter = explorerAdapter
onItemClickListener = OnItemClickListener { _, _, position, _ -> onExplorerItemClick(position) } layoutManager = LinearLayoutManager(this@BaseExplorerActivity)
onItemLongClickListener = OnItemLongClickListener { _, _, position, _ -> onExplorerItemLongClick(position); true }
} }
refresher.setOnRefreshListener { refresher.setOnRefreshListener {
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
@ -172,7 +170,6 @@ open class BaseExplorerActivity : BaseActivity() {
protected open fun onExplorerItemClick(position: Int) { protected open fun onExplorerItemClick(position: Int) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty() val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) { if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) { if (!wasSelecting) {
val fullPath = explorerElements[position].fullPath val fullPath = explorerElements[position].fullPath
@ -203,7 +200,6 @@ open class BaseExplorerActivity : BaseActivity() {
} }
protected open fun onExplorerItemLongClick(position: Int) { protected open fun onExplorerItemLongClick(position: Int) {
explorerAdapter.onItemLongClick(position)
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -262,7 +258,7 @@ open class BaseExplorerActivity : BaseActivity() {
) )
} }
} }
explorerAdapter.setExplorerElements(explorerElements) explorerAdapter.explorerElements = explorerElements
} }
}.start() }.start()
} }

View File

@ -235,9 +235,8 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
override fun onExplorerItemLongClick(position: Int) { override fun onExplorerItemLongClick(position: Int) {
super.onExplorerItemLongClick(position)
cancelItemAction() cancelItemAction()
explorerAdapter.onItemLongClick(position)
invalidateOptionsMenu()
} }
private fun createNewFile(fileName: String){ private fun createNewFile(fileName: String){
@ -380,7 +379,7 @@ class ExplorerActivity : BaseExplorerActivity() {
if (size > 1) { if (size > 1) {
dialog.setMessage(getString(R.string.multiple_delete_confirm, explorerAdapter.selectedItems.size.toString())) dialog.setMessage(getString(R.string.multiple_delete_confirm, explorerAdapter.selectedItems.size.toString()))
} else { } else {
dialog.setMessage(getString(R.string.single_delete_confirm, explorerAdapter.getItem(explorerAdapter.selectedItems[0]).name)) dialog.setMessage(getString(R.string.single_delete_confirm, explorerAdapter.explorerElements[explorerAdapter.selectedItems[0]].name))
} }
dialog.show() dialog.show()
true true
@ -441,7 +440,7 @@ class ExplorerActivity : BaseExplorerActivity() {
private fun removeSelectedItems() { private fun removeSelectedItems() {
var failedItem: String? = null var failedItem: String? = null
for (i in explorerAdapter.selectedItems) { for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.getItem(i) val element = explorerAdapter.explorerElements[i]
val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name) val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name)
if (element.isDirectory) { if (element.isDirectory) {
val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath) val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath)

View File

@ -23,7 +23,6 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun onExplorerItemClick(position: Int) { override fun onExplorerItemClick(position: Int) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty() val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) { if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) { if (!wasSelecting) {
val fullPath = PathUtils.pathJoin(currentDirectoryPath, explorerElements[position].name) val fullPath = PathUtils.pathJoin(currentDirectoryPath, explorerElements[position].name)

View File

@ -8,15 +8,19 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
val fullPath: String = PathUtils.pathJoin(parentPath, name) val fullPath: String = PathUtils.pathJoin(parentPath, name)
val isDirectory: Boolean val isDirectory: Boolean
get() = elementType.toInt() == 0 get() = elementType.toInt() == DIRECTORY_TYPE
val isParentFolder: Boolean val isParentFolder: Boolean
get() = elementType.toInt() == -1 get() = elementType.toInt() == PARENT_FOLDER_TYPE
val isRegularFile: Boolean val isRegularFile: Boolean
get() = elementType.toInt() == 1 get() = elementType.toInt() == REGULAR_FILE_TYPE
companion object { companion object {
const val DIRECTORY_TYPE = 0
const val PARENT_FOLDER_TYPE = -1
const val REGULAR_FILE_TYPE = 1
@JvmStatic @JvmStatic
//this function is needed because I had some problems calling the constructor from JNI, probably due to arguments with default values //this function is needed because I had some problems calling the constructor from JNI, probably due to arguments with default values
fun new(name: String, elementType: Short, size: Long, mTime: Long, parentPath: String): ExplorerElement { fun new(name: String, elementType: Short, size: Long, mTime: Long, parentPath: String): ExplorerElement {

View File

@ -14,27 +14,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <include layout="@layout/explorer_content"/>
android:id="@+id/text_dir_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="@string/dir_empty"
android:visibility="invisible"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"

View File

@ -14,27 +14,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <include layout="@layout/explorer_content"/>
android:id="@+id/text_dir_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="@string/dir_empty"
android:visibility="invisible"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -12,27 +12,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <include layout="@layout/explorer_content"/>
android:id="@+id/text_dir_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="@string/dir_empty"
android:visibility="invisible"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"

View File

@ -4,6 +4,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/selectable_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground">
<ImageView <ImageView
android:id="@+id/icon_element" android:id="@+id/icon_element"
android:layout_width="50dp" android:layout_width="50dp"
@ -46,3 +52,5 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/text_dir_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="@string/dir_empty"
android:visibility="invisible"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</merge>

View File

@ -53,10 +53,6 @@
<item name="android:background">@drawable/button_background</item> <item name="android:background">@drawable/button_background</item>
</style> </style>
<style name="listViewNoDivider">
<item name="android:divider">#000000</item>
<item name="android:dividerHeight">0dp</item>
</style>
<style name="infoBarTextView"> <style name="infoBarTextView">
<item name="android:ellipsize">start</item> <item name="android:ellipsize">start</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>