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: 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
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.LinearLayout
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.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
@ -16,79 +15,47 @@ import sushi.hardcore.droidfs.util.PathUtils
import java.text.DateFormat
import java.util.*
class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault())
private var explorerElements = listOf<ExplorerElement>()
class ExplorerElementAdapter(
context: Context,
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)
val selectedItems: MutableList<Int> = ArrayList()
override fun getCount(): Int {
override fun getItemCount(): Int {
return explorerElements.size
}
override fun getItem(position: Int): ExplorerElement {
return explorerElements[position]
}
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)) {
view.setBackgroundColor(ContextCompat.getColor(context, R.color.itemSelected))
} else {
view.setBackgroundColor(Color.alpha(0))
}
return view
}
fun onItemClick(position: Int) {
if (selectedItems.isNotEmpty()) {
if (!explorerElements[position].isParentFolder) {
if (selectedItems.contains(position)) {
selectedItems.remove(position)
} else {
selectedItems.add(position)
}
notifyDataSetChanged()
}
}
}
fun onItemLongClick(position: Int) {
private fun toggleSelection(position: Int): Boolean {
if (!explorerElements[position].isParentFolder) {
if (!selectedItems.contains(position)) {
selectedItems.add(position)
} else {
if (selectedItems.contains(position)) {
selectedItems.remove(position)
} else {
selectedItems.add(position)
return true
}
notifyDataSetChanged()
}
return false
}
private fun onItemClick(position: Int): Boolean {
onExplorerElementClick(position)
if (selectedItems.isNotEmpty()) {
return toggleSelection(position)
}
return false
}
private fun onItemLongClick(position: Int): Boolean {
onExplorerElementLongClick(position)
return toggleSelection(position)
}
fun selectAll() {
@ -105,8 +72,112 @@ class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
notifyDataSetChanged()
}
fun setExplorerElements(explorer_elements: List<ExplorerElement>) {
this.explorerElements = explorer_elements
unSelectAll()
open class ExplorerElementViewHolder(
itemView: View,
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.View
import android.view.WindowManager
import android.widget.AdapterView.OnItemClickListener
import android.widget.AdapterView.OnItemLongClickListener
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.ConstValues
@ -67,7 +66,7 @@ open class BaseExplorerActivity : BaseActivity() {
protected var usf_keep_open = false
private lateinit var toolbar: androidx.appcompat.widget.Toolbar
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 textDirEmpty: TextView
private lateinit var currentPathText: TextView
@ -88,7 +87,7 @@ open class BaseExplorerActivity : BaseActivity() {
init()
toolbar = findViewById(R.id.toolbar)
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)
textDirEmpty = findViewById(R.id.text_dir_empty)
currentPathText = findViewById(R.id.current_path_text)
@ -96,14 +95,13 @@ open class BaseExplorerActivity : BaseActivity() {
setSupportActionBar(toolbar)
title = ""
titleText.text = getString(R.string.volume, volumeName)
explorerAdapter = ExplorerElementAdapter(this)
explorerAdapter = ExplorerElementAdapter(this, ::onExplorerItemClick, ::onExplorerItemLongClick)
explorerViewModel= ViewModelProvider(this).get(ExplorerViewModel::class.java)
currentDirectoryPath = explorerViewModel.currentDirectoryPath
setCurrentPath(currentDirectoryPath)
listExplorer.apply {
recycler_view_explorer.apply {
adapter = explorerAdapter
onItemClickListener = OnItemClickListener { _, _, position, _ -> onExplorerItemClick(position) }
onItemLongClickListener = OnItemLongClickListener { _, _, position, _ -> onExplorerItemLongClick(position); true }
layoutManager = LinearLayoutManager(this@BaseExplorerActivity)
}
refresher.setOnRefreshListener {
setCurrentPath(currentDirectoryPath)
@ -172,7 +170,6 @@ open class BaseExplorerActivity : BaseActivity() {
protected open fun onExplorerItemClick(position: Int) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) {
val fullPath = explorerElements[position].fullPath
@ -203,7 +200,6 @@ open class BaseExplorerActivity : BaseActivity() {
}
protected open fun onExplorerItemLongClick(position: Int) {
explorerAdapter.onItemLongClick(position)
invalidateOptionsMenu()
}
@ -262,7 +258,7 @@ open class BaseExplorerActivity : BaseActivity() {
)
}
}
explorerAdapter.setExplorerElements(explorerElements)
explorerAdapter.explorerElements = explorerElements
}
}.start()
}

View File

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

View File

@ -23,7 +23,6 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun onExplorerItemClick(position: Int) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) {
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 isDirectory: Boolean
get() = elementType.toInt() == 0
get() = elementType.toInt() == DIRECTORY_TYPE
val isParentFolder: Boolean
get() = elementType.toInt() == -1
get() = elementType.toInt() == PARENT_FOLDER_TYPE
val isRegularFile: Boolean
get() = elementType.toInt() == 1
get() = elementType.toInt() == REGULAR_FILE_TYPE
companion object {
const val DIRECTORY_TYPE = 0
const val PARENT_FOLDER_TYPE = -1
const val REGULAR_FILE_TYPE = 1
@JvmStatic
//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 {

View File

@ -14,27 +14,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<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">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/explorer_content"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@ -14,27 +14,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<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">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/explorer_content"/>
</RelativeLayout>

View File

@ -12,27 +12,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<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">
<ListView
android:id="@+id/list_explorer"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/listViewNoDivider"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/explorer_content"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@ -4,45 +4,53 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon_element"
android:layout_width="50dp"
android:layout_height="50dp"/>
<LinearLayout
android:id="@+id/selectable_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:orientation="vertical">
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/text_element_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/adapter_text_size"/>
<ImageView
android:id="@+id/icon_element"
android:layout_width="50dp"
android:layout_height="50dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="bottom">
android:layout_marginTop="3dp"
android:orientation="vertical">
<TextView
android:id="@+id/text_element_mtime"
android:layout_width="wrap_content"
android:id="@+id/text_element_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginEnd="7dp"/>
android:textSize="@dimen/adapter_text_size"/>
<TextView
android:id="@+id/text_element_size"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"/>
android:orientation="horizontal"
android:layout_gravity="bottom">
<TextView
android:id="@+id/text_element_mtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginEnd="7dp"/>
<TextView
android:id="@+id/text_element_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"/>
</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>
</style>
<style name="listViewNoDivider">
<item name="android:divider">#000000</item>
<item name="android:dividerHeight">0dp</item>
</style>
<style name="infoBarTextView">
<item name="android:ellipsize">start</item>
<item name="android:singleLine">true</item>