diff --git a/app/build.gradle b/app/build.gradle index bd25e8f..bdb83c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'com.esri.arcgisruntime:arcgis-android:100.10.0' + implementation 'com.esri.arcgisruntime:arcgis-android-toolkit:100.11.0-3131' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/util/Event.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/util/Event.kt deleted file mode 100644 index 376bb9e..0000000 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/util/Event.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020 Esri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.esri.arcgisruntime.opensourceapps.datacollection.util - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer - -/** - * Used as a wrapper for data that is exposed via a LiveData that represents an event. - * - *

- * This avoids a common problem with events: on configuration change (like rotation) an update - * can be emitted if the observer is active. This LiveData only calls the observable if there's an - * explicit call to postValue(). - *

- * https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 - */ -open class Event(private val content: T) { - private var hasBeenHandled = false - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } -} - -/** - * Adds the given event function to observer being added to the observers list within the lifespan - * of the given owner. The events are dispatched on the main thread. - * - * @param owner The LifecycleOwner which controls the observer - * @param onEventRaised The observer function that will receive the events from the observer -*/ -fun LiveData>.observeEvent(owner: LifecycleOwner, onEventRaised: (T) -> Unit) { - observe(owner, Observer> { event -> - event?.getContentIfNotHandled()?.let { onEventRaised(it) } - }) -} - -/** - * Raises an event with given argument by calling postValue() on the MutableLiveData object. - * - * @param arg The argument to pass to the observers - */ -fun MutableLiveData>.raiseEvent(arg: T) { - postValue(Event(arg)) -} - -/** - * Raises an event by calling postValue() on the MutableLiveData object. - */ -fun MutableLiveData>.raiseEvent() { - postValue(Event(Unit)) -} diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/DataCollectionViewModel.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/DataCollectionViewModel.kt index 0a4320b..56d2f28 100644 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/DataCollectionViewModel.kt +++ b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/DataCollectionViewModel.kt @@ -25,9 +25,9 @@ import androidx.security.crypto.MasterKeys import com.esri.arcgisruntime.loadable.LoadStatus import com.esri.arcgisruntime.mapping.ArcGISMap import com.esri.arcgisruntime.opensourceapps.datacollection.R -import com.esri.arcgisruntime.opensourceapps.datacollection.util.Event +import com.esri.arcgisruntime.toolkit.util.Event import com.esri.arcgisruntime.opensourceapps.datacollection.util.Logger -import com.esri.arcgisruntime.opensourceapps.datacollection.util.raiseEvent +import com.esri.arcgisruntime.toolkit.util.raiseEvent import com.esri.arcgisruntime.portal.Portal import com.esri.arcgisruntime.portal.PortalItem import com.esri.arcgisruntime.portal.PortalUser @@ -73,6 +73,11 @@ class DataCollectionViewModel(application: Application, val mapViewModel: MapVie private val _bottomSheetState: MutableLiveData = MutableLiveData(BottomSheetBehavior.STATE_HIDDEN) val bottomSheetState: LiveData = _bottomSheetState + private val _isShowPopupEditControls = MutableLiveData(false) + // Depicts whether to show the edit layout to modify the selected popup attributes + // being displayed in the bottomsheet + val isShowPopupEditControls: LiveData = _isShowPopupEditControls + private val encryptedSharedPrefs by lazy { val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) EncryptedSharedPreferences.create( @@ -180,6 +185,15 @@ class DataCollectionViewModel(application: Application, val mapViewModel: MapVie _bottomSheetState.value = bottomSheetState } + /** + * Sets whether to display the Popup editing control view. + * + * @param showPopupEditControls + */ + fun setShowPopupEditControls(showPopupEditControls: Boolean) { + _isShowPopupEditControls.value = showPopupEditControls + } + /** * Loads the Portal with the given value of the loginRequired flag and sets it on the Map. */ diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/IdentifyResultViewModel.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/IdentifyResultViewModel.kt index 3b72fdd..21ec487 100644 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/IdentifyResultViewModel.kt +++ b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/IdentifyResultViewModel.kt @@ -25,8 +25,9 @@ import com.esri.arcgisruntime.layers.FeatureLayer import com.esri.arcgisruntime.layers.LayerContent import com.esri.arcgisruntime.mapping.GeoElement import com.esri.arcgisruntime.mapping.view.IdentifyLayerResult -import com.esri.arcgisruntime.opensourceapps.datacollection.util.Event -import com.esri.arcgisruntime.opensourceapps.datacollection.util.raiseEvent +import com.esri.arcgisruntime.toolkit.util.Event +import com.esri.arcgisruntime.toolkit.util.raiseEvent +import com.esri.arcgisruntime.toolkit.popup.PopupViewModel /** * The view model for IdentifyResultFragment, that is responsible for processing the result of @@ -38,6 +39,9 @@ class IdentifyResultViewModel(val popupViewModel: PopupViewModel) : ViewModel() private val _showIdentifyResultEvent = MutableLiveData>() val showIdentifyResultEvent: LiveData> = _showIdentifyResultEvent + private val _dismissIdentifyResultEvent = MutableLiveData>() + val dismissIdentifyResultEvent: LiveData> = _dismissIdentifyResultEvent + private val _identifyLayerResult = MutableLiveData() val identifyLayerResult: LiveData = _identifyLayerResult @@ -89,6 +93,13 @@ class IdentifyResultViewModel(val popupViewModel: PopupViewModel) : ViewModel() popupViewModel.clearPopup() } + /** + * Raises an event to dismiss the identify results + */ + fun dismissIdentifyLayerResult() { + _dismissIdentifyResultEvent.raiseEvent() + } + /** * Raises an event to show the Popup */ diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/PopupViewModel.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/PopupViewModel.kt deleted file mode 100644 index cced00f..0000000 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/viewmodels/PopupViewModel.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2020 Esri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.esri.arcgisruntime.ArcGISRuntimeException -import com.esri.arcgisruntime.concurrent.ListenableFuture -import com.esri.arcgisruntime.data.Feature -import com.esri.arcgisruntime.data.FeatureEditResult -import com.esri.arcgisruntime.data.FeatureTable -import com.esri.arcgisruntime.data.ServiceFeatureTable -import com.esri.arcgisruntime.mapping.popup.Popup -import com.esri.arcgisruntime.mapping.popup.PopupManager -import com.esri.arcgisruntime.opensourceapps.datacollection.util.Event -import com.esri.arcgisruntime.opensourceapps.datacollection.util.raiseEvent - -/** - * The view model that represents a Popup. It supports: - * - *

- * - * A PopupViewModel can be bound to a PopupView for visualisation of the Popup GeoElement's - * attributes and editing experience. - */ -class PopupViewModel(application: Application) : AndroidViewModel(application) { - - private val _popup = MutableLiveData() - // The Popup whose fields are viewed and edited in the PopupView - val popup: LiveData = _popup - - private val _popupManager = MutableLiveData() - // The manager for the Popup responsible for viewing and editing of the Popup - val popupManager: LiveData = _popupManager - - private val _isPopupInEditMode = MutableLiveData() - // Depicts whether the PopupManager is currently in editing mode - // When in edit mode the user can edit the values of fields of the Popup - val isPopupInEditMode: LiveData = _isPopupInEditMode - - private val _showSavePopupErrorEvent = MutableLiveData>() - // This event is raised when an error is encountered while trying to save edits on a popup. - // It passes the exception message to all the observers in the observeEvent() method. - val showSavePopupErrorEvent: LiveData> = _showSavePopupErrorEvent - - private val _showSavingProgressEvent = MutableLiveData>() - // This event is raised when the save operation begins/ends. - // It passes true to all the observers in the observeEvent() method when the save operation - // is initiated and false when it ends. - val showSavingProgressEvent: LiveData> = _showSavingProgressEvent - - private val _confirmCancelPopupEditingEvent = MutableLiveData>() - // This event is raised when the user cancels the edit mode on the Popup. - // It is used for showing confirmation dialog to the user, before calling cancelEditing() - val confirmCancelPopupEditingEvent: LiveData> = _confirmCancelPopupEditingEvent - - /** - * Updates popup property to set the popup field values being displayed in - * the bottom sheet. Creates PopupManager for PopupView to perform edit operations. - * - * @param popup - */ - fun setPopup(popup: Popup) { - _popup.value = popup - _popupManager.value = PopupManager(getApplication(), _popup.value) - } - - /** - * Enables/disables edit mode on the PopupView - */ - fun setEditMode(isEnabled: Boolean) { - _isPopupInEditMode.value = isEnabled - } - - /** - * Clear the popup - */ - fun clearPopup() { - _popup.value = null - _popupManager.value = null - } - - /** - * Cancels the edit mode. - */ - fun cancelEditing() { - _popupManager.value?.cancelEditing() - _isPopupInEditMode.value = false - } - - /** - * Raises ConfirmCancelPopupEditingEvent that can be observed and used for - * prompting user with confirmation dialog to make sure the user wants to cancel edits. - * To be followed by cancelEditing() if the user response is positive. - */ - fun confirmCancelEditing() { - _confirmCancelPopupEditingEvent.raiseEvent() - } - - /** - * Saves Popup edits by applying changes to the feature service associated with a Popup's - * feature. - */ - fun savePopupEdits() { - // show the Progress bar informing user that save operation is in progress - _showSavingProgressEvent.raiseEvent(true) - _popupManager.value?.let { popupManager -> - // Call finishEditingAsync() to apply edit changes locally and end the popup manager - // editing session - val finishEditingFuture: ListenableFuture = - popupManager.finishEditingAsync() - finishEditingFuture.addDoneListener { - try { - finishEditingFuture.get() - - // The edits were applied successfully to the local geodatabase, - // push those changes to the feature service by calling applyEditsAsync() - val feature: Feature = popupManager.popup.geoElement as Feature - val featureTable: FeatureTable = feature.featureTable - if (featureTable is ServiceFeatureTable) { - val applyEditsFuture: ListenableFuture> = - featureTable.applyEditsAsync() - applyEditsFuture.addDoneListener { - // dismiss the Progress bar - _showSavingProgressEvent.raiseEvent(false) - // dismiss edit mode - _isPopupInEditMode.value = false - try { - val featureEditResults: List = - applyEditsFuture.get() - // Check for errors in FeatureEditResults - if (featureEditResults.any { result -> result.hasCompletedWithErrors() }) { - // an error was encountered when trying to apply edits - val exception = - featureEditResults.filter { featureEditResult -> featureEditResult.hasCompletedWithErrors() }[0].error - // show the error message to the user - exception.message?.let { exceptionMessage -> - _showSavePopupErrorEvent.raiseEvent(exceptionMessage) - } - } - - } catch (exception: Exception) { - // show the error message to the user - exception.message?.let { exceptionMessage -> - _showSavePopupErrorEvent.raiseEvent(exceptionMessage) - } - } - } - } - } catch (exception: Exception) { - // dismiss the Progress bar - _showSavingProgressEvent.raiseEvent(false) - // show the error message to the user - exception.message?.let { exceptionMessage -> - _showSavePopupErrorEvent.raiseEvent(exceptionMessage) - } - } - } - } - } -} diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/DataCollectionFragment.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/DataCollectionFragment.kt index 664a4b7..2bea997 100644 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/DataCollectionFragment.kt +++ b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/DataCollectionFragment.kt @@ -17,6 +17,7 @@ package com.esri.arcgisruntime.opensourceapps.datacollection.views.fragments import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -27,7 +28,6 @@ import androidx.core.view.GravityCompat import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController @@ -37,15 +37,21 @@ import com.esri.arcgisruntime.mapping.view.DefaultMapViewOnTouchListener import com.esri.arcgisruntime.opensourceapps.datacollection.R import com.esri.arcgisruntime.opensourceapps.datacollection.databinding.FragmentDataCollectionBinding import com.esri.arcgisruntime.opensourceapps.datacollection.util.Logger -import com.esri.arcgisruntime.opensourceapps.datacollection.util.observeEvent import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.DataCollectionViewModel import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.IdentifyResultViewModel import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.MapViewModel -import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.PopupViewModel import com.esri.arcgisruntime.security.AuthenticationManager import com.esri.arcgisruntime.security.DefaultAuthenticationChallengeHandler +import com.esri.arcgisruntime.toolkit.popup.PopupViewModel +import com.esri.arcgisruntime.toolkit.util.observeEvent import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetBehavior.* +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_DRAGGING +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_SETTLING +import com.google.android.material.bottomsheet.BottomSheetBehavior.from import kotlinx.android.synthetic.main.fragment_data_collection.* import java.security.InvalidParameterException import kotlin.math.roundToInt @@ -80,25 +86,15 @@ class DataCollectionFragment : Fragment() { private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { + dataCollectionViewModel.setShowPopupEditControls(false) when { // we handle the back button press here to navigate back in the bottomsheet. // when the user presses the back button when PopupAttributesListFragment is showing // from the bottomsheet_navigation graph we pop it from the BackStack to go to the // previous IdentifyResultFragment. When the backstack is at identifyResultFragment - // popBackStack() will return a false and we will hide the bottomsheet and clear the - // selected feature in the Featurelayer. If the bottomsheet is already hidden exit - // the DataCollectionActivity. + // popBackStack() will return a false and we will exit the DataCollectionActivity. !bottomSheetNavController.popBackStack(R.id.identifyResultFragment, false) -> - if (bottomSheetBehavior.state == STATE_COLLAPSED) { - dataCollectionViewModel.setCurrentBottomSheetState(STATE_HIDDEN) - resetIdentifyResult() - } else { - requireActivity().finish() - } - // if the bottomsheet is in the STATE_EXPANDED then we are showing - // PopupAttributeListFragment and we need return back to the IdentifyResultFragment - // which shows up in STATE_COLLAPSED - bottomSheetBehavior.state == STATE_EXPANDED -> dataCollectionViewModel.setCurrentBottomSheetState(STATE_COLLAPSED) + requireActivity().finish() } } } @@ -116,6 +112,7 @@ class DataCollectionFragment : Fragment() { val view = fragmentDataCollectionBinding.root + fragmentDataCollectionBinding.popupViewModel = popupViewModel fragmentDataCollectionBinding.dataCollectionViewModel = dataCollectionViewModel fragmentDataCollectionBinding.lifecycleOwner = this @@ -128,8 +125,30 @@ class DataCollectionFragment : Fragment() { ?: throw InvalidParameterException("bottomSheetNavHostFragment must exist") bottomSheetNavController = bottomSheetNavHostFragment.findNavController() - dataCollectionViewModel.bottomSheetState.observe(viewLifecycleOwner, Observer { - bottomSheetBehavior.state = it + dataCollectionViewModel.bottomSheetState.observe(viewLifecycleOwner, { bottomSheetState -> + if (bottomSheetState == STATE_HIDDEN) { + bottomSheetBehavior.isHideable = true + } + bottomSheetBehavior.state = bottomSheetState + }) + + bottomSheetBehavior.addBottomSheetCallback(object : + BottomSheetBehavior.BottomSheetCallback() { + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + // handle onSlide + } + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (bottomSheetNavController.currentDestination?.id == R.id.popupFragment) { + when (newState) { + STATE_COLLAPSED -> dataCollectionViewModel.setShowPopupEditControls(false) + STATE_HIDDEN -> dataCollectionViewModel.setShowPopupEditControls(false) + STATE_HALF_EXPANDED -> dataCollectionViewModel.setShowPopupEditControls(true) + STATE_EXPANDED -> dataCollectionViewModel.setShowPopupEditControls(true) + } + } + } }) // On orientation change if we have a valid value for identifyLayerResult, @@ -143,24 +162,34 @@ class DataCollectionFragment : Fragment() { } identifyResultViewModel.showPopupEvent.observeEvent(viewLifecycleOwner) { + if (bottomSheetBehavior.state != STATE_COLLAPSED) { + dataCollectionViewModel.setShowPopupEditControls(true) + } bottomSheetNavController.navigate(R.id.action_identifyResultFragment_to_popupFragment) - // PopupAttributeListFragment shows all popup attributes, so we - // show them in expanded(full screen) state of the bottom sheet - dataCollectionViewModel.setCurrentBottomSheetState(STATE_EXPANDED) } identifyResultViewModel.showIdentifyResultEvent.observeEvent(viewLifecycleOwner) { - // user has kicked off event to show IdentifyResultsFragment by tapping on the header of - // bottomsheet containing popupAttributeListFragment. - if (bottomSheetNavController.currentDestination?.id == R.id.popupFragment) { - bottomSheetNavController.popBackStack() + // IdentifyResultFragment shows a few selected popup attributes. We + // show them in half expanded state of the bottom sheet + if (dataCollectionViewModel.bottomSheetState.value == STATE_HIDDEN) { + bottomSheetBehavior.isHideable = false + dataCollectionViewModel.setCurrentBottomSheetState(STATE_HALF_EXPANDED) } - // IdentifyResultFragment only shows a few selected popup attributes, so we - // show them in collapsed state(roughly 1/4 screen size) of the bottom sheet - dataCollectionViewModel.setCurrentBottomSheetState(STATE_COLLAPSED) } - requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + popupViewModel.dismissPopupEvent.observeEvent(viewLifecycleOwner) { + resetIdentifyResult() + dataCollectionViewModel.setShowPopupEditControls(false) + dataCollectionViewModel.setCurrentBottomSheetState(STATE_HIDDEN) + bottomSheetNavController.popBackStack(R.id.identifyResultFragment, false) + } + + identifyResultViewModel.dismissIdentifyResultEvent.observeEvent(viewLifecycleOwner) { + resetIdentifyResult() + dataCollectionViewModel.setCurrentBottomSheetState(STATE_HIDDEN) + } + + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback) return view } @@ -169,7 +198,7 @@ class DataCollectionFragment : Fragment() { super.onViewCreated(view, savedInstanceState) // setup the app bar and drawer layout - val navController = activity?.findNavController(R.id.bottomSheetNavHostFragment) + val navController = activity?.findNavController(R.id.navHostFragment) navController?.let { val appBarConfiguration = AppBarConfiguration(navController.graph, drawer_layout) view.findViewById(R.id.toolbar) @@ -189,37 +218,31 @@ class DataCollectionFragment : Fragment() { } } - // Handle the navigation button clicks on the toolbar to open the drawer and act as a back - // button - toolbar.setNavigationOnClickListener { - // When the user presses the navigation button to go back from PopupAttributeListFragment - // to IdentifyResultFragment in the bottomsheet, the bottomSheetNavController will still - // be holding on to PopupAttributeListFragment as the current destination when we last - // navigated to it. If that is the case we navigate to IdentifyResultFragment, else we - // have already navigated to IdentifyResultFragment in the bottomsheet and - // show the drawer layout. - if (bottomSheetNavController.currentDestination?.id == R.id.popupFragment) { - requireActivity().onBackPressed() - } else { - drawer_layout.openDrawer(GravityCompat.START) - } - } - mapView.onTouchListener = object : DefaultMapViewOnTouchListener(context, mapView) { override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { - dataCollectionViewModel.setCurrentBottomSheetState(STATE_HIDDEN) - - e?.let { - val screenPoint = android.graphics.Point( - it.x.roundToInt(), - it.y.roundToInt() - ) - identifyLayer(screenPoint) + + // Only perform identify on the mapview if the Popup is not in edit mode + if (popupViewModel.isPopupInEditMode.value == false) { + // If the user tapped on the mapview to perform an identify and + // is currently looking at a popup's attributes we move back to + // IdentifyResultFragment to perform identify + if (bottomSheetNavController.currentDestination?.id == R.id.popupFragment) { + bottomSheetNavController.popBackStack(R.id.identifyResultFragment, false) + } + + dataCollectionViewModel.setCurrentBottomSheetState(STATE_HIDDEN) + + e?.let { + val screenPoint = android.graphics.Point( + it.x.roundToInt(), + it.y.roundToInt() + ) + identifyLayer(screenPoint) + } } return true } - } } diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/IdentifyResultFragment.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/IdentifyResultFragment.kt index ce9d236..ad6c58a 100644 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/IdentifyResultFragment.kt +++ b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/IdentifyResultFragment.kt @@ -26,7 +26,7 @@ import androidx.fragment.app.activityViewModels import com.esri.arcgisruntime.opensourceapps.datacollection.R import com.esri.arcgisruntime.opensourceapps.datacollection.databinding.FragmentIdentifyResultBinding import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.IdentifyResultViewModel -import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.PopupViewModel +import com.esri.arcgisruntime.toolkit.popup.PopupViewModel /** * Responsible for displaying properties of the resulting GeoElement of an identify operation in diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/PopupFragment.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/PopupFragment.kt index e69ad15..729eab1 100644 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/PopupFragment.kt +++ b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/fragments/PopupFragment.kt @@ -20,15 +20,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.esri.arcgisruntime.mapping.popup.Popup import com.esri.arcgisruntime.opensourceapps.datacollection.R import com.esri.arcgisruntime.opensourceapps.datacollection.databinding.FragmentPopupBinding -import com.esri.arcgisruntime.opensourceapps.datacollection.util.observeEvent -import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.IdentifyResultViewModel -import com.esri.arcgisruntime.opensourceapps.datacollection.viewmodels.PopupViewModel +import com.esri.arcgisruntime.toolkit.popup.PopupViewModel +import com.esri.arcgisruntime.toolkit.util.observeEvent import kotlinx.android.synthetic.main.fragment_popup.* /** @@ -54,9 +55,13 @@ class PopupFragment : Fragment() { popupViewModel.showSavingProgressEvent.observeEvent(viewLifecycleOwner) { isShowProgressBar -> if (isShowProgressBar) { + requireActivity().window.setFlags( + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) progressBarLayout.visibility = View.VISIBLE } else { progressBarLayout.visibility = View.GONE + requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) } } @@ -68,9 +73,36 @@ class PopupFragment : Fragment() { showConfirmCancelEditingDialog() } + popupViewModel.confirmDeletePopupEvent.observeEvent(viewLifecycleOwner) { + showConfirmDeletePopupDialog() + } + + popupViewModel.showDeletePopupErrorEvent.observeEvent(viewLifecycleOwner) { errorMessage -> + showAlertDialog(errorMessage) + } + return binding.root } + /** + * Shows dialog to confirm deleting the popup. + */ + private fun showConfirmDeletePopupDialog() { + val dialogBuilder = AlertDialog.Builder(requireContext()) + dialogBuilder.setMessage("Delete ${(popupViewModel.popup.value as Popup).title}?") + .setCancelable(false) + // positive button text and action + .setPositiveButton(getString(R.string.ok)) { dialog, id -> + popupViewModel.deletePopup() + } + // negative button text and action + .setNegativeButton(getString(R.string.cancel)) { dialog, id -> dialog.cancel() + } + val alert = dialogBuilder.create() + // show alert dialog + alert.show() + } + /** * Shows dialog to confirm cancelling edit mode on popup view. */ diff --git a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/popup/PopupView.kt b/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/popup/PopupView.kt deleted file mode 100644 index 86566fe..0000000 --- a/app/src/main/java/com/esri/arcgisruntime/opensourceapps/datacollection/views/popup/PopupView.kt +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2020 Esri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.esri.arcgisruntime.opensourceapps.datacollection.views.popup - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.text.InputType -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.Spinner -import android.widget.TextView -import androidx.core.widget.doAfterTextChanged -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.esri.arcgisruntime.ArcGISRuntimeException -import com.esri.arcgisruntime.data.CodedValueDomain -import com.esri.arcgisruntime.data.Field -import com.esri.arcgisruntime.mapping.popup.Popup -import com.esri.arcgisruntime.mapping.popup.PopupField -import com.esri.arcgisruntime.mapping.popup.PopupManager -import com.esri.arcgisruntime.opensourceapps.datacollection.BR -import com.esri.arcgisruntime.opensourceapps.datacollection.R -import kotlinx.android.synthetic.main.item_popup_row.view.* -import kotlinx.android.synthetic.main.layout_popupview.view.* - -private const val TAG = "PopupView" - -/** - * Displays the popup attribute list in a [RecyclerView]. - */ -class PopupView : FrameLayout { - - private val popupAttributeListAdapter by lazy { PopupAttributeListAdapter() } - private var isEditMode: Boolean = false - - lateinit var popupManager: PopupManager - lateinit var popup: Popup - - /** - * Constructor used when instantiating this View directly to attach it to another view programmatically. - */ - constructor(context: Context) : super(context) { - init(context) - } - - /** - * Constructor used when defining this view in an XML layout. - */ - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context) - } - - /** - * Initializes this PopupView by inflating the layout and setting the [RecyclerView] adapter. - */ - private fun init(context: Context) { - inflate(context, R.layout.layout_popupview, this) - popupRecyclerView.layoutManager = LinearLayoutManager(context) - popupRecyclerView.adapter = popupAttributeListAdapter - } - - /** - * Enables/Disables edit mode on the PopupView. - */ - fun setEditMode(isEnabled: Boolean) { - isEditMode = isEnabled - if (isEnabled) { - popupAttributeListAdapter.submitList(popupManager.editableFields) - popupManager.startEditing() - } else { - popupAttributeListAdapter.submitList(popupManager.displayedFields) - } - popupAttributeListAdapter.notifyDataSetChanged() - } - - /** - * Adapter used by PopupView to display a list of PopupAttributes in a - * recyclerView. - */ - private inner class PopupAttributeListAdapter : - ListAdapter(DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = DataBindingUtil.inflate( - inflater, - R.layout.item_popup_row, - parent, - false - ) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val popupField: PopupField = getItem(position) - holder.updateView(popupField) - - holder.bind(popupField) - } - } - - /** - * Callback for calculating the diff between two non-null items in a list. - */ - private class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame( - oldItem: PopupField, - newItem: PopupField - ): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame( - oldItem: PopupField, - newItem: PopupField - ): Boolean { - return oldItem.fieldName == newItem.fieldName - } - } - - /** - * The PopupAttributeListAdapter ViewHolder. - */ - private inner class ViewHolder(private val binding: ViewDataBinding) : - RecyclerView.ViewHolder(binding.root) { - - val labelTextView: TextView by lazy { - binding.root.labelTextView - } - - val valueTextView: TextView by lazy { - binding.root.valueTextView - } - - val valueEditText: EditText by lazy { - binding.root.valueEditText - } - - val codedValueDomainSpinner: Spinner by lazy { - binding.root.codedValueDomainSpinner - } - - fun bind( - popupField: PopupField - ) { - binding.setVariable(BR.popupField, popupField) - binding.setVariable(BR.popupManager, popupManager) - binding.executePendingBindings() - } - - /** - * Toggles the view for popup field value from edittext to textview and vice-versa, given the - * edit mode of the popupView. - */ - fun updateView(popupField: PopupField) { - if (isEditMode) { - val codedValueDomain : CodedValueDomain? = popupManager.getDomain(popupField) as? CodedValueDomain - if (codedValueDomain != null) { - setUpSpinner(codedValueDomain, popupField) - valueEditText.visibility = View.GONE - valueTextView.visibility = View.GONE - codedValueDomainSpinner.visibility = View.VISIBLE - } else { - valueEditText.inputType = getInputType(popupManager.getFieldType(popupField)) - valueEditText.visibility = View.VISIBLE - valueTextView.visibility = View.GONE - //save original colors - val oldColors: ColorStateList = labelTextView.textColors - // here we assign and hold the values of the editable fields, entered by the user - // in popupAttribute.tempValue - valueEditText.doAfterTextChanged { - if (valueEditText.hasFocus()) { - - val validationError: ArcGISRuntimeException? = updateValue( - popupField, - valueEditText.text.toString() - ) - if (validationError != null) { - val fieldLabelWithValidationError = - popupField.label + ": " + validationError.message - labelTextView.text = fieldLabelWithValidationError - labelTextView.setTextColor(Color.RED) - } else { - labelTextView.text = popupField.label - labelTextView.setTextColor(oldColors) - } - } - } - } - } else { - valueEditText.visibility = View.GONE - codedValueDomainSpinner.visibility = View.GONE - valueTextView.visibility = View.VISIBLE - } - } - - /** - * Sets up spinner for PopupFields that have a CodedValueDomain. - */ - private fun setUpSpinner(codedValueDomain: CodedValueDomain, popupField: PopupField) { - val codedValuesNames = mutableListOf() - codedValueDomain.codedValues.forEach { codedValue -> codedValuesNames.add(codedValue.name) } - codedValueDomainSpinner.adapter = ArrayAdapter( - binding.root.context, - android.R.layout.simple_spinner_dropdown_item, - codedValuesNames - ) - val codedValuePosition = codedValueDomain.codedValues.indexOfFirst { codedValue -> - codedValue.code == popupManager.getFieldValue(popupField) - } - // set the PopupField value as selected in the spinner - codedValueDomainSpinner.setSelection(codedValuePosition) - codedValueDomainSpinner.onItemSelectedListener = - object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) { - TODO("Not yet implemented") - } - - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - popupManager.updateValue( - codedValueDomain.codedValues[position].code, - popupField - ) - } - } - } - - /** - * Updates the value of the specified PopupField to the appropriately cast string value of - * the specified value - */ - private fun updateValue(popupField: PopupField, newValue: String): ArcGISRuntimeException? { - var error: ArcGISRuntimeException? = null - when (popupManager.getFieldType(popupField)) { - Field.Type.SHORT -> error = - if (newValue.toShortOrNull() != null) { - popupManager.updateValue(newValue.toShort(), popupField) - } else { - popupManager.updateValue(newValue, popupField) - } - Field.Type.INTEGER -> error = - if (newValue.toIntOrNull() != null) { - popupManager.updateValue(newValue.toInt(), popupField) - } else { - popupManager.updateValue(newValue, popupField) - } - Field.Type.FLOAT -> error = - if (newValue.toFloatOrNull() != null) { - popupManager.updateValue(newValue.toFloat(), popupField) - } else { - popupManager.updateValue(newValue, popupField) - } - Field.Type.DOUBLE -> error = - if (newValue.toDoubleOrNull() != null) { - popupManager.updateValue(newValue.toDouble(), popupField) - } else { - popupManager.updateValue(newValue, popupField) - } - Field.Type.TEXT -> error = popupManager.updateValue(newValue, popupField) - - else -> Log.i( - TAG, - "Unhandled field type: " + popupManager.getFieldType(popupField) - ) - } - return error - } - - /** - * Returns the int value representing the input type for EditText view. - */ - private fun getInputType(fieldType: Field.Type): Int { - return when (fieldType) { - Field.Type.SHORT, Field.Type.INTEGER -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED - Field.Type.FLOAT, Field.Type.DOUBLE -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED - else -> InputType.TYPE_CLASS_TEXT - } - } - } -} diff --git a/app/src/main/res/drawable/ic_save_24.xml b/app/src/main/res/drawable/ic_save_24.xml new file mode 100644 index 0000000..a73dcf2 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_trash_24.xml b/app/src/main/res/drawable/ic_trash_24.xml new file mode 100644 index 0000000..db47363 --- /dev/null +++ b/app/src/main/res/drawable/ic_trash_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_x_circle_24.xml b/app/src/main/res/drawable/ic_x_circle_24.xml new file mode 100644 index 0000000..7720aa5 --- /dev/null +++ b/app/src/main/res/drawable/ic_x_circle_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-w600dp/fragment_data_collection.xml b/app/src/main/res/layout-w600dp/fragment_data_collection.xml index ea91a83..43cc201 100644 --- a/app/src/main/res/layout-w600dp/fragment_data_collection.xml +++ b/app/src/main/res/layout-w600dp/fragment_data_collection.xml @@ -19,10 +19,14 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - + + + - - + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/toolbar" + app:map="@{dataCollectionViewModel.mapViewModel.map}" /> - + @@ -83,9 +93,100 @@ app:navGraph="@navigation/bottomsheet_navigation" /> - - + + + + + + + + + + + + + + + + + - + + + - - - - + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/toolbar" + app:map="@{dataCollectionViewModel.mapViewModel.map}" /> + + @@ -83,9 +93,97 @@ app:navGraph="@navigation/bottomsheet_navigation" /> - - + + + + + + + + + + + + + + + + + + type="com.esri.arcgisruntime.toolkit.popup.PopupViewModel"/> @@ -45,6 +45,13 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="00.333" /> + + @@ -67,14 +74,27 @@ android:text="@{popupViewModel.popup}" android:textAlignment="viewStart" android:textSize="14sp" - app:layout_constraintStart_toEndOf="@id/symbol" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="0.148" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.65"/> + + diff --git a/app/src/main/res/layout/fragment_navigation_drawer.xml b/app/src/main/res/layout/fragment_navigation_drawer.xml index ef9cbbb..7a2c343 100644 --- a/app/src/main/res/layout/fragment_navigation_drawer.xml +++ b/app/src/main/res/layout/fragment_navigation_drawer.xml @@ -27,54 +27,89 @@ - - + android:layout_width="match_parent" + android:layout_height="match_parent"> + + - + app:layout_constraintTop_toBottomOf="@+id/profileStartGuideline" + app:layout_constraintVertical_bias="0.407"> + + + + + + + app:layout_constraintTop_toBottomOf="@+id/profileStartGuideline" + app:layout_constraintVertical_bias="0.307" /> + app:layout_constraintTop_toBottomOf="@+id/accessPortal" + app:layout_constraintVertical_bias="0.0" />