隐藏缩略图的开关切换

This commit is contained in:
litingting 2025-10-15 18:31:33 +08:00
parent 592918d6ce
commit 37e894ad47
13 changed files with 539 additions and 153 deletions

View File

@ -7,7 +7,7 @@ import androidx.viewbinding.ViewBinding
import kotlin.let
abstract class BaseAdapter<K, T : ViewBinding>(
protected val mContext: Context
protected val mContext: Context,
) : RecyclerView.Adapter<BaseAdapter.VHolder<T>>() {
protected val data: MutableList<K> = mutableListOf()
@ -28,6 +28,8 @@ abstract class BaseAdapter<K, T : ViewBinding>(
}
fun getCurrentData() = data
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VHolder<T> {

View File

@ -0,0 +1,39 @@
package com.ux.video.file.filerecovery.base
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.viewbinding.ViewBinding
/**
* 高性能 RecyclerView 通用适配器基类支持 DiffUtil + ViewBinding
*/
abstract class DiffBaseAdapter<K, T : ViewBinding>(
protected val mContext: Context,
diffCallback: DiffUtil.ItemCallback<K>
) : ListAdapter<K, DiffBaseAdapter.VHolder<T>>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VHolder<T> {
return VHolder(getViewBinding(parent))
}
override fun onBindViewHolder(holder: VHolder<T>, position: Int) {
bindItem(holder, getItem(position))
}
/**
* 更新数据自动触发 DiffUtil 计算
*/
fun submitData(items: List<K>?) {
submitList(items ?: emptyList())
}
protected abstract fun getViewBinding(parent: ViewGroup): T
protected abstract fun bindItem(holder: VHolder<T>, item: K)
class VHolder<V : ViewBinding>(val vb: V) : androidx.recyclerview.widget.RecyclerView.ViewHolder(vb.root)
}

View File

@ -4,8 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import com.ux.video.file.filerecovery.base.BaseAdapter
import com.ux.video.file.filerecovery.base.DiffBaseAdapter
import com.ux.video.file.filerecovery.databinding.PhotoDisplayDateAdapterBinding
import com.ux.video.file.filerecovery.utils.Common
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
@ -15,12 +18,14 @@ class PhotoDisplayDateAdapter(
mContext: Context,
var mColumns: Int,
var viewModel: ScanRepository,
var onSelectedUpdate: (updatePath: String, isAdd: Boolean) -> Unit,
var clickItem:(item:ResultPhotosFiles)-> Unit
var onSelectedUpdate: (resultPhotosFiles: ResultPhotosFiles, isAdd: Boolean) -> Unit,
var clickItem: (item: ResultPhotosFiles) -> Unit
) :
BaseAdapter<Pair<String, List<ResultPhotosFiles>>, PhotoDisplayDateAdapterBinding>(mContext) {
private var allSelected: Boolean? = null
// private var hideThumbnails = false
override fun getViewBinding(parent: ViewGroup): PhotoDisplayDateAdapterBinding =
PhotoDisplayDateAdapterBinding.inflate(
LayoutInflater.from(parent.context),
@ -28,11 +33,21 @@ class PhotoDisplayDateAdapter(
false
)
fun updateHideThumbnails(isChecked: Boolean) {
// hideThumbnails = isChecked
notifyDataSetChanged()
}
/**
* 返回所有嵌套的数据量总数
*/
fun getTotalChildCount(): Int {
return data.sumOf { it.second.size }
fun getTotalChildCount(hideThumbnails: Boolean): Int {
if(hideThumbnails){
return data.sumOf { it.second.filter { !it.isThumbnail }.size }
}else{
return data.sumOf { it.second.size }
}
}
/**
@ -42,14 +57,14 @@ class PhotoDisplayDateAdapter(
allSelected = allSelect
data.forEach {
it.second.forEach { item ->
onSelectedUpdate(item.path.toString(),allSelect)
onSelectedUpdate(item, allSelect)
}
}
notifyDataSetChanged()
allSelected = null
}
fun resetAllValue(b: Boolean?){
fun resetAllValue(b: Boolean?) {
allSelected = b
}
@ -59,26 +74,36 @@ class PhotoDisplayDateAdapter(
}
@SuppressLint("SetTextI18n")
override fun bindItem(
holder: VHolder<PhotoDisplayDateAdapterBinding>,
item: Pair<String, List<ResultPhotosFiles>>
) {
holder.vb.run {
item.run {
val (date, files) = item
val childAdapter = PhotoDisplayDateChildAdapter(
mContext,
mColumns,
viewModel,
{ path, addOrRemove, isDateAllSelected ->
//点击当前Adapter某一天的全选或者子Item上的选中都会回调到这里
tvDayAllSelect.isSelected = isDateAllSelected
onSelectedUpdate(path.toString(),addOrRemove)
},clickItem).apply { setData(files) }
{ resultPhotosFiles, addOrRemove, isDateAllSelected ->
//点击当前Adapter某一天的全选或者子Item上的选中都会回调到这里
tvDayAllSelect.isSelected = isDateAllSelected
onSelectedUpdate(resultPhotosFiles, addOrRemove)
}, { updateHideThumbnails->
tvDayAllSelect.isSelected = updateHideThumbnails
},clickItem
).apply { setData(files) }
// if (hideThumbnails && files.filter { !it.isThumbnail }.isEmpty()) {
// holder.vb.root.isVisible = false
// return
// }else{
// holder.vb.root.isVisible = true
// childAdapter.updateHideThumbnails(hideThumbnails)
// }
allSelected?.let {
childAdapter.setAllSelected(it)
@ -90,12 +115,9 @@ class PhotoDisplayDateAdapter(
textDate.text = date
textChildCounts.text = "(${files.size})"
recyclerChild.apply {
layoutManager = GridLayoutManager(context, mColumns)
val gridSpacingItemDecoration =
GridSpacingItemDecoration(mColumns, Common.itemSpacing, Common.horizontalSpacing)
Common.showLog("---------mColumns=${mColumns}")
// resetItemDecorationOnce(gridSpacingItemDecoration)
adapter = childAdapter
isNestedScrollingEnabled = false
}
@ -104,5 +126,21 @@ class PhotoDisplayDateAdapter(
}
}
object ItemDiffCallback : DiffUtil.ItemCallback<Pair<String, List<ResultPhotosFiles>>>() {
override fun areItemsTheSame(
oldItem: Pair<String, List<ResultPhotosFiles>>,
newItem: Pair<String, List<ResultPhotosFiles>>
): Boolean {
return oldItem.first == newItem.first
}
override fun areContentsTheSame(
oldItem: Pair<String, List<ResultPhotosFiles>>,
newItem: Pair<String, List<ResultPhotosFiles>>
): Boolean {
return oldItem == newItem
}
}
}

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
@ -36,14 +37,18 @@ class PhotoDisplayDateChildAdapter(
* @param addOrRemove 选中还是取消选中
* @param dateAllSelected 这组数据是否全部选中某一天
*/
var onSelectedUpdate: (updatePath: String, addOrRemove: Boolean, dateAllSelected: Boolean) -> Unit,
var onSelectedUpdate: (resultPhotosFiles: ResultPhotosFiles, addOrRemove: Boolean, dateAllSelected: Boolean) -> Unit,
var hideThumbnailsUpdate:(dateAllSelected: Boolean)-> Unit,
var clickItem:(item:ResultPhotosFiles)-> Unit
) :
NewBaseAdapter<ResultPhotosFiles>(mContext) {
// private var hideThumbnails: Boolean? = null
//日期组某一天的数据选择状态维护
val dateSelectedMap = mutableSetOf<String>()
val dateSelectedMap = mutableSetOf<ResultPhotosFiles>()
//实际显示数据集合(包含隐藏的缩略图)
// val visibleList = data.toMutableSet()
companion object {
private const val TYPE_TWO = 2
@ -51,10 +56,17 @@ class PhotoDisplayDateChildAdapter(
private const val TYPE_FOUR = 4
}
// fun getVisibleList() = visibleList
fun updateHideThumbnails(isChecked: Boolean){
// hideThumbnails = isChecked
notifyDataSetChanged()
hideThumbnailsUpdate.invoke(getVisibleCount(dateSelectedMap.toMutableList(),isChecked) == getVisibleCount(data,isChecked))
}
fun setAllSelected(isAdd: Boolean) {
data.forEach {
addOrRemove(it.path.toString(), isAdd)
addOrRemove(it, isAdd)
}
notifyDataSetChanged()
}
@ -120,10 +132,12 @@ class PhotoDisplayDateChildAdapter(
when (holder) {
is TwoHolder -> holder.vb.run {
// root.isVisible = !(hideThumbnails == true && item.isThumbnail)
initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item)
}
is ThreeHolder -> holder.vb.run {
// root.isVisible = !(hideThumbnails == true && item.isThumbnail)
initDateView(rootLayout, imageSelect, textSize, imageThumbnail, item)
}
}
@ -145,17 +159,17 @@ class PhotoDisplayDateChildAdapter(
item: ResultPhotosFiles
) {
item.run {
viewModel.checkIsSelect(path.toString()).let {
viewModel.checkIsSelect(this).let {
imageSelectStatus.isSelected = it
addOrRemove(path.toString(), it)
addOrRemove(this, it)
}
imageSelectStatus.setOnClickListener {
it.isSelected = !it.isSelected
it.isSelected.let { newStatus ->
addOrRemove(path.toString(), newStatus)
addOrRemove(this, newStatus)
}
}
textSize.text = Common.formatFileSize(mContext, size)
textSize.text = sizeString
Glide.with(mContext)
.load(targetFile)
@ -197,14 +211,30 @@ class PhotoDisplayDateChildAdapter(
}
private fun addOrRemove(path: String, boolean: Boolean) {
private fun addOrRemove(resultPhotosFiles: ResultPhotosFiles, boolean: Boolean) {
if (boolean) {
dateSelectedMap.add(path)
dateSelectedMap.add(resultPhotosFiles)
} else {
dateSelectedMap.remove(path)
dateSelectedMap.remove(resultPhotosFiles)
}
onSelectedUpdate.invoke(path, boolean, dateSelectedMap.size == itemCount)
onSelectedUpdate.invoke(resultPhotosFiles, boolean, dateSelectedMap.size == data.size)
// updateSelected(resultPhotosFiles,boolean)
}
private fun updateSelected(resultPhotosFiles: ResultPhotosFiles,boolean: Boolean){
// hideThumbnails?.let {
// onSelectedUpdate.invoke(resultPhotosFiles, boolean, getVisibleCount(dateSelectedMap.toMutableList(),it) == getVisibleCount(data,it))
// }
}
fun getVisibleCount(list: MutableList<ResultPhotosFiles> = data, hideThumbnails: Boolean): Int {
if(hideThumbnails){
return list.filter { !it.isThumbnail }.size
}else{
return list.size
}
}
}

View File

@ -17,10 +17,10 @@ import com.ux.video.file.filerecovery.utils.Common.setItemSelect
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySize
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySizeList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterRemoveThumbnailsAsync
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterThumbnailsAsync
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRange
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinDateRangeList
//import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonths
//import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonthsList
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
import com.ux.video.file.filerecovery.utils.ExtendFunctions.mbToBytes
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
@ -28,6 +28,7 @@ import com.ux.video.file.filerecovery.utils.ScanManager
import com.ux.video.file.filerecovery.utils.ScanManager.copySelectedFilesAsync
import com.ux.video.file.filerecovery.utils.ScanManager.deleteFilesAsync
import com.ux.video.file.filerecovery.utils.ScanRepository
import kotlinx.coroutines.launch
import java.util.Date
class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
@ -90,12 +91,16 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
private lateinit var sortByDateReverse: List<Pair<String, List<ResultPhotosFiles>>>
private lateinit var sortedByDatePositive: List<Pair<String, List<ResultPhotosFiles>>>
//最新显示的数据集合(包含缩略图 ,只保存当前筛选后或者排序后显示的数据不受switch切换影响
private var currentDateList: List<Pair<String, List<ResultPhotosFiles>>>? = null
private var currentSizeList: List<ResultPhotosFiles>? = null
//选中的所有数据集合(实际选中)
private lateinit var allSelectedSetList: Set<String>
private lateinit var allSelectedSetList: Set<ResultPhotosFiles>
//选中的所有数据集合(筛选后的数据实际显示的所有选中)
private lateinit var filterSelectedSetList: Set<String>
private lateinit var filterSelectedSetList: Set<ResultPhotosFiles>
private lateinit var mItemDecoration: GridSpacingItemDecoration
@ -124,11 +129,14 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
Common.showLog("当前显示筛选数据 选中状态更新: ${displaySet.size}")
updateCurrentIsAllSelectStatus()
}
binding.imageViewBack.setOnClickListener { finish() }
list?.let {
binding.tvThumbnailCounts.text =
getString(R.string.hide_thumbnails, it.filter { it.isThumbnail }.size)
//降序(最近的在前面)
sortByDateReverse = Common.getSortByDayNewToOld(it)
sortByDateReverse = Common.getSortByDayNewToOldInit(it)
//升序(时间最远的在前面)
sortedByDatePositive = Common.getSortByDayOldToNew(sortByDateReverse)
sortBySizeBigToSmall = Common.getSortBySizeBigToSmall(it)
@ -137,8 +145,11 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
sizeSortAdapter = PhotoDisplayDateChildAdapter(
this@PhotoSortingActivity,
columns, viewModel,
{ path, isAdd, allSelected ->
viewModel.toggleSelection(isAdd, path)
{ resultPhotosFiles, isAdd, allSelected ->
viewModel.toggleSelection(isAdd, resultPhotosFiles)
}, { hide ->
}) { item ->
startActivity(
Intent(
@ -166,10 +177,47 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
})
}.apply {
setData(sortByDateReverse)
resetCurrentDateList(sortByDateReverse)
}
setDateAdapter()
setFilter()
binding.run {
imageViewBack.setOnClickListener { finish() }
switchHideThumbnails.setOnCheckedChangeListener { _, isChecked ->
when (recyclerView.adapter) {
is PhotoDisplayDateAdapter -> {
lifecycleScope.launch {
dateAdapter?.run {
initGetCurrentDateList().let { list->
val filterThumbnailsAsync =
if (isChecked) list.filterThumbnailsAsync() else list
setData(filterThumbnailsAsync)
checkRefreshDisPlaySelected(list1 = filterThumbnailsAsync)
}
}
}
}
is PhotoDisplayDateChildAdapter -> {
lifecycleScope.launch {
sizeSortAdapter?.run {
initGetCurrentSizeList().let {
val filterThumbnailsAsync =
if (isChecked) it.filterRemoveThumbnailsAsync() else it
setData(filterThumbnailsAsync)
checkRefreshDisPlaySelected(list2 = filterThumbnailsAsync)
}
}
}
}
}
updateCurrentIsAllSelectStatus()
}
tvRecover.setOnClickListener {
showRecoveringDialog()
lifecycleScope.copySelectedFilesAsync(
@ -209,26 +257,63 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
when (it) {
SORT_ASC_DATE -> {
setDateAdapter()
dateAdapter?.setData(sortedByDatePositive)
sortReverse = false
lifecycleScope.launch {
initGetCurrentDateList().let {
val filterThumbnailsAsync =
if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
val sortByDayOldToNew =
Common.getSortByDayOldToNew(filterThumbnailsAsync)
dateAdapter?.setData(sortByDayOldToNew)
resetCurrentDateList(sortByDayOldToNew)
}
sortReverse = false
}
}
SORT_DESC_DATE -> {
setDateAdapter()
dateAdapter?.setData(sortByDateReverse)
sortReverse = true
lifecycleScope.launch {
initGetCurrentDateList().let {
val filterThumbnailsAsync =
if (switchHideThumbnails.isChecked) it.filterThumbnailsAsync() else it
val sortByDayNewToOld = Common.getSortByDayNewToOld(filterThumbnailsAsync)
dateAdapter?.setData(sortByDayNewToOld)
resetCurrentDateList(sortByDayNewToOld)
}
sortReverse = true
}
}
SORT_DESC_SIZE -> {
setSizeAdapter()
sizeSortAdapter?.setData(sortBySizeBigToSmall)
sortReverse = true
lifecycleScope.launch {
initGetCurrentSizeList().let {
val filterThumbnailsAsync =
if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
val sortBySizeBigToSmall =
Common.getSortBySizeBigToSmall(filterThumbnailsAsync)
sizeSortAdapter?.setData(sortBySizeBigToSmall)
resetCurrentSizeList(sortBySizeBigToSmall)
}
sortReverse = true
}
}
SORT_ASC_SIZE -> {
setSizeAdapter()
sizeSortAdapter?.setData(sortBySizeSmallToBig)
sortReverse = false
lifecycleScope.launch {
initGetCurrentSizeList().let {
val filterThumbnailsAsync =
if (switchHideThumbnails.isChecked) it.filterRemoveThumbnailsAsync() else it
val sortBySizeSmallToBig =
Common.getSortBySizeSmallToBig(filterThumbnailsAsync)
sizeSortAdapter?.setData(sortBySizeSmallToBig)
resetCurrentSizeList(sortBySizeSmallToBig)
}
sortReverse = false
}
}
}
}
@ -254,25 +339,50 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
when (binding.recyclerView.adapter) {
is PhotoDisplayDateAdapter -> {
val adapter = binding.recyclerView.adapter as PhotoDisplayDateAdapter
binding.tvSelectAll.isSelected = it == adapter.getTotalChildCount()
binding.tvSelectAll.isSelected = it == adapter.getTotalChildCount(false)
}
is PhotoDisplayDateChildAdapter -> {
binding.tvSelectAll.isSelected = it == binding.recyclerView.adapter?.itemCount
val adapter = binding.recyclerView.adapter as PhotoDisplayDateChildAdapter
binding.tvSelectAll.isSelected =
it == adapter.itemCount
}
}
}
}
private fun initGetCurrentSizeList(): List<ResultPhotosFiles> {
currentSizeList = currentSizeList ?: currentDateList?.flatMap { it.second }
return currentSizeList!!
}
private fun resetCurrentSizeList(currentList: List<ResultPhotosFiles>) {
currentSizeList = currentList
currentDateList = null
}
private fun initGetCurrentDateList(): List<Pair<String, List<ResultPhotosFiles>>> {
currentDateList = currentDateList ?: Common.getSortByDayNewToOldInit(currentSizeList!!)
return currentDateList!!
}
private fun resetCurrentDateList(currentList: List<Pair<String, List<ResultPhotosFiles>>> ) {
currentDateList = currentList
currentSizeList = null
}
/**
* 重置适配器 日期显示和大小显示适配器切换
*/
private fun setDateAdapter() {
binding.recyclerView.run {
adapter = dateAdapter?.apply { setColumns(columns) }
layoutManager = LinearLayoutManager(this@PhotoSortingActivity)
val bPx = 16.dpToPx(context)
setPadding(0, 0, 0, 0)
setPadding(0, 0, 0, 70.dpToPx(context))
clipToPadding = false
}
}
@ -280,7 +390,7 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
binding.recyclerView.run {
val aPx = 16.dpToPx(context)
val bPx = 6.dpToPx(context)
setPadding(aPx, 0, bPx, 0)
setPadding(aPx, 0, bPx, 70.dpToPx(context))
clipToPadding = false
layoutManager = GridLayoutManager(context, columns)
adapter = sizeSortAdapter
@ -293,6 +403,7 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
*/
private fun setFilter() {
//日期筛选
binding.run {
filterDateLayout.setOnClickListener {
setItemSelect(it as LinearLayout, true)
@ -304,40 +415,45 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
when (clickValue) {
data[0] -> {
filterDate = FILTER_DATE_ALL
titleDate.text = clickValue
startFilter()
}
data[1] -> {
filterDate = FILTER_DATE_1
titleDate.text = clickValue
startFilter()
}
data[2] -> {
filterDate = FILTER_DATE_6
titleDate.text = clickValue
startFilter()
}
data[3] -> {
filterDate = FILTER_DATE_24
titleDate.text = clickValue
startFilter()
}
data[4] -> {
filterDate = FILTER_DATE_CUSTOMER
if (showDialog)
showStartDateDialog(true,null)
showStartDateDialog(true, null)
else {
titleDate.text = clickValue
startFilter()
}
}
}
}, onResetDate = { isStart,currentDate ->
}, onResetDate = { isStart, currentDate ->
if (isStart) {
showStartDateDialog(false,currentDate)
showStartDateDialog(false, currentDate)
} else {
showEndDateDialog(false,currentDate)
showEndDateDialog(false, currentDate)
}
}) {
@ -360,6 +476,7 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
data,
0,
{ clickValue ->
titleSize.text = clickValue
when (clickValue) {
data[0] -> filterSize = FILTER_SIZE_ALL
data[1] -> filterSize = FILTER_SIZE_1
@ -430,8 +547,6 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
tvDelete.text = getString(R.string.delete_placeholder, selectedCounts)
tvRecover.text = getString(R.string.recover_placeholder, selectedCounts)
}
}
/**
@ -452,15 +567,10 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
)
.filterBySize(filterSizeCovert.first, filterSizeCovert.second)
.let { currentList ->
//对筛选后的数据与实际选中集合对比 ,得出筛选后显示的数据中的选中数据
val checkSelectListContain =
Common.checkSelectListContainDate(currentList, allSelectedSetList)
updateButtonCounts(checkSelectListContain.first)
viewModel.filterResetDisplayFlow(checkSelectListContain.second)
Common.showLog("筛选后重置 allSelectedSetList=${allSelectedSetList.size} filterSelectedSetList=${filterSelectedSetList.size} Thread=${Thread.currentThread().name}")
checkRefreshDisPlaySelected(list1 = currentList)
dateAdapter?.resetAllValue(null)
dateAdapter?.setData(currentList)
resetCurrentDateList(currentList)
}
@ -476,31 +586,58 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
)
.filterBySizeList(filterSizeCovert.first, filterSizeCovert.second)
.let { currentList ->
//对筛选后的数据与实际选中集合对比 ,得出筛选后显示的数据中的选中数据
val checkSelectListContain =
Common.checkSelectListContainSize(currentList, allSelectedSetList)
updateButtonCounts(checkSelectListContain.first)
viewModel.filterResetDisplayFlow(checkSelectListContain.second)
checkRefreshDisPlaySelected(list2 = currentList)
sizeSortAdapter?.setData(currentList)
resetCurrentSizeList(currentList)
}
}
}
}
/**
* 数据筛选或者缩略图切换显示后对比刷新实际显示的选中数据
*/
private fun checkRefreshDisPlaySelected(
list1: List<Pair<String, List<ResultPhotosFiles>>>? = null,
list2: List<ResultPhotosFiles>? = null
) {
lifecycleScope.launch {
list1?.let {
val checkSelectListContain =
Common.checkSelectListContainDateAsync(it, allSelectedSetList)
viewModel.filterResetDisplayFlow(checkSelectListContain.second)
}
list2?.let {
val checkSelectListContain =
Common.checkSelectListContainSize(it, allSelectedSetList)
viewModel.filterResetDisplayFlow(checkSelectListContain.second)
}
}
}
private fun filterSizeCovert(filterSize: Int): Pair<Long, Long> {
return when (filterSize) {
FILTER_SIZE_ALL -> Pair(-1L, -1L)
FILTER_SIZE_1 -> Pair(0L, 1.mbToBytes())
FILTER_SIZE_5 -> Pair(1L, 5.mbToBytes())
FILTER_SIZE_5 -> Pair(1.mbToBytes(), 5.mbToBytes())
FILTER_SIZE_OVER_5 -> Pair(5.mbToBytes(), Long.MAX_VALUE)
else -> Pair(-1L, -1L)
}
}
private fun showStartDateDialog(isNeedSetSelected: Boolean,currentDate: Date?) {
/**
* 显示筛选开始日期弹窗
*/
private fun showStartDateDialog(isNeedSetSelected: Boolean, currentDate: Date?) {
dialogCustomerDateStart = dialogCustomerDateStart ?: DatePickerDialogFragment(
this,
@ -514,16 +651,9 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
filterStartDate = selectedDate
// filterDatePopupWindows?.updateStartEndDate(start = selectedDate)
Log.d("showStartDateDialog", "isFirst=${isNeedSetSelected}--------")
showEndDateDialog(isNeedSetSelected,null)
showEndDateDialog(isNeedSetSelected, null)
// if (isFirst) {
// showEndDateDialog(true)
// } else {
// if (filterDate == FILTER_DATE_CUSTOMER) {
// startFilter()
// filterDatePopupWindows?.dismiss()
// }
// }
}
onClickCancel = {
@ -534,7 +664,10 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
}
private fun showEndDateDialog(isNeedSetSelected: Boolean,currentDate: Date?) {
/**
* 显示筛选结束日期弹窗
*/
private fun showEndDateDialog(isNeedSetSelected: Boolean, currentDate: Date?) {
dialogCustomerDateEnd = dialogCustomerDateEnd ?: DatePickerDialogFragment(
this,
getString(R.string.end_date),
@ -546,12 +679,13 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
filterStartDate?.let { setRangeDate(it) }
onPickerChooseListener = { selectedDate ->
filterEndDate = selectedDate
if(isNeedSetSelected){
if (isNeedSetSelected) {
filterDatePopupWindows?.updateSelectPos(4, filterStartDate!!, filterEndDate!!)
startFilter()
filterDatePopupWindows?.dismiss()
}else{
filterDatePopupWindows?.updateStartEndDate(filterStartDate,filterEndDate)
binding.titleDate.text = resources.getStringArray(R.array.filter_date)[4]
} else {
filterDatePopupWindows?.updateStartEndDate(filterStartDate, filterEndDate)
if (filterDate == FILTER_DATE_CUSTOMER) {
startFilter()
filterDatePopupWindows?.dismiss()
@ -566,6 +700,10 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
}
}
/**
* 显示恢复中弹窗
*/
private fun showRecoveringDialog() {
dialogRecovering =
dialogRecovering ?: RecoveringDialogFragment(filterSelectedSetList.size) {
@ -574,6 +712,10 @@ class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
dialogRecovering?.show(supportFragmentManager, "")
}
/**
* 显示删除中弹窗
*/
private fun showDeletingDialog() {
dialogDeleting = dialogDeleting ?: DeletingDialogFragment(filterSelectedSetList.size) {
complete()

View File

@ -10,9 +10,23 @@ data class ResultPhotosFiles(
val name: String,
val path: String? = null,
val size: Long, // 字节
val sizeString: String,
val lastModified: Long, // 时间戳
var resolution: String // 尺寸
) : Parcelable {
val targetFile: File?
get() = path?.let { File(it) }
//是否为缩略图文件(宽高任一小于 256
val isThumbnail: Boolean
get() {
val parts = resolution.lowercase().split("*").mapNotNull {
it.trim().toIntOrNull()
}
if (parts.size == 2) {
val (width, height) = parts
return width < 256 || height < 256
}
return false
}
}

View File

@ -111,7 +111,7 @@ class ScanningActivity : BaseActivity<ActivityScanningBinding>() {
val total = 800
lifecycleScope.launch {
val root = Environment.getExternalStorageDirectory()
ScanManager.scanAllDocuments(root, type = scanType).flowOn(Dispatchers.IO).collect {
ScanManager.scanAllDocuments(this@ScanningActivity,root, type = scanType).flowOn(Dispatchers.IO).collect {
when (it) {
is ScanState.Progress -> {
updateProgress(it)
@ -131,7 +131,7 @@ class ScanningActivity : BaseActivity<ActivityScanningBinding>() {
lifecycleScope.launch {
val root = Environment.getExternalStorageDirectory()
ScanManager.scanHiddenPhotoAsync(root, type = scanType).flowOn(Dispatchers.IO).collect {
ScanManager.scanHiddenPhotoAsync(this@ScanningActivity,root, type = scanType).flowOn(Dispatchers.IO).collect {
when (it) {
is ScanState.Progress -> {
updateProgress(it)

View File

@ -12,6 +12,8 @@ import com.ux.video.file.filerecovery.App
import com.ux.video.file.filerecovery.R
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Date
import java.util.Locale
import kotlin.collections.sortedBy
@ -40,9 +42,7 @@ object Common {
/**
* 默认按照日期分类将最新的排前面 降序
*/
fun getSortByDayNewToOld(list: ArrayList<ResultPhotosFiles>): List<Pair<String, List<ResultPhotosFiles>>> {
fun getSortByDayNewToOldInit(list: List<ResultPhotosFiles>): List<Pair<String, List<ResultPhotosFiles>>> {
val grouped = list.groupBy {
dateFormat.format(Date(it.lastModified))
}
@ -52,6 +52,16 @@ object Common {
return parentData
}
fun getSortByDayNewToOld(
list: List<Pair<String, List<ResultPhotosFiles>>>
): List<Pair<String, List<ResultPhotosFiles>>> {
return list.sortedByDescending { pair ->
dateFormat.parse(pair.first)?.time ?: 0L
}
}
/**
* 按照日期排序 时间最早的排前面 升序
*
@ -62,14 +72,14 @@ object Common {
/**
* 按照文件大小排序将最大的排前面 降序
*/
fun getSortBySizeBigToSmall(list: ArrayList<ResultPhotosFiles>) = list.sortedByDescending {
fun getSortBySizeBigToSmall(list: List<ResultPhotosFiles>) = list.sortedByDescending {
it.size
}
/**
* 按照文件大小排序将最小的排前面 升序
*/
fun getSortBySizeSmallToBig(list: ArrayList<ResultPhotosFiles>) = list.sortedBy {
fun getSortBySizeSmallToBig(list: List<ResultPhotosFiles>) = list.sortedBy {
it.size
}
@ -81,16 +91,19 @@ object Common {
if (size < 1024) {
return "$size B"
}
val kb = size / 1024.0
if (kb < 1024) {
return String.format(context.getString(R.string.size_kb), kb)
return String.format(Locale.CHINA, context.getString(R.string.size_kb), kb)
}
val mb = kb / 1024.0
if (mb < 1024) {
return String.format(context.getString(R.string.size_kb), mb)
return String.format(Locale.CHINA, context.getString(R.string.size_mb), mb)
}
val gb = mb / 1024.0
return String.format(context.getString(R.string.size_gb), gb)
return String.format(Locale.CHINA, context.getString(R.string.size_gb), gb)
}
@ -130,12 +143,7 @@ object Common {
/**
* 设置全部子View的选中
*/
// fun setItemSelect(view: ViewGroup, boolean: Boolean) {
// for (i in 0 until view.childCount) {
// val child = view.getChildAt(i)
// child.isSelected = boolean
// }
// }
fun setItemSelect(view: View, selected: Boolean) {
view.isSelected = selected
if (view is ViewGroup) {
@ -154,14 +162,14 @@ object Common {
*/
fun checkSelectListContainDate(
list: List<Pair<String, List<ResultPhotosFiles>>>,
selected: Set<String>
): Pair<Int, MutableSet<String>> {
val currentSelected = mutableSetOf<String>()
selected: Set<ResultPhotosFiles>
): Pair<Int, MutableSet<ResultPhotosFiles>> {
val currentSelected = mutableSetOf<ResultPhotosFiles>()
val totalSelectedCount = list.sumOf { pair ->
pair.second.count {
val isSelected = it.path in selected
if (isSelected) currentSelected.add(it.path.toString())
val isSelected = it in selected
if (isSelected) currentSelected.add(it)
isSelected
}
}
@ -170,21 +178,75 @@ object Common {
return totalSelectedCount to currentSelected
}
suspend fun checkSelectListContainDateAsync(
list: List<Pair<String, List<ResultPhotosFiles>>>,
selected: Set<ResultPhotosFiles>
): Pair<Int, MutableSet<ResultPhotosFiles>> = withContext(Dispatchers.Default) {
val currentSelected = mutableSetOf<ResultPhotosFiles>()
var totalSelectedCount = 0
fun checkSelectListContainSize(
list: List<ResultPhotosFiles>,
selected: Set<String>
): Pair<Int, MutableSet<String>> {
val currentSelected = mutableSetOf<String>()
val totalSelectedCount = list.count {
val isSelected = it.path in selected
if (isSelected) currentSelected.add(it.path.toString())
isSelected
// 高效遍历外层 + 内层列表
for (pair in list) {
for (file in pair.second) {
if (file in selected) {
totalSelectedCount++
currentSelected.add(file)
}
}
}
showLog("-------totalSelectedCount 222=${totalSelectedCount}")
return totalSelectedCount to currentSelected
totalSelectedCount to currentSelected
}
// fun checkSelectListContainSize(
// list: List<ResultPhotosFiles>,
// selected: Set<String>
// ): Pair<Int, MutableSet<String>> {
// val currentSelected = mutableSetOf<String>()
// val totalSelectedCount = list.count {
// val isSelected = it.path in selected
// if (isSelected) currentSelected.add(it.path.toString())
// isSelected
// }
// showLog("-------totalSelectedCount 222=${totalSelectedCount}")
// return totalSelectedCount to currentSelected
// }
suspend fun checkSelectListContainSize(
list: List<ResultPhotosFiles>,
selected: Set<ResultPhotosFiles>
): Pair<Int, MutableSet<ResultPhotosFiles>> = withContext(Dispatchers.Default) {
val currentSelected = mutableSetOf<ResultPhotosFiles>()
var totalSelectedCount = 0
// 高效遍历外层 + 内层列表
for (pair in list) {
if (pair in selected) {
totalSelectedCount++
currentSelected.add(pair)
}
}
totalSelectedCount to currentSelected
}
/**
* 去掉缩略图的集合
*/
suspend fun filterThumbnailsAsync(
originalList: MutableList<Pair<String, List<ResultPhotosFiles>>>
): List<Pair<String, List<ResultPhotosFiles>>> = withContext(Dispatchers.Default) {
originalList.asSequence()
.map { (key, files) ->
key to files.asSequence().filter { !it.isThumbnail }.toList()
}
.filter { it.second.isNotEmpty() }
.toList()
}
fun getFormatDate(time: Long): String {
return dateFormat.format(Date(time))
}

View File

@ -8,6 +8,8 @@ import android.os.Parcelable
import android.util.TypedValue
import androidx.recyclerview.widget.RecyclerView
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Date
object ExtendFunctions {
@ -176,8 +178,32 @@ object ExtendFunctions {
}
}
fun Int.mbToBytes(): Long {
return this * 1024L * 1024L
return this * 1000L * 1000L
}
/**
* 移除掉缩略图后的数据
*/
suspend fun List<Pair<String, List<ResultPhotosFiles>>>.filterThumbnailsAsync(): List<Pair<String, List<ResultPhotosFiles>>> =
withContext(Dispatchers.Default) {
this@filterThumbnailsAsync.asSequence()
.mapNotNull { (key, files) ->
val filtered = files.asSequence().filter { !it.isThumbnail }.toList()
if (filtered.isNotEmpty()) key to filtered else null
}
.toList()
}
/**
* 移除掉缩略图后的数据
*/
suspend fun List<ResultPhotosFiles>.filterRemoveThumbnailsAsync(): List<ResultPhotosFiles> =
withContext(Dispatchers.Default) {
this@filterRemoveThumbnailsAsync.asSequence()
.filter { !it.isThumbnail } // 去掉 isThumbnail = true 的项
.toList()
}
}

View File

@ -1,8 +1,14 @@
package com.ux.video.file.filerecovery.utils
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.OpenableColumns
import android.util.Log
import androidx.annotation.RequiresApi
import com.ux.video.file.filerecovery.photo.ResultPhotos
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import com.ux.video.file.filerecovery.result.ScanningActivity
@ -48,6 +54,7 @@ object ScanManager {
* 扫描所有文件
*/
fun scanAllDocuments(
context: Context,
root: File, maxDepth: Int = 5,
maxFiles: Int = 5000,
type: Int
@ -100,6 +107,7 @@ object ScanManager {
name = file.name,
path = file.absolutePath,
size = file.length(),
sizeString = android.text.format.Formatter.formatFileSize(context, file.length()),
lastModified = file.lastModified(),
resolution = getImageSize(file).run {
"$first*$second"
@ -110,7 +118,8 @@ object ScanManager {
}
emit(ScanState.Complete(ArrayList(map)))
}
private fun getImageSize(file: File): Pair<Int, Int> {
private fun getImageSize(file: File): Pair<Int, Int> {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options)
@ -125,12 +134,14 @@ object ScanManager {
* @param maxFiles // 最大收集的文件数
*/
fun scanHiddenPhotoAsync(
context: Context,
root: File, maxDepth: Int = 5,
maxFiles: Int = 5000, type: Int
): Flow<ScanState> = flow {
val result = mutableMapOf<String, MutableList<File>>()
var fileCount = 0
@RequiresApi(Build.VERSION_CODES.R)
suspend fun scanDir(dir: File, depth: Int, insideHidden: Boolean = false) {
if (!dir.exists() || !dir.isDirectory) return
if (depth > maxDepth || fileCount >= maxFiles) return
@ -181,17 +192,30 @@ object ScanManager {
name = file.name,
path = file.absolutePath,
size = file.length(),
sizeString = android.text.format.Formatter.formatFileSize(context, file.length()),
lastModified = file.lastModified(),
resolution = getImageSize(file).run {
"$first*$second"
}
)
}
ResultPhotos(dir, ArrayList(resultPhotosFilesList))
}
emit(ScanState.Complete(ArrayList(map)))
}
private fun getFileSizeByMediaStore(context: Context, file: File): Long {
val uri = Uri.fromFile(file)
context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)
?.use { cursor ->
val index = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor.moveToFirst()) {
return cursor.getLong(index)
}
}
return file.length() // fallback
}
private fun isFormatFile(file: File, types: List<String>): Boolean {
val ext = file.extension.lowercase()
@ -223,7 +247,7 @@ object ScanManager {
* @param folder "AllRecovery/Photo"
*/
fun CoroutineScope.copySelectedFilesAsync(
selectedSet: Set<String>,
selectedSet: Set<ResultPhotosFiles>,
rootDir: File = Common.rootDir,
folder: String,
onProgress: (currentCounts: Int, fileName: String, success: Boolean) -> Unit,
@ -232,8 +256,8 @@ object ScanManager {
launch(Dispatchers.IO) {
val targetDir = File(rootDir, folder)
if (!targetDir.exists()) targetDir.mkdirs()
selectedSet.forEachIndexed { index, path ->
val srcFile = File(path)
selectedSet.forEachIndexed { index, resultPhotosFiles ->
val srcFile = File(resultPhotosFiles.path!!)
if (srcFile.exists() && srcFile.isFile) {
val destFile = File(targetDir, srcFile.name)
var success = false
@ -265,23 +289,23 @@ object ScanManager {
*
*/
fun CoroutineScope.deleteFilesAsync(
selectedSet: Set<String>,
onProgress: (currentCounts: Int, path: String, success: Boolean) -> Unit,
selectedSet: Set<ResultPhotosFiles>,
onProgress: (currentCounts: Int, fileName: String, success: Boolean) -> Unit,
onComplete: (currentCounts: Int) -> Unit
) {
launch(Dispatchers.IO) {
var deletedCount = 0
selectedSet.forEachIndexed { index, path ->
selectedSet.forEachIndexed { index, resultPhotosFiles ->
try {
val file = File(path)
val file = File(resultPhotosFiles.path!!)
if (file.exists() && file.delete()) {
deletedCount++
}
withContext(Dispatchers.Main) {
onProgress(index + 1, path, true)
onProgress(index + 1, file.name, true)
}
} catch (e: Exception) {
onProgress(index + 1, path, false)
onProgress(index + 1, resultPhotosFiles.path!!, false)
}
}
withContext(Dispatchers.Main) {

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.ux.video.file.filerecovery.photo.ResultPhotos
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -19,24 +20,27 @@ class ScanRepository : ViewModel() {
private val _selectedLiveData = MutableLiveData<Set<String>>(emptySet())
val selectedLiveData: LiveData<Set<String>> = _selectedLiveData
private val _selectedLiveData = MutableLiveData<Set<ResultPhotosFiles>>(emptySet())
val selectedLiveData: LiveData<Set<ResultPhotosFiles>> = _selectedLiveData
// 当前筛选显示的选中项
private val _selectedDisplayLiveData = MutableLiveData<Set<String>>(emptySet())
val selectedDisplayLiveData: LiveData<Set<String>> = _selectedDisplayLiveData
private val _selectedDisplayLiveData = MutableLiveData<Set<ResultPhotosFiles>>(emptySet())
val selectedDisplayLiveData: LiveData<Set<ResultPhotosFiles>> = _selectedDisplayLiveData
fun toggleSelection(isAdd: Boolean, path: String) {
fun toggleSelection(isAdd: Boolean, resultPhotosFiles: ResultPhotosFiles) {
val current = _selectedLiveData.value?.toMutableSet() ?: mutableSetOf()
val currentDisplay = _selectedDisplayLiveData.value?.toMutableSet() ?: mutableSetOf()
resultPhotosFiles.let {
if (isAdd) {
current.add(path)
currentDisplay.add(path)
} else {
current.remove(path)
currentDisplay.remove(path)
if (isAdd) {
current.add(it)
currentDisplay.add(it)
} else {
current.remove(it)
currentDisplay.remove(it)
}
}
Common.showLog( "toggleSelection------------ _selectedDisplayFlow=${_selectedDisplayLiveData.value?.size} _selectedFlow=${_selectedLiveData.value?.size} ")
_selectedLiveData.value = current.toSet()
_selectedDisplayLiveData.value = currentDisplay.toSet()
@ -47,9 +51,9 @@ class ScanRepository : ViewModel() {
/**
* 数据筛选后重置当前显示的选中集合
* 数据筛选后或者缩略图显示开关切换后 重置当前显示的选中集合
*/
fun filterResetDisplayFlow(list: MutableSet<String>){
fun filterResetDisplayFlow(list: MutableSet<ResultPhotosFiles>){
_selectedDisplayLiveData.value = list.toSet()
Common.showLog( "筛选后重置 _selectedDisplayFlow=${_selectedDisplayLiveData.value?.size} _selectedFlow=${_selectedLiveData.value?.size} ")
@ -57,13 +61,10 @@ class ScanRepository : ViewModel() {
fun checkIsSelect(path: String): Boolean {
val current = _selectedDisplayLiveData.value
return current?.contains(path) == true
fun checkIsSelect(resultPhotosFiles: ResultPhotosFiles): Boolean {
val current = _selectedLiveData.value
return current?.contains(resultPhotosFiles) == true
}
companion object {
val instance by lazy { ScanRepository() }
}
}

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:width="28dp"
android:height="28dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M518,87.1c-235.3,0 -426,190.7 -426,426s190.7,426 426,426 426,-190.7 426,-426 -190.7,-426 -426,-426zM518,855.1c-188.9,0 -342.1,-153.2 -342.1,-342.1S329,170.9 518,170.9s342.1,153.2 342.1,342.1 -153.2,342.1 -342.1,342.1z"
android:fillColor="@color/white"/>
android:fillColor="@color/black"/>
</vector>

View File

@ -55,6 +55,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/date"
android:maxWidth="85dp"
android:maxLines="1"
android:ellipsize="end"
android:id="@+id/title_date"
android:textColor="@color/selector_black_blue"
android:textSize="16sp"
app:fontType="bold" />
@ -80,6 +84,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/size"
android:maxWidth="85dp"
android:maxLines="1"
android:id="@+id/title_size"
android:ellipsize="end"
android:textColor="@color/selector_black_blue"
android:textSize="16sp"
app:fontType="bold" />