diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index c0b016a3..ad54610b 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -1,6 +1,7 @@ package net.vonforst.evmap.adapter import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt b/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt index c11e8195..e577a1da 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt @@ -1,10 +1,8 @@ package net.vonforst.evmap.fragment -import android.os.Bundle +import android.content.Context import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.core.os.BundleCompat import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager @@ -13,97 +11,53 @@ import net.vonforst.evmap.adapter.ConnectorAdapter import net.vonforst.evmap.adapter.ConnectorDetailsAdapter import net.vonforst.evmap.adapter.SingleViewAdapter import net.vonforst.evmap.api.availability.ChargeLocationStatus -import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.databinding.DialogConnectorDetailsBinding import net.vonforst.evmap.databinding.DialogConnectorDetailsHeaderBinding -import net.vonforst.evmap.databinding.DialogDataSourceSelectBinding -import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding import net.vonforst.evmap.model.Chargepoint -import net.vonforst.evmap.model.FILTERS_DISABLED import net.vonforst.evmap.storage.PreferenceDataSource -import net.vonforst.evmap.ui.MaterialDialogFragment -import java.time.Instant -class ConnectorDetailsDialog : MaterialDialogFragment() { - private lateinit var binding: DialogConnectorDetailsBinding - - companion object { - fun getInstance( - chargepoint: Chargepoint, - status: List?, - evseIds: List? = null, - labels: List? = null, - lastChange: List? = null - ): ConnectorDetailsDialog { - val dialog = ConnectorDetailsDialog() - dialog.arguments = Bundle().apply { - putParcelable("chargepoint", chargepoint) - putParcelableArrayList("status", status?.let { ArrayList(status) }) - putStringArrayList("evseIds", evseIds?.let { ArrayList(it) }) - putStringArrayList("labels", labels?.let { ArrayList(it) }) - putSerializable("lastChange", lastChange?.let { ArrayList(it) }) - } - return dialog - } - } - - override fun createView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = DialogConnectorDetailsBinding.inflate(inflater, container, false) - prefs = PreferenceDataSource(requireContext()) - return binding.root - } - - private lateinit var prefs: PreferenceDataSource - - override fun initView(view: View, savedInstanceState: Bundle?) { - val args = requireArguments() - - val chargepoint = BundleCompat.getParcelable(args, "chargepoint", Chargepoint::class.java)!! - val status = - BundleCompat.getParcelableArrayList(args, "status", ChargepointStatus::class.java) - val evseIds = args.getStringArrayList("evseIds") - val labels = args.getStringArrayList("labels") - val lastChange = args.getSerializable("lastChange") as ArrayList? - - val items = if (status != null) { - List(chargepoint.count) { i -> - ConnectorDetailsAdapter.ConnectorDetails( - status.get(i), - evseIds?.get(i), - labels?.get(i), - lastChange?.get(i) - ) - }.sortedBy { it.evseId ?: it.label } - } else emptyList() +class ConnectorDetailsDialog( + val binding: DialogConnectorDetailsBinding, + context: Context, + onClose: () -> Unit +) { + private val headerBinding: DialogConnectorDetailsHeaderBinding + private val detailsAdapter = ConnectorDetailsAdapter() + init { binding.list.apply { itemAnimator = null layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) } - - val headerBinding = DataBindingUtil.inflate( + headerBinding = DataBindingUtil.inflate( LayoutInflater.from(context), R.layout.dialog_connector_details_header, binding.list, false ) - if (items.isEmpty()) headerBinding.divider.visibility = View.GONE - - val joinedAdapter = ConcatAdapter( + binding.list.adapter = ConcatAdapter( SingleViewAdapter(headerBinding.root), - ConnectorDetailsAdapter().apply { - submitList(items) - } + detailsAdapter ) - - binding.list.adapter = joinedAdapter - headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(chargepoint, status) - binding.btnClose.setOnClickListener { - dismiss() + onClose() } } + + fun setData(cp: Chargepoint, status: ChargeLocationStatus?) { + val cpStatus = status?.status?.get(cp) + val items = if (status != null) { + List(cp.count) { i -> + ConnectorDetailsAdapter.ConnectorDetails( + cpStatus?.get(i), + status.evseIds?.get(cp)?.get(i), + status.labels?.get(cp)?.get(i), + status.lastChange?.get(cp)?.get(i) + ) + }.sortedBy { it.evseId ?: it.label } + } else emptyList() + detailsAdapter.submitList(items) + + headerBinding.divider.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE + headerBinding.item = ConnectorAdapter.ChargepointWithAvailability(cp, cpStatus) + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index 70e016e7..4fff8b85 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -53,6 +53,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.MaterialArcMotion import com.google.android.material.transition.MaterialContainerTransform +import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialSharedAxis import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike @@ -107,6 +108,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac private var requestingLocationUpdates = false private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior + private lateinit var detailsDialog: ConnectorDetailsDialog private lateinit var prefs: PreferenceDataSource private var markers: MutableBiMap = HashBiMap() private var clusterMarkers: List = emptyList() @@ -128,6 +130,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac return } + if (vm.selectedChargepoint.value != null) { + closeConnectorDetailsDialog() + vm.selectedChargepoint.value = null + return + } + if (binding.search.hasFocus()) { removeSearchFocus() } @@ -269,6 +277,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it } } bottomSheetBehavior.isCollapsible = bottomSheetCollapsible + binding.detailView.connectorDetails setupObservers() setupClickListeners() @@ -322,6 +331,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac binding.appLogo.root.visibility = View.GONE binding.search.visibility = View.VISIBLE } + + detailsDialog = + ConnectorDetailsDialog(binding.detailView.connectorDetails, requireContext()) { + closeConnectorDetailsDialog() + vm.selectedChargepoint.value = null + } } override fun onResume() { @@ -670,6 +685,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac vm.mapTrafficEnabled.observe(viewLifecycleOwner) { map?.setTrafficEnabled(it) } + vm.selectedChargepoint.observe(viewLifecycleOwner) { + binding.detailView.connectorDetailsCard.visibility = + if (it != null) View.VISIBLE else View.INVISIBLE + if (it != null) { + detailsDialog.setData(it, vm.availability.value?.data) + } + updateBackPressedCallback() + } updateBackPressedCallback() } @@ -714,6 +737,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac || vm.searchResult.value != null || (vm.layersMenuOpen.value ?: false) || binding.search.hasFocus() + || vm.selectedChargepoint.value != null } private fun unhighlightAllMarkers() { @@ -815,15 +839,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac binding.detailView.connectors.apply { adapter = ConnectorAdapter().apply { - onClickListener = { item -> - val dialog = ConnectorDetailsDialog.getInstance( - item.chargepoint, - item.status, - vm.availability.value?.data?.evseIds?.get(item.chargepoint), - vm.availability.value?.data?.labels?.get(item.chargepoint), - vm.availability.value?.data?.lastChange?.get(item.chargepoint), - ) - dialog.show(parentFragmentManager, null) + onClickListener = { + vm.selectedChargepoint.value = it.chargepoint + openConnectorDetailsDialog() } } itemAnimator = null @@ -910,6 +928,44 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } } + private fun openConnectorDetailsDialog() { + val chargepoints = vm.chargerDetails.value?.data?.chargepointsMerged ?: return + val chargepoint = vm.selectedChargepoint.value ?: return + val index = chargepoints.indexOf(chargepoint).takeIf { it >= 0 } ?: return + val vh = binding.detailView.connectors.findViewHolderForAdapterPosition(index) ?: return + + val materialTransform = MaterialContainerTransform().apply { + startView = vh.itemView + endView = binding.detailView.connectorDetailsCard + setPathMotion(MaterialArcMotion()) + duration = 250 + scrimColor = Color.TRANSPARENT + addTarget(binding.detailView.connectorDetailsCard) + isElevationShadowEnabled = false + fadeMode = FADE_MODE_CROSS + } + TransitionManager.beginDelayedTransition(binding.root, materialTransform) + } + + private fun closeConnectorDetailsDialog() { + val chargepoints = vm.chargerDetails.value?.data?.chargepointsMerged ?: return + val chargepoint = vm.selectedChargepoint.value ?: return + val index = chargepoints.indexOf(chargepoint).takeIf { it >= 0 } ?: return + val vh = binding.detailView.connectors.findViewHolderForAdapterPosition(index) ?: return + + val materialTransform = MaterialContainerTransform().apply { + startView = binding.detailView.connectorDetailsCard + endView = vh.itemView + setPathMotion(MaterialArcMotion()) + duration = 200 + scrimColor = Color.TRANSPARENT + addTarget(vh.itemView) + isElevationShadowEnabled = false + fadeMode = FADE_MODE_CROSS + } + TransitionManager.beginDelayedTransition(binding.root, materialTransform) + } + private fun showPaymentMethodsDialog(charger: ChargeLocation) { val activity = activity ?: return val chargecardData = vm.chargeCardMap.value ?: return diff --git a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt index 416207d9..4f1d45cd 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt @@ -139,8 +139,8 @@ fun setRecyclerViewData(recyclerView: ViewPager2, items: List?) { } @BindingAdapter("connectorIcon") -fun getConnectorItem(view: ImageView, type: String) { - view.setImageResource(iconForPlugType(type)) +fun getConnectorItem(view: ImageView, type: String?) { + view.setImageResource(type?.let { iconForPlugType(it) } ?: 0) } @BindingAdapter("srcCompat") diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index 92343c7e..8c16edfa 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -16,7 +16,6 @@ import kotlinx.parcelize.Parcelize import net.vonforst.evmap.api.availability.AvailabilityRepository import net.vonforst.evmap.api.availability.ChargeLocationStatus import net.vonforst.evmap.api.availability.tesla.Pricing -import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi import net.vonforst.evmap.api.createApi import net.vonforst.evmap.api.fronyx.PredictionData import net.vonforst.evmap.api.fronyx.PredictionRepository @@ -150,7 +149,11 @@ class MapViewModel(application: Application, private val state: SavedStateHandle } val chargerSparse: MutableLiveData by lazy { - state.getLiveData("chargerSparse") + state.getLiveData("chargerSparse").apply { + observeForever { + selectedChargepoint.value = null + } + } } private val triggerChargerDetailsRefresh = MutableLiveData(false) val chargerDetails: LiveData> = chargerSparse.switchMap { charger -> @@ -167,6 +170,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle } } + val selectedChargepoint: MutableLiveData by lazy { + state.getLiveData("selectedChargepoint") + } + val charger: MediatorLiveData> by lazy { MediatorLiveData>().apply { addSource(chargerDetails) { diff --git a/app/src/main/res/layout/detail_view.xml b/app/src/main/res/layout/detail_view.xml index 0c851ad2..923fe2eb 100644 --- a/app/src/main/res/layout/detail_view.xml +++ b/app/src/main/res/layout/detail_view.xml @@ -560,6 +560,24 @@ app:layout_constraintBottom_toBottomOf="@+id/textView13" app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintTop_toTopOf="@+id/textView13" /> + + + + diff --git a/app/src/main/res/layout/dialog_connector_details_header.xml b/app/src/main/res/layout/dialog_connector_details_header.xml index 691b1ae7..e92db8e9 100644 --- a/app/src/main/res/layout/dialog_connector_details_header.xml +++ b/app/src/main/res/layout/dialog_connector_details_header.xml @@ -79,7 +79,7 @@ android:layout_height="wrap_content" android:layout_marginStart="36dp" android:layout_marginTop="4dp" - android:text="@{UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + " · " + item.chargepoint.formatPower()}" + android:text="@{item != null ? UtilsKt.nameForPlugType(ChargepointApiKt.stringProvider(context), item.chargepoint.type) + " · " + item.chargepoint.formatPower() : null}" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" app:goneUnless="@{item.chargepoint.hasKnownPower()}" app:layout_constraintBottom_toTopOf="@id/textView8"