[GSoC] Saved Image Fragment Scroll State (#4528)

* Saved Image Fragment Scroll State

* Fix delete image

* Fixed Delete bug

* Changed custom selector icon
This commit is contained in:
Aditya-Srivastav 2021-08-08 18:38:11 +05:30 committed by Aditya Srivastava
parent 267b6b2a1b
commit 658a7ec3d7
11 changed files with 186 additions and 83 deletions

View file

@ -1,19 +1,7 @@
package fr.free.nrw.commons.customselector.helper
import android.content.Context
import com.mapbox.android.core.FileUtils
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.filepicker.Constants
import timber.log.Timber
import java.io.*
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.LinkedHashMap
/**
* Image Helper object, includes all the static functions required by custom selector.
@ -24,7 +12,7 @@ object ImageHelper {
/**
* Returns the list of folders from given image list.
*/
fun folderListFromImages(images: List<Image>): List<Folder> {
fun folderListFromImages(images: List<Image>): ArrayList<Folder> {
val folderMap: MutableMap<Long, Folder> = LinkedHashMap()
for (image in images) {
val bucketId = image.bucketId
@ -61,6 +49,17 @@ object ImageHelper {
return list.indexOf(image)
}
/**
* getIndex: Returns the index of image in given list.
*/
fun getIndexFromId(list: ArrayList<Image>, imageId: Long): Int {
for(i in list){
if(i.id == imageId)
return list.indexOf(i)
}
return 0;
}
/**
* Gets the list of indices from the master list.
*/

View file

@ -1,5 +1,5 @@
package fr.free.nrw.commons.customselector.listeners
interface FolderClickListener {
fun onFolderClick(folderId: Long, folderName: String)
fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long)
}

View file

@ -11,6 +11,7 @@ import com.bumptech.glide.Glide
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
class FolderAdapter(
/**
@ -43,16 +44,38 @@ class FolderAdapter(
*/
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val folder = folders[position]
val count = folder.images.size
val previewImage = folder.images[0]
Glide.with(context).load(previewImage.uri).into(holder.image)
holder.name.text = folder.name
holder.count.text = count.toString()
holder.itemView.setOnClickListener{
itemClickListener.onFolderClick(folder.bucketId, folder.name)
}
val toBeRemoved = ArrayList<Image>()
//todo load image thumbnail.
for(image in folder.images) {
// Remove all the top images that do not exist anymore
if(context.contentResolver.getType(image.uri) == null){
// File not found
toBeRemoved.add(image)
} else {
break
}
}
holder.image.setImageDrawable (null)
folder.images.removeAll(toBeRemoved)
val count = folder.images.size
if(count == 0) {
// Folder is empty, remove folder from the adapter.
holder.itemView.post{
val updatePosition = folders.indexOf(folder)
folders.removeAt(updatePosition)
notifyItemRemoved(updatePosition)
notifyItemRangeChanged(updatePosition, folders.size)
}
} else {
val previewImage = folder.images[0]
Glide.with(context).load(previewImage.uri).into(holder.image)
holder.name.text = folder.name
holder.count.text = count.toString()
holder.itemView.setOnClickListener {
itemClickListener.onFolderClick(folder.bucketId, folder.name, 0)
}
}
}
/**

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons.customselector.ui.adapter
import android.content.Context
import android.view.ViewGroup
import fr.free.nrw.commons.R
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
@ -11,6 +10,7 @@ import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
@ -59,7 +59,7 @@ class ImageAdapter(
* Create View holder.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false)
val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false)
return ImageViewHolder(itemView)
}
@ -68,36 +68,46 @@ class ImageAdapter(
*/
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val image=images[position]
val selectedIndex = ImageHelper.getIndex(selectedImages,image)
val isSelected = selectedIndex != -1
if(isSelected){
holder.itemSelected(selectedIndex+1)
}
else {
holder.itemUnselected();
}
Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image)
imageLoader.queryAndSetView(holder,image)
holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position)
holder.image.setImageDrawable (null)
if (context.contentResolver.getType(image.uri) == null) {
// Image does not exist anymore, update adapter.
holder.itemView.post {
val updatedPosition = images.indexOf(image)
images.remove(image)
notifyItemRemoved(updatedPosition)
notifyItemRangeChanged(updatedPosition, images.size)
}
} else {
val selectedIndex = ImageHelper.getIndex(selectedImages, image)
val isSelected = selectedIndex != -1
if (isSelected) {
holder.itemSelected(selectedIndex + 1)
} else {
holder.itemUnselected();
}
Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image)
imageLoader.queryAndSetView(holder, image)
holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position)
}
}
}
/**
* Handle click event on an image, update counter on images.
*/
private fun selectOrRemoveImage(holder:ImageViewHolder, position:Int){
val clickedIndex = ImageHelper.getIndex(selectedImages,images[position])
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
val clickedIndex = ImageHelper.getIndex(selectedImages, images[position])
if (clickedIndex != -1) {
selectedImages.removeAt(clickedIndex)
notifyItemChanged(position,ImageUnselected())
notifyItemChanged(position, ImageUnselected())
val indexes = ImageHelper.getIndexList(selectedImages, images)
for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated())
}
} else {
if(holder.isItemUploaded()){
Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Already Uploaded image", Toast.LENGTH_SHORT).show()
} else {
selectedImages.add(images[position])
notifyItemChanged(position, ImageSelectedOrUpdated())
@ -109,7 +119,7 @@ class ImageAdapter(
/**
* Initialize the data set.
*/
fun init(newImages:List<Image>) {
fun init(newImages: List<Image>) {
val oldImageList:ArrayList<Image> = images
val newImageList:ArrayList<Image> = ArrayList(newImages)
val diffResult = DiffUtil.calculateDiff(
@ -128,6 +138,10 @@ class ImageAdapter(
return images.size
}
fun getImageIdAt(position: Int): Long {
return images.get(position).id
}
/**
* Image view holder.
*/

View file

@ -7,7 +7,6 @@ import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.ViewModelProvider
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
@ -17,7 +16,7 @@ import fr.free.nrw.commons.theme.BaseActivity
import java.io.File
import javax.inject.Inject
class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener, FragmentManager.OnBackStackChangedListener {
class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener {
/**
* View model.
@ -58,10 +57,11 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
setupViews()
// Open folder if saved in prefs.
if(prefs.contains("FolderId")){
val lastOpenFolderId: Long = prefs.getLong("FolderId", 0L)
val lastOpenFolderName: String? = prefs.getString("FolderName", null)
lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it) }
if(prefs.contains(FOLDER_ID)){
val lastOpenFolderId: Long = prefs.getLong(FOLDER_ID, 0L)
val lastOpenFolderName: String? = prefs.getString(FOLDER_NAME, null)
val lastItemId: Long = prefs.getLong(ITEM_ID, 0)
lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it, lastItemId) }
}
}
@ -107,9 +107,9 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
/**
* override on folder click, change the toolbar title on folder click.
*/
override fun onFolderClick(folderId: Long, folderName: String) {
override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, ImageFragment.newInstance(folderId))
.add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId))
.addToBackStack(null)
.commit()
@ -172,25 +172,27 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
super.onBackPressed()
val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if(fragment != null && fragment is FolderFragment){
isImageFragmentOpen = false
changeTitle(getString(R.string.custom_selector_title))
}
}
/**
* On activity destroy
* If image fragment is open, overwrite its attributes otherwise discard the values.
*/
override fun onDestroy() {
if(isImageFragmentOpen){
prefs.edit().putLong("FolderId", bucketId).putString("FolderName", bucketName).apply()
prefs.edit().putLong(FOLDER_ID, bucketId).putString(FOLDER_NAME, bucketName).apply()
} else {
prefs.edit().remove("FolderId").remove("FolderName").apply()
prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply()
}
super.onDestroy()
}
/**
* Called whenever the contents of the back stack change.
*/
override fun onBackStackChanged() {
if(supportFragmentManager.backStackEntryCount == 0) {
isImageFragmentOpen = false
}
companion object {
const val FOLDER_ID : String = "FolderId"
const val FOLDER_NAME : String = "FolderName"
const val ITEM_ID : String = "ItemId"
}
}

View file

@ -14,6 +14,7 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
@ -55,6 +56,11 @@ class FolderFragment : CommonsDaggerSupportFragment() {
*/
private lateinit var gridLayoutManager: GridLayoutManager
/**
* Folder List.
*/
private lateinit var folders : ArrayList<Folder>
/**
* Companion newInstance.
*/
@ -102,7 +108,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
*/
private fun handleResult(result: Result) {
if(result.status is CallbackStatus.SUCCESS){
val folders = ImageHelper.folderListFromImages(result.images)
folders = ImageHelper.folderListFromImages(result.images)
folderAdapter.init(folders)
folderAdapter.notifyDataSetChanged()
selectorRV?.let {
@ -114,6 +120,11 @@ class FolderFragment : CommonsDaggerSupportFragment() {
}
}
override fun onResume() {
folderAdapter.notifyDataSetChanged()
super.onResume()
}
/**
* Return Column count ie span count for grid view adapter.
*/

View file

@ -1,6 +1,8 @@
package fr.free.nrw.commons.customselector.ui.selector
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -13,11 +15,15 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import kotlinx.android.synthetic.main.fragment_custom_selector.*
import fr.free.nrw.commons.theme.BaseActivity
import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
import java.io.File
import java.io.FileInputStream
import java.net.URI
import javax.inject.Inject
class ImageFragment: CommonsDaggerSupportFragment() {
@ -27,13 +33,18 @@ class ImageFragment: CommonsDaggerSupportFragment() {
*/
private var bucketId: Long? = null
/**
* Last ImageItem Id.
*/
private var lastItemId: Long? = null
/**
* View model for images.
*/
private var viewModel: CustomSelectorViewModel? = null
/**
* View Elements
* View Elements.
*/
private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null
@ -67,14 +78,16 @@ class ImageFragment: CommonsDaggerSupportFragment() {
* BucketId args name
*/
const val BUCKET_ID = "BucketId"
const val LAST_ITEM_ID = "LastItemId"
/**
* newInstance from bucketId.
*/
fun newInstance(bucketId: Long): ImageFragment {
fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment {
val fragment = ImageFragment()
val args = Bundle()
args.putLong(BUCKET_ID, bucketId)
args.putLong(LAST_ITEM_ID, lastItemId)
fragment.arguments = args
return fragment
}
@ -87,6 +100,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bucketId = arguments?.getLong(BUCKET_ID)
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java)
}
@ -116,6 +130,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
return root
}
lateinit var filteredImages: ArrayList<Image>;
/**
* Handle view model result.
*/
@ -123,9 +139,14 @@ class ImageFragment: CommonsDaggerSupportFragment() {
if(result.status is CallbackStatus.SUCCESS){
val images = result.images
if(images.isNotEmpty()) {
imageAdapter.init(ImageHelper.filterImages(images,bucketId))
selectorRV?.let{
filteredImages = ImageHelper.filterImages(images, bucketId)
imageAdapter.init(filteredImages)
selectorRV?.let {
it.visibility = View.VISIBLE
lastItemId?.let { pos ->
(it.layoutManager as GridLayoutManager)
.scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos))
}
}
}
else{
@ -149,11 +170,35 @@ class ImageFragment: CommonsDaggerSupportFragment() {
// todo change span count depending on the device orientation and other factos.
}
override fun onResume() {
imageAdapter.notifyDataSetChanged()
super.onResume()
}
/**
* OnDestroy Cleanup the imageLoader coroutine.
* OnDestroy
* Cleanup the imageLoader coroutine.
* Save the Image Fragment state.
*/
override fun onDestroy() {
imageLoader?.cleanUP()
val position = (selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition()
// Check for empty RecyclerView.
if (position != -1) {
context?.let { context ->
context.getSharedPreferences(
"CustomSelector",
BaseActivity.MODE_PRIVATE
)?.let { prefs ->
prefs.edit()?.let { editor ->
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
}
}
}
}
super.onDestroy()
}
}

View file

@ -13,6 +13,7 @@ import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import kotlinx.coroutines.*
import timber.log.Timber
import java.io.FileNotFoundException
import java.io.IOException
import java.net.UnknownHostException
import java.util.*
@ -86,6 +87,8 @@ class ImageLoader @Inject constructor(
}
val imageSHA1 = getImageSHA1(image.uri)
if(imageSHA1.isEmpty())
return@launch
val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let {
@ -195,9 +198,14 @@ class ImageLoader @Inject constructor(
mapImageSHA1[uri]?.let{
return@withContext it
}
val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri))
mapImageSHA1[uri] = result
result
try {
val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri))
mapImageSHA1[uri] = result
result
} catch (e: FileNotFoundException){
e.printStackTrace()
""
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -70,17 +70,17 @@
app:srcCompat="@drawable/ic_photo_white_24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_custom_gallery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/button_blue"
android:visibility="gone"
app:backgroundTint="@color/main_background_light"
app:useCompatPadding="true"
app:elevation="@dimen/tiny_margin"
app:fabSize="mini"
app:srcCompat="@drawable/commons_logo"
android:background="@drawable/commons"/>
android:id="@+id/fab_custom_gallery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/button_blue"
android:visibility="gone"
app:backgroundTint="@color/main_background_light"
app:useCompatPadding="true"
app:elevation="@dimen/tiny_margin"
app:fabSize="mini"
app:srcCompat="@drawable/ic_custom_image_picker"
android:background="@drawable/commons"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_plus"

View file

@ -16,7 +16,6 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="@color/white"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
@ -29,7 +28,6 @@
android:id="@+id/album_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:alpha="0.15" />
<LinearLayout