created CustomFabController to manage FAB actions

This commit is contained in:
Nishthajain7 2025-06-08 22:59:08 +05:30
parent c93939824d
commit e48a8059c4
3 changed files with 198 additions and 303 deletions

View file

@ -1,9 +1,7 @@
package fr.free.nrw.commons.contributions
import android.Manifest.permission
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
@ -12,13 +10,7 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.VisibleForTesting
import androidx.paging.PagedList
import androidx.recyclerview.widget.GridLayoutManager
@ -35,12 +27,11 @@ import fr.free.nrw.commons.contributions.WikipediaInstructionsDialogFragment.Com
import fr.free.nrw.commons.databinding.FragmentContributionsListBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.di.NetworkingModule
import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.ui.CustomFabController
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.SystemThemeUtils
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
import fr.free.nrw.commons.wikidata.model.WikiSite
import org.apache.commons.lang3.StringUtils
import javax.inject.Inject
@ -82,13 +73,7 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
var sessionManager: SessionManager? = null
private var binding: FragmentContributionsListBinding? = null
private var fab_close: Animation? = null
private var fab_open: Animation? = null
private var rotate_forward: Animation? = null
private var rotate_backward: Animation? = null
private var isFabOpen = false
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var fabController: CustomFabController
@VisibleForTesting
var rvContributionsList: RecyclerView? = null
@ -105,46 +90,12 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
private var contributionsSize = 0
private var userName: String? = null
private val galleryPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromGallery(
result!!, requireActivity(), callbacks!!
)
}
}
private val customSelectorLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromCustomSelector(
result!!, requireActivity(), callbacks!!
)
}
}
private val cameraPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromCamera(
result!!, requireActivity(), callbacks!!
)
}
}
@SuppressLint("NewApi")
override fun onCreate(
savedInstanceState: Bundle?
) {
super.onCreate(savedInstanceState)
//Now that we are allowing this fragment to be started for
// any userName- we expect it to be passed as an argument
if (arguments != null) {
userName = requireArguments().getString(ProfileActivity.KEY_USERNAME)
}
@ -152,28 +103,6 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
if (StringUtils.isEmpty(userName)) {
userName = sessionManager!!.userName
}
inAppCameraLocationPermissionLauncher =
registerForActivityResult(RequestMultiplePermissions()) { result ->
val areAllGranted = result.values.all { it }
if (areAllGranted) {
controller?.locationPermissionCallback?.onLocationPermissionGranted()
} else {
activity?.let { currentActivity ->
if (currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) {
controller?.handleShowRationaleFlowCameraLocation(
currentActivity,
inAppCameraLocationPermissionLauncher, // Pass launcher
cameraPickLauncherForResult
)
} else {
controller?.locationPermissionCallback?.onLocationPermissionDenied(
currentActivity.getString(R.string.in_app_camera_location_permission_denied)
)
}
}
}
}
}
override fun onCreateView(
@ -186,11 +115,6 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
rvContributionsList = binding!!.contributionsList
contributionsListPresenter!!.onAttachView(this)
binding!!.fabCustomGallery.setOnClickListener { v: View? -> launchCustomSelector() }
binding!!.fabCustomGallery.setOnLongClickListener {
showShortToast(context, R.string.custom_selector_title)
true
}
if (sessionManager!!.userName == userName) {
binding!!.tvContributionsOfUser.visibility = View.GONE
@ -241,9 +165,18 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fabController = CustomFabController(
this,
requireContext(),
binding!!.fabPlus,
binding!!.fabCamera,
binding!!.fabGallery,
binding!!.fabCustomGallery,
controller!!
)
fabController.initializeLaunchers()
initRecyclerView()
initializeAnimations()
setListeners()
fabController.setListeners(controller!!, requireActivity())
}
private fun initRecyclerView() {
@ -310,9 +243,7 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
*/
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN) {
if (isFabOpen) {
animateFAB(isFabOpen)
}
fabController.closeFabMenuIfOpen()
}
return false
}
@ -357,79 +288,14 @@ open class ContributionsListFragment : CommonsDaggerSupportFragment(), Contribut
)
}
private fun initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(activity, R.anim.fab_open)
fab_close = AnimationUtils.loadAnimation(activity, R.anim.fab_close)
rotate_forward = AnimationUtils.loadAnimation(activity,R.anim.rotate_forward)
rotate_backward = AnimationUtils.loadAnimation(activity,R.anim.rotate_backward)
}
private fun setListeners() {
binding!!.fabPlus.setOnClickListener { animateFAB(isFabOpen) }
binding!!.fabCamera.setOnClickListener {
controller!!.initiateCameraPick(
requireActivity(),
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
animateFAB(isFabOpen)
}
binding!!.fabCamera.setOnLongClickListener {
showShortToast(
context,
R.string.add_contribution_from_camera
)
true
}
binding!!.fabGallery.setOnClickListener {
controller!!.initiateGalleryPick(requireActivity(), galleryPickLauncherForResult, true)
animateFAB(isFabOpen)
}
binding!!.fabGallery.setOnLongClickListener {
showShortToast(context, R.string.menu_from_gallery)
true
}
}
/**
* Launch Custom Selector.
*/
private fun launchCustomSelector() {
controller!!.initiateCustomGalleryPickWithPermission(
requireActivity(),
customSelectorLauncherForResult
)
animateFAB(isFabOpen)
}
fun scrollToTop() {
rvContributionsList!!.smoothScrollToPosition(0)
}
private fun animateFAB(isFabOpen: Boolean) {
this.isFabOpen = !isFabOpen
if (binding!!.fabPlus.isShown) {
if (isFabOpen) {
binding!!.fabPlus.startAnimation(rotate_backward)
binding!!.fabCamera.startAnimation(fab_close)
binding!!.fabGallery.startAnimation(fab_close)
binding!!.fabCustomGallery.startAnimation(fab_close)
binding!!.fabCamera.hide()
binding!!.fabGallery.hide()
binding!!.fabCustomGallery.hide()
} else {
binding!!.fabPlus.startAnimation(rotate_forward)
binding!!.fabCamera.startAnimation(fab_open)
binding!!.fabGallery.startAnimation(fab_open)
binding!!.fabCustomGallery.startAnimation(fab_open)
binding!!.fabCamera.show()
binding!!.fabGallery.show()
binding!!.fabCustomGallery.show()
}
this.isFabOpen = !isFabOpen
}
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/

View file

@ -0,0 +1,167 @@
package fr.free.nrw.commons.ui
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import fr.free.nrw.commons.R
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.filepicker.FilePicker
import android.os.Build
import android.Manifest.permission
import android.app.Activity
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
class CustomFabController(
private val fragment: Fragment,
context: Context,
private val fabPlus: FloatingActionButton,
private val fabCamera: FloatingActionButton,
private val fabGallery: FloatingActionButton,
private val fabCustomGallery: FloatingActionButton,
private val controller: ContributionController
) {
private var isFabOpen = false
private val fabOpen: Animation = AnimationUtils.loadAnimation(context, R.anim.fab_open)
private val fabClose: Animation = AnimationUtils.loadAnimation(context, R.anim.fab_close)
private val rotateForward: Animation = AnimationUtils.loadAnimation(context, R.anim.rotate_forward)
private val rotateBackward: Animation = AnimationUtils.loadAnimation(context, R.anim.rotate_backward)
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private lateinit var galleryPickLauncherForResult: ActivityResultLauncher<Intent>
private lateinit var customSelectorLauncherForResult: ActivityResultLauncher<Intent>
private lateinit var cameraPickLauncherForResult: ActivityResultLauncher<Intent>
fun initializeLaunchers() {
inAppCameraLocationPermissionLauncher =
fragment.registerForActivityResult(RequestMultiplePermissions()) { result ->
val areAllGranted = result.values.all { it }
if (areAllGranted) {
controller.locationPermissionCallback?.onLocationPermissionGranted()
} else {
val activity = fragment.activity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
activity?.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION) == true
) {
controller.handleShowRationaleFlowCameraLocation(
activity,
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
} else {
activity?.getString(R.string.in_app_camera_location_permission_denied)?.let {
controller.locationPermissionCallback?.onLocationPermissionDenied(
it
)
}
}
}
}
galleryPickLauncherForResult = fragment.registerForActivityResult(StartActivityForResult()) { result: ActivityResult? ->
controller.handleActivityResultWithCallback(fragment.requireActivity()) { callbacks: FilePicker.Callbacks? ->
controller.onPictureReturnedFromGallery(result!!, fragment.requireActivity(), callbacks!!)
}
}
customSelectorLauncherForResult = fragment.registerForActivityResult(StartActivityForResult()) { result: ActivityResult? ->
controller.handleActivityResultWithCallback(fragment.requireActivity()) { callbacks: FilePicker.Callbacks? ->
controller.onPictureReturnedFromCustomSelector(result!!, fragment.requireActivity(), callbacks!!)
}
}
cameraPickLauncherForResult = fragment.registerForActivityResult(StartActivityForResult()) { result: ActivityResult? ->
controller.handleActivityResultWithCallback(fragment.requireActivity()) { callbacks: FilePicker.Callbacks? ->
controller.onPictureReturnedFromCamera(result!!, fragment.requireActivity(), callbacks!!)
}
}
}
private fun toggleFabMenu() {
isFabOpen = !isFabOpen
if (fabPlus.isShown) {
if (isFabOpen) {
fabPlus.startAnimation(rotateForward)
fabCamera.startAnimation(fabOpen)
fabGallery.startAnimation(fabOpen)
fabCustomGallery.startAnimation(fabOpen)
fabCamera.show()
fabGallery.show()
fabCustomGallery.show()
} else {
fabPlus.startAnimation(rotateBackward)
fabCamera.startAnimation(fabClose)
fabGallery.startAnimation(fabClose)
fabCustomGallery.startAnimation(fabClose)
fabCamera.hide()
fabGallery.hide()
fabCustomGallery.hide()
}
}
}
private fun closeFabMenu() {
if (isFabOpen) toggleFabMenu()
}
fun closeFabMenuIfOpen() {
if (isFabOpen) {
closeFabMenu()
}
}
fun setListeners(controller: ContributionController, activity: Activity) {
fabPlus.setOnClickListener {
toggleFabMenu()
}
fabCamera.setOnClickListener {
controller.initiateCameraPick(
activity,
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
closeFabMenu()
}
fabCamera.setOnLongClickListener {
showShortToast(activity, R.string.add_contribution_from_camera)
true
}
fabGallery.setOnClickListener {
controller.initiateGalleryPick(activity, galleryPickLauncherForResult, true)
closeFabMenu()
}
fabGallery.setOnLongClickListener {
showShortToast(activity, R.string.menu_from_gallery)
true
}
fabCustomGallery.setOnClickListener {
controller.initiateCustomGalleryPickWithPermission(
activity,
customSelectorLauncherForResult
)
closeFabMenu()
}
fabCustomGallery.setOnLongClickListener {
showShortToast(activity, R.string.custom_selector_title)
true
}
}
}

View file

@ -1,19 +1,10 @@
package fr.free.nrw.commons.upload
import android.Manifest.permission
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import fr.free.nrw.commons.CommonsApplication
@ -25,9 +16,8 @@ import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_QUEUED
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.databinding.FragmentPendingUploadsBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.ui.CustomFabController
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.ViewUtil
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
import java.util.Locale
import javax.inject.Inject
@ -51,36 +41,7 @@ class PendingUploadsFragment :
@JvmField
@Inject
var controller: ContributionController? = null
private var fab_close: Animation? = null
private var fab_open: Animation? = null
private var rotate_forward: Animation? = null
private var rotate_backward: Animation? = null
private var isFabOpen = false
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private val cameraPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromCamera(
result!!, requireActivity(), callbacks!!
)
}
}
private val galleryPickLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromGallery(
result!!, requireActivity(), callbacks!!
)
}
}
private lateinit var fabController: CustomFabController
override fun onAttach(context: Context) {
super.onAttach(context)
@ -95,37 +56,7 @@ class PendingUploadsFragment :
savedInstanceState: Bundle?,
): View {
super.onCreate(savedInstanceState)
inAppCameraLocationPermissionLauncher =
registerForActivityResult(RequestMultiplePermissions()) { result ->
val areAllGranted = result.values.all { it }
if (areAllGranted) {
controller?.locationPermissionCallback?.onLocationPermissionGranted()
} else {
activity?.let { currentActivity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
currentActivity.shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)
) {
controller?.handleShowRationaleFlowCameraLocation(
currentActivity,
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
} else {
controller?.locationPermissionCallback?.onLocationPermissionDenied(
currentActivity.getString(R.string.in_app_camera_location_permission_denied)
)
}
}
}
}
binding = FragmentPendingUploadsBinding.inflate(inflater, container, false)
binding.fabCustomGallery.setOnClickListener { launchCustomSelector() }
binding.fabCustomGallery.setOnLongClickListener {
showShortToast(context, R.string.custom_selector_title)
true
}
pendingUploadsPresenter.onAttachView(this)
initAdapter()
return binding.root
@ -135,14 +66,20 @@ class PendingUploadsFragment :
adapter = PendingUploadsAdapter(this)
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fabController = CustomFabController(
this,
requireContext(),
binding.fabPlus,
binding.fabCamera,
binding.fabGallery,
binding.fabCustomGallery,
controller!!
)
fabController.initializeLaunchers()
fabController.setListeners(controller!!, requireActivity())
initRecyclerView()
initializeAnimations()
setListeners()
}
/**
@ -178,7 +115,7 @@ class PendingUploadsFragment :
binding.noPendingTextView.visibility = View.GONE
binding.pendingUploadsLl.visibility = View.VISIBLE
adapter.submitList(list)
binding.progressTextView.setText("$contributionsSize uploads left")
binding.progressTextView.text = "$contributionsSize uploads left"
if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) {
uploadProgressActivity.setPausedIcon(true)
} else {
@ -188,81 +125,6 @@ class PendingUploadsFragment :
}
}
private fun initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(activity, R.anim.fab_open)
fab_close = AnimationUtils.loadAnimation(activity, R.anim.fab_close)
rotate_forward = AnimationUtils.loadAnimation(activity, R.anim.rotate_forward)
rotate_backward = AnimationUtils.loadAnimation(activity, R.anim.rotate_backward)
}
private fun setListeners() {
binding.fabPlus.setOnClickListener { animateFAB(isFabOpen) }
binding.fabCamera.setOnClickListener {
controller!!.initiateCameraPick(
requireActivity(),
inAppCameraLocationPermissionLauncher,
cameraPickLauncherForResult
)
animateFAB(isFabOpen)
}
binding.fabCamera.setOnLongClickListener {
showShortToast(
context,
R.string.add_contribution_from_camera
)
true
}
binding.fabGallery.setOnClickListener {
controller!!.initiateGalleryPick(requireActivity(), galleryPickLauncherForResult, true)
animateFAB(isFabOpen)
}
binding.fabGallery.setOnLongClickListener {
showShortToast(context, R.string.menu_from_gallery)
true
}
}
private val customSelectorLauncherForResult = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult()
) { result: ActivityResult? ->
controller!!.handleActivityResultWithCallback(requireActivity()
) { callbacks: FilePicker.Callbacks? ->
controller!!.onPictureReturnedFromCustomSelector(
result!!, requireActivity(), callbacks!!
)
}
}
protected fun launchCustomSelector() {
controller!!.initiateCustomGalleryPickWithPermission(
requireActivity(),
customSelectorLauncherForResult
)
animateFAB(isFabOpen)
}
private fun animateFAB(isFabOpen: Boolean) {
this.isFabOpen = !isFabOpen
if (binding.fabPlus.isShown) {
if (isFabOpen) {
binding.fabPlus.startAnimation(rotate_backward)
binding.fabCamera.startAnimation(fab_close)
binding.fabGallery.startAnimation(fab_close)
binding.fabCustomGallery.startAnimation(fab_close)
binding.fabCamera.hide()
binding.fabGallery.hide()
binding.fabCustomGallery.hide()
} else {
binding.fabPlus.startAnimation(rotate_forward)
binding.fabCamera.startAnimation(fab_open)
binding.fabGallery.startAnimation(fab_open)
binding.fabCustomGallery.startAnimation(fab_open)
binding.fabCamera.show()
binding.fabGallery.show()
binding.fabCustomGallery.show()
}
this.isFabOpen = !isFabOpen
}
}
/**
* Cancels a specific upload after getting a confirmation from the user using Dialog.
*/
@ -276,7 +138,7 @@ class PendingUploadsFragment :
String.format(locale, activity.getString(R.string.yes)),
String.format(locale, activity.getString(R.string.no)),
{
ViewUtil.showShortToast(context, R.string.cancelling_upload)
showShortToast(context, R.string.cancelling_upload)
pendingUploadsPresenter.deleteUpload(
contribution, requireContext().applicationContext,
)