add favorites view

This commit is contained in:
Johan von Forstner
2020-04-19 22:19:29 +02:00
parent febc72f190
commit 84bbdaf4ec
17 changed files with 363 additions and 25 deletions

View File

@@ -68,7 +68,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.maps.android:android-maps-utils:0.5'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:8ec7fee516'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f5195b3266'
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.android.libraries.places:places:2.2.0'

View File

@@ -13,6 +13,7 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.FavoritesViewModel
interface Equatable {
override fun equals(other: Any?): Boolean;
@@ -29,15 +30,15 @@ abstract class DataBindingAdapter<T : Equatable>() :
}
override fun onBindViewHolder(holder: ViewHolder<T>, position: Int) =
holder.bind(getItem(position))
bind(holder, getItem(position))
class ViewHolder<T>(private val binding: ViewDataBinding) :
class ViewHolder<T>(val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
}
fun bind(item: T) {
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
open fun bind(holder: ViewHolder<T>, item: T) {
holder.binding.setVariable(BR.item, item)
holder.binding.executePendingBindings()
}
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
@@ -112,3 +113,13 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
else null
)
}
class FavoritesAdapter(val vm: FavoritesViewModel) : DataBindingAdapter<ChargeLocation>() {
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun bind(holder: ViewHolder<ChargeLocation>, item: ChargeLocation) {
holder.binding.setVariable(BR.vm, vm)
super.bind(holder, item)
}
}

View File

@@ -44,7 +44,7 @@ data class ChargeLocation(
//val chargecards: Boolean?
@Embedded val openinghours: OpeningHours?,
@Embedded val cost: Cost?
) : ChargepointListItem() {
) : ChargepointListItem(), Equatable {
val maxPower: Double
get() {
return chargepoints.map { it.power }.max() ?: 0.0

View File

@@ -0,0 +1,94 @@
package net.vonforst.evmap.fragment
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.model.LatLng
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FavoritesAdapter
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment() {
private lateinit var binding: FragmentFavoritesBinding
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
viewModelFactory {
FavoritesViewModel(
requireActivity().application,
getString(R.string.goingelectric_key)
)
}
})
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_favorites, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
val navController = findNavController()
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.favsList.apply {
adapter = FavoritesAdapter(vm)
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
context, LinearLayoutManager.VERTICAL
)
)
}
vm.favorites.observe(viewLifecycleOwner, Observer {
print(it.toString())
})
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
vm.location.value = LatLng(location.latitude, location.longitude)
}
}
}
}

View File

@@ -78,6 +78,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private lateinit var clusterIconGenerator: ClusterIconGenerator
private lateinit var chargerIconGenerator: ChargerIconGenerator
private lateinit var animator: MarkerAnimator
private lateinit var favToggle: MenuItem
override fun onCreateView(
inflater: LayoutInflater,
@@ -115,6 +116,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav)
setupObservers()
setupClickListeners()
setupAdapters()
@@ -173,6 +177,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.detailAppBar.toolbar.setNavigationOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
}
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_fav -> {
toggleFavorite()
true
}
else -> false
}
}
}
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
vm.deleteFavorite(charger)
} else {
vm.insertFavorite(charger)
}
}
private fun setupObservers() {
@@ -193,6 +216,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle()
} else {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
}
@@ -201,6 +225,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val chargepoints = it.data
if (chargepoints != null) updateMap(chargepoints)
})
vm.favorites.observe(viewLifecycleOwner, Observer {
updateFavoriteToggle()
})
}
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
favToggle.setIcon(R.drawable.ic_fav)
} else {
favToggle.setIcon(R.drawable.ic_fav_no)
}
}
private fun setupAdapters() {
@@ -340,8 +377,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) ==
PackageManager.PERMISSION_GRANTED
) == PackageManager.PERMISSION_GRANTED
}
private fun updateMap(chargepoints: List<ChargepointListItem>) {

View File

@@ -7,10 +7,10 @@ import net.vonforst.evmap.api.goingelectric.ChargeLocation
@Dao
interface ChargeLocationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg locations: ChargeLocation)
suspend fun insert(vararg locations: ChargeLocation)
@Delete
fun delete(vararg locations: ChargeLocation)
suspend fun delete(vararg locations: ChargeLocation)
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>

View File

