From 81907104630793f69cf7353fdf12a32b97d4f604 Mon Sep 17 00:00:00 2001 From: Fernando Sanz Date: Wed, 19 Jan 2022 16:39:41 +0100 Subject: [PATCH] Selected items controlled and hide/show FAB button --- .../adapters/filelist/FileListAdapter.kt | 39 +++++- .../adapters/filelist/SelectableAdapter.kt | 86 ++++++++++++ .../ui/files/filelist/MainFileListFragment.kt | 129 ++++++++++++++++-- 3 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/SelectableAdapter.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/FileListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/FileListAdapter.kt index 80a271b1068..e7f0c53d1b8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/FileListAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/FileListAdapter.kt @@ -51,7 +51,7 @@ class FileListAdapter( private val isShowingJustFolders: Boolean, private val layoutManager: StaggeredGridLayoutManager, private val listener: FileListAdapterListener, -) : RecyclerView.Adapter() { +) : SelectableAdapter() { var files = mutableListOf() private var account: Account? = AccountUtils.getCurrentOwnCloudAccount(context) @@ -129,6 +129,17 @@ class FileListAdapter( } } + fun getCheckedItems(): List { + val checkedItems = mutableListOf() + val checkedPositions = getSelectedItems() + + for (i in checkedPositions) { + checkedItems.add(files[i] as OCFile) + } + + return checkedItems + } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val viewType = getItemViewType(position) @@ -202,13 +213,34 @@ class FileListAdapter( } - holder.itemView.setOnClickListener { listener.clickItem(file) } + holder.itemView.setOnClickListener { + listener.clickItem( + ocFile = file, + position = position + ) + } + + holder.itemView.setOnLongClickListener { + listener.longClickItem( + ocFile = file, + position = position + ) + } val checkBoxV = holder.itemView.findViewById(R.id.custom_checkbox).apply { visibility = View.GONE } holder.itemView.setBackgroundColor(Color.WHITE) + if (isSelected(position)) { + holder.itemView.setBackgroundColor(ContextCompat.getColor(context, R.color.selected_item_background)) + checkBoxV.setImageResource(R.drawable.ic_checkbox_marked) + } else { + holder.itemView.setBackgroundColor(Color.WHITE) + checkBoxV.setImageResource(R.drawable.ic_checkbox_blank_outline) + } + checkBoxV.visibility = View.VISIBLE + if (file.isFolder) { //Folder fileIcon.setImageResource( @@ -323,7 +355,8 @@ class FileListAdapter( } interface FileListAdapterListener { - fun clickItem(ocFile: OCFile) + fun clickItem(ocFile: OCFile, position: Int) + fun longClickItem(ocFile: OCFile, position: Int): Boolean = true } inner class GridViewHolder(val binding: GridItemBinding) : RecyclerView.ViewHolder(binding.root) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/SelectableAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/SelectableAdapter.kt new file mode 100644 index 00000000000..35c22b974a3 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/adapters/filelist/SelectableAdapter.kt @@ -0,0 +1,86 @@ +/** + * ownCloud Android client application + * + * @author Fernando Sanz Velasco + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.presentation.adapters.filelist + +import android.util.SparseBooleanArray +import androidx.recyclerview.widget.RecyclerView + +abstract class SelectableAdapter : + RecyclerView.Adapter() { + private val selectedItems: SparseBooleanArray = SparseBooleanArray() + + /** + * Indicates if the item at position position is selected + * @param position Position of the item to check + * @return true if the item is selected, false otherwise + */ + fun isSelected(position: Int): Boolean { + return getSelectedItems().contains(position) + } + + /** + * Toggle the selection status of the item at a given position + * @param position Position of the item to toggle the selection status for + */ + fun toggleSelection(position: Int) { + if (selectedItems[position, false]) { + selectedItems.delete(position) + } else { + selectedItems.put(position, true) + } + notifyItemChanged(position) + } + + /** + * Clear the selection status for all items + */ + fun clearSelection() { + val selection = getSelectedItems() + selectedItems.clear() + for (i in selection) { + notifyItemChanged(i) + } + } + + /** + * Count the selected items + * @return Selected items count + */ + val selectedItemCount: Int + get() = selectedItems.size() + + /** + * Indicates the list of selected items + * @return List of selected items ids + */ + fun getSelectedItems(): List { + val items: MutableList = ArrayList(selectedItems.size()) + for (i in 0 until selectedItems.size()) { + items.add(selectedItems.keyAt(i)) + } + return items + } + + companion object { + private val TAG = SelectableAdapter::class.java.simpleName + } + +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt index e4f06c155d9..c428f45fea7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/filelist/MainFileListFragment.kt @@ -24,10 +24,15 @@ import android.content.Context import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView @@ -44,6 +49,7 @@ import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.cancel import com.owncloud.android.extensions.parseError import com.owncloud.android.extensions.showMessageInSnackbar +import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.presentation.UIResult import com.owncloud.android.presentation.adapters.filelist.FileListAdapter import com.owncloud.android.presentation.observers.EmptyDataObserver @@ -57,15 +63,15 @@ import com.owncloud.android.presentation.ui.files.SortType import com.owncloud.android.presentation.ui.files.ViewType import com.owncloud.android.presentation.ui.files.createfolder.CreateFolderDialogFragment import com.owncloud.android.presentation.viewmodels.files.FilesViewModel +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.fragment.FileFragment -import com.owncloud.android.ui.fragment.OCFileListFragment import com.owncloud.android.utils.ColumnQuantity import com.owncloud.android.utils.FileStorageUtils import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.java.KoinJavaComponent.get import timber.log.Timber import java.io.File -import java.lang.ClassCastException class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.SortOptionsListener, SortOptionsView.CreateFolderListener, CreateFolderDialogFragment.CreateFolderListener, SearchView.OnQueryTextListener { @@ -81,7 +87,6 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor private var containerActivity: FileFragment.ContainerActivity? = null private var files: List = emptyList() - private var miniFabClicked = false private lateinit var layoutManager: StaggeredGridLayoutManager private lateinit var fileListAdapter: FileListAdapter @@ -91,6 +96,10 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor private var file: OCFile? = null + var actionMode: ActionMode? = null + private var statusBarColorActionMode: Int? = null + private var statusBarColor: Int? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -142,6 +151,8 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor } private fun initViews() { + setHasOptionsMenu(true) + statusBarColorActionMode = ContextCompat.getColor(requireContext(), R.color.action_mode_status_bar_background) //Set view and footer correctly if (mainFileListViewModel.isGridModeSetAsPreferred()) { @@ -164,15 +175,28 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor isShowingJustFolders = isShowingJustFolders(), listener = object : FileListAdapter.FileListAdapterListener { - override fun clickItem(ocFile: OCFile) { - if (ocFile.isFolder) { - file = ocFile - mainFileListViewModel.listDirectory(ocFile) - // TODO Manage animation listDirectoryWithAnimationDown - } else { /// Click on a file - // TODO Click on a file + override fun clickItem(ocFile: OCFile, position: Int) { + if (actionMode != null) { + toggleSelection(position) + } else { + if (ocFile.isFolder) { + file = ocFile + mainFileListViewModel.listDirectory(ocFile) + // TODO Manage animation listDirectoryWithAnimationDown + } else { /// Click on a file + // TODO Click on a file + } + } + } + + override fun longClickItem(ocFile: OCFile, position: Int): Boolean { + if (actionMode == null) { + actionMode = (activity as AppCompatActivity).startSupportActionMode(actionModeCallback) } + toggleSelection(position) + return true } + }) binding.recyclerViewMainFileList.adapter = fileListAdapter @@ -191,6 +215,20 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor } } + private fun toggleSelection(position: Int) { + fileListAdapter.toggleSelection(position) + fileListAdapter.selectedItemCount.also { + if (it == 0) { + actionMode?.finish() + } else { + actionMode?.apply { + title = it.toString() + invalidate() + } + } + } + } + private fun subscribeToViewModels() { // Observe the action of retrieving the list of files from DB. mainFileListViewModel.getFilesListStatusLiveData.observe(viewLifecycleOwner, Event.EventObserver { @@ -505,5 +543,76 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.Sor return MainFileListFragment().apply { arguments = args } } } + + private val actionModeCallback: ActionMode.Callback = object : ActionMode.Callback { + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + actionMode = mode + + val inflater = requireActivity().menuInflater + inflater.inflate(R.menu.file_actions_menu, menu) + mode?.invalidate() + + //set gray color + val window = activity?.window + statusBarColor = window?.statusBarColor ?: -1 + + //hide FAB in multi selection mode + binding.fabMain.visibility = View.GONE + (containerActivity as FileDisplayActivity).showBottomNavBar(false) + + // Hide sort options view in multi-selection mode + binding.optionsLayout.visibility = View.GONE + + return true + } + + /** + * Updates available action in menu depending on current selection. + */ + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val checkedFiles = fileListAdapter.getCheckedItems() + val checkedCount = checkedFiles.size + val title = resources.getQuantityString( + R.plurals.items_selected_count, + checkedCount, + checkedCount + ) + mode?.title = title + val fileMenuFilter = FileMenuFilter(checkedFiles, (requireActivity() as FileActivity).account, containerActivity, activity) + + fileMenuFilter.filter( + menu, + false, + true, + fileListOption?.isAvailableOffline() ?: false, + fileListOption?.isSharedByLink() ?: false + ) + + return true + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + return false + } + + override fun onDestroyActionMode(mode: ActionMode?) { + + actionMode = null + + // reset to previous color + requireActivity().window.statusBarColor = statusBarColor!! + + // show FAB on multi selection mode exit + setFabEnabled(true) + + (containerActivity as FileDisplayActivity).showBottomNavBar(true) + + // Show sort options view when multi-selection mode finish + binding.optionsLayout.visibility = View.VISIBLE + + fileListAdapter.clearSelection() + } + } }