@@ -19,23 +19,23 @@ class Converters {
}
@TypeConverter
fun fromChargepointList(value: List<Chargepoint>): String {
fun fromChargepointList(value: List<Chargepoint>?): String {
return chargepointListAdapter.toJson(value)
}
@TypeConverter
fun toChargepointList(value: String): List<Chargepoint> {
return chargepointListAdapter.fromJson(value)!!
fun toChargepointList(value: String): List<Chargepoint>? {
return chargepointListAdapter.fromJson(value)
}
@TypeConverter
fun fromChargerPhotoList(value: List<ChargerPhoto>): String {
fun fromChargerPhotoList(value: List<ChargerPhoto>?): String {
return chargerPhotoListAdapter.toJson(value)
}
@TypeConverter
fun toChargerPhotoList(value: String): List<ChargerPhoto> {
return chargerPhotoListAdapter.fromJson(value)!!
fun toChargerPhotoList(value: String): List<ChargerPhoto>? {
return chargerPhotoListAdapter.fromJson(value)
}
@TypeConverter

View File

@@ -0,0 +1,54 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
class FavoritesViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {
private var api = GoingElectricApi.create(geApiKey)
private var db = AppDatabase.getInstance(application)
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
}
val location: MutableLiveData<LatLng> by lazy {
MutableLiveData<LatLng>()
}
/*val availability: MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>> by lazy {
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
addSource(favorites) { chargers ->
if (chargers != null) {
viewModelScope.launch {
chargers.map {
availability.value = Resource.loading(null)
}
}
} else {
value = null
}
}
}
}*/
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
}
}
fun deleteFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
}
}
}

View File

@@ -91,11 +91,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
fun insertFavorite(charger: ChargeLocation) {
db.chargeLocationsDao().insert(charger)
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
}
}
fun deleteFavorite(charger: ChargeLocation) {
db.chargeLocationsDao().delete(charger)
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
}
}
private fun loadChargepoints(mapPosition: MapPosition) {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z" />
</vector>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.FavoritesViewModel" />
<variable
name="vm"
type="FavoritesViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favs_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:data="@{vm.favorites}" />
</LinearLayout>
</layout>

View File

@@ -145,6 +145,7 @@
android:id="@+id/detail_app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"></com.mahc.custombottomsheetbehavior.MergedAppBarLayout>
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.api.UtilsKt" />
<variable
name="item"
type="net.vonforst.evmap.api.goingelectric.ChargeLocation" />
<variable
name="vm"
type="net.vonforst.evmap.viewmodel.FavoritesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/textView15"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
<TextView
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.address.toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView15"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.formatChargepoints()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
<TextView
android:id="@+id/textView16"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/distance_format(UtilsKt.distanceBetween(vm.location.latitude, vm.location.longitude, item.coordinates.lat, item.coordinates.lng) / 1000)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_fav"
android:icon="@drawable/ic_fav_no"
android:title="@string/fav_add"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -12,7 +12,7 @@
tools:layout="@layout/fragment_map">
<action
android:id="@+id/action_map_to_galleryFragment"
app:destination="@id/galleryFragment"
app:destination="@id/gallery"
app:enterAnim="@anim/fragment_fade_enter"
app:exitAnim="@anim/fragment_fade_exit"
app:popEnterAnim="@anim/fragment_fade_enter"
@@ -21,9 +21,16 @@
<fragment
android:id="@+id/about"
android:name="net.vonforst.evmap.fragment.AboutFragment"
android:label="@string/about" />
android:label="@string/about"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/galleryFragment"
android:id="@+id/gallery"
android:name="net.vonforst.evmap.fragment.GalleryFragment"
android:label="GalleryFragment"></fragment>
android:label="GalleryFragment"
tools:layout="@layout/fragment_gallery" />
<fragment
android:id="@+id/favs"
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
android:label="@string/menu_favs"
tools:layout="@layout/fragment_favorites" />
</navigation>

View File

@@ -36,4 +36,7 @@
<string name="copyright_summary">©2020 Johan von Forstner</string>
<string name="other">Sonstiges</string>
<string name="privacy">Datenschutzerklärung</string>
<string name="fav_add">Zu Favoriten hinzufügen</string>
<string name="fav_remove">Aus Favoriten entfernen</string>
<string name="distance_format">%.1f km</string>
</resources>

View File

@@ -35,4 +35,7 @@
<string name="copyright_summary">©2020 Johan von Forstner</string>
<string name="other">Other</string>
<string name="privacy">Privacy Notice</string>
<string name="fav_add">Add to favorites</string>
<string name="fav_remove">Remove from favorites</string>
<string name="distance_format">%.1f km</string>
</resources>