move stuff to fragment

This commit is contained in:
Johan von Forstner
2020-04-04 19:43:01 +02:00
parent c0428d463c
commit 7fa3830d17
10 changed files with 642 additions and 511 deletions

View File

@@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
compileSdkVersion 29
@@ -58,6 +59,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
@@ -73,6 +75,16 @@ dependencies {
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3'
// navigation library
def nav_version = "2.3.0-alpha04"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'

View File

@@ -12,6 +12,7 @@ import androidx.viewpager2.widget.ViewPager2
import com.johan.evmap.adapter.GalleryAdapter
import com.johan.evmap.adapter.galleryTransitionName
import com.johan.evmap.databinding.ActivityGalleryBinding
import com.johan.evmap.fragment.MapFragment
import com.ortiz.touchview.TouchImageView
@@ -68,8 +69,8 @@ class GalleryActivity : AppCompatActivity() {
override fun finishAfterTransition() {
isReturning = true
val data = Intent()
data.putExtra(MapsActivity.EXTRA_STARTING_GALLERY_POSITION, startingPosition)
data.putExtra(MapsActivity.EXTRA_CURRENT_GALLERY_POSITION, currentPosition)
data.putExtra(MapFragment.EXTRA_STARTING_GALLERY_POSITION, startingPosition)
data.putExtra(MapFragment.EXTRA_CURRENT_GALLERY_POSITION, currentPosition)
setResult(Activity.RESULT_OK, data)
super.finishAfterTransition()
}

View File

@@ -1,424 +1,70 @@
package com.johan.evmap
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.SharedElementCallback
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
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.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import com.google.android.material.snackbar.Snackbar
import com.johan.evmap.adapter.ConnectorAdapter
import com.johan.evmap.adapter.DetailAdapter
import com.johan.evmap.adapter.GalleryAdapter
import com.johan.evmap.adapter.galleryTransitionName
import com.johan.evmap.api.*
import com.johan.evmap.databinding.ActivityMapsBinding
import com.johan.evmap.ui.ClusterIconGenerator
import com.johan.evmap.ui.getBitmapDescriptor
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.johan.evmap.api.ChargeLocation
const val REQUEST_LOCATION_PERMISSION = 1
class MapsActivityViewModel : ViewModel() {
val chargepoints: MutableLiveData<List<ChargepointListItem>> by lazy {
MutableLiveData<List<ChargepointListItem>>().apply {
value = emptyList()
}
}
val charger: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
}
val availability: MutableLiveData<ChargeLocationStatus> by lazy {
MutableLiveData<ChargeLocationStatus>()
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
}
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var binding: ActivityMapsBinding
private var map: GoogleMap? = null
private lateinit var api: GoingElectricApi
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val vm: MapsActivityViewModel by viewModels()
private var markers: Map<Marker, ChargeLocation> = emptyMap()
private var clusterMarkers: List<Marker> = emptyList()
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private var reenterState: Bundle? = null
class MapsActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
api = GoingElectricApi.create(getString(R.string.goingelectric_key))
binding = DataBindingUtil.setContentView(this, R.layout.activity_maps)
binding.lifecycleOwner = this
binding.vm = vm
setContentView(R.layout.activity_maps)
ActivityCompat.setExitSharedElementCallback(this, exitElementCallback)
setSupportActionBar(binding.toolbar)
title = ""
//ActivityCompat.setExitSharedElementCallback(this, exitElementCallback)
//setSupportActionBar(binding.toolbar)
//title = ""
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
setupAdapters()
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
vm.charger.observe(this, object : Observer<ChargeLocation> {
var previousCharger = vm.charger.value
override fun onChanged(charger: ChargeLocation?) {
if (charger != null) {
if (previousCharger == null ||
previousCharger!!.id != charger.id
) {
vm.availability.value = null
bottomSheetBehavior.state =
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
loadChargerDetails()
}
} else {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
}
previousCharger = charger
}
})
vm.chargepoints.observe(this, Observer {
updateMap(it)
})
binding.fabLocate.setOnClickListener {
if (!hasLocationPermission()) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
} else {
enableLocation(true)
}
}
binding.fabDirections.setOnClickListener {
val charger = vm.charger.value
if (charger != null) {
val intent = Intent(Intent.ACTION_VIEW)
val coord = charger.coordinates
// google maps navigation
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
// fallback: generic geo intent
intent.data = Uri.parse("geo:${coord.lat},${coord.lng}")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
Snackbar.make(
binding.root,
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)
}
}
}
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value
if (charger != null) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https:${charger.url}"))
startActivity(intent)
}
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
}
private fun setupAdapters() {
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
override fun onItemClick(view: View, position: Int) {
val photos = vm.charger.value?.photos ?: return
val intent = Intent(this@MapsActivity, GalleryActivity::class.java).apply {
putExtra(GalleryActivity.EXTRA_PHOTOS, ArrayList<ChargerPhoto>(photos))
putExtra(GalleryActivity.EXTRA_POSITION, position)
}
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this@MapsActivity, view, view.transitionName
)
startActivity(intent, options.toBundle())
}
}
binding.gallery.apply {
adapter = GalleryAdapter(this@MapsActivity, galleryClickListener)
layoutManager =
LinearLayoutManager(this@MapsActivity, LinearLayoutManager.HORIZONTAL, false)
addItemDecoration(DividerItemDecoration(
this@MapsActivity, LinearLayoutManager.HORIZONTAL
).apply {
setDrawable(getDrawable(R.drawable.gallery_divider)!!)
})
}
binding.detailView.connectors.apply {
adapter = ConnectorAdapter()
layoutManager =
LinearLayoutManager(this@MapsActivity, LinearLayoutManager.HORIZONTAL, false)
}
binding.detailView.details.apply {
adapter = DetailAdapter()
layoutManager =
LinearLayoutManager(this@MapsActivity, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
this@MapsActivity,
LinearLayoutManager.VERTICAL
)
)
}
}
override fun onMapReady(map: GoogleMap) {
this.map = map
map.setOnCameraIdleListener {
loadChargepoints()
}
map.setOnMarkerClickListener { marker ->
when (marker) {
in markers -> {
vm.charger.value = markers[marker]
true
}
in clusterMarkers -> {
val newZoom = map.cameraPosition.zoom + 2
map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom))
true
}
else -> false
}
}
map.setOnMapClickListener {
vm.charger.value = null
}
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
map.setMapStyle(
if (mode == Configuration.UI_MODE_NIGHT_YES) {
MapStyleOptions.loadRawResourceStyle(this, R.raw.maps_night_mode)
} else null
)
if (hasLocationPermission()) {
enableLocation(false)
} else {
// center the camera on Europe
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
map.moveCamera(cameraUpdate)
}
}
@SuppressLint("MissingPermission")
private fun enableLocation(animate: Boolean) {
val map = this.map ?: return
map.isMyLocationEnabled = true
vm.myLocationEnabled.value = true
map.uiSettings.isMyLocationButtonEnabled = false
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f)
if (animate) {
map.animateCamera(camUpdate)
} else {
map.moveCamera(camUpdate)
}
}
}
}
private fun hasLocationPermission() =
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
private fun loadChargepoints() {
val map = this.map ?: return
val bounds = map.projection.visibleRegion.latLngBounds
api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = map.cameraPosition.zoom < 12, zoom = map.cameraPosition.zoom,
clusterDistance = 70
).enqueue(object : Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
//TODO: show error message
t.printStackTrace()
}
override fun onResponse(
call: Call<ChargepointList>,
response: Response<ChargepointList>
) {
if (!response.isSuccessful || response.body()!!.status != "ok") {
//TODO: show error message
return
}
vm.chargepoints.value = response.body()!!.chargelocations
}
})
}
private fun loadChargerDetails() {
val charger = vm.charger.value ?: return
api.getChargepointDetail(charger.id).enqueue(object : Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
//TODO: show error message
t.printStackTrace()
}
override fun onResponse(
call: Call<ChargepointList>,
response: Response<ChargepointList>
) {
if (!response.isSuccessful || response.body()!!.status != "ok") {
//TODO: show error message
return
}
vm.charger.value = response.body()!!.chargelocations[0] as ChargeLocation
}
})
lifecycleScope.launch {
loadChargerAvailability(charger)
}
}
private suspend fun loadChargerAvailability(charger: ChargeLocation) {
var availability: ChargeLocationStatus? = null
withContext(Dispatchers.IO) {
for (ad in availabilityDetectors) {
try {
availability = ad.getAvailability(charger)
break
} catch (e: IOException) {
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
e.printStackTrace()
}
}
}
vm.availability.value = availability
}
private fun updateMap(chargepoints: List<ChargepointListItem>) {
val map = this.map ?: return
markers.keys.forEach { it.remove() }
clusterMarkers.forEach { it.remove() }
val iconGenerator = ClusterIconGenerator(this)
val clusters = chargepoints.filterIsInstance<ChargeLocationCluster>()
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
markers = chargers.map { charger ->
map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
getBitmapDescriptor(
R.drawable.ic_map_marker_charging, when {
charger.maxPower >= 100 -> R.color.charger_100kw
charger.maxPower >= 43 -> R.color.charger_43kw
charger.maxPower >= 20 -> R.color.charger_20kw
charger.maxPower >= 11 -> R.color.charger_11kw
else -> R.color.charger_low
}, this
)
)
) to charger
}.toMap()
clusterMarkers = clusters.map { cluster ->
map.addMarker(
MarkerOptions()
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
.icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon(cluster.clusterCount.toString())))
)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_LOCATION_PERMISSION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
enableLocation(true)
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
navController = findNavController(R.id.nav_host_fragment)
}
override fun onBackPressed() {
if (bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED &&
bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
) {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
} else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) {
vm.charger.value = null
}
fun navigateTo(charger: ChargeLocation) {
val intent = Intent(Intent.ACTION_VIEW)
val coord = charger.coordinates
// google maps navigation
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
val pm = packageManager
if (intent.resolveActivity(pm) != null) {
startActivity(intent);
} else {
super.onBackPressed()
// fallback: generic geo intent
intent.data = Uri.parse("geo:${coord.lat},${coord.lng}")
if (intent.resolveActivity(pm) != null) {
startActivity(intent);
} else {
// TODO:
/*
Snackbar.make(
,
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)*/
}
}
}
private val exitElementCallback = object : SharedElementCallback() {
/*private val exitElementCallback = object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
if (reenterState != null) {
val startingPosition = reenterState!!.getInt(EXTRA_STARTING_GALLERY_POSITION)
val currentPosition = reenterState!!.getInt(EXTRA_CURRENT_GALLERY_POSITION)
val startingPosition = reenterState!!.getInt(MapFragment.EXTRA_STARTING_GALLERY_POSITION)
val currentPosition = reenterState!!.getInt(MapFragment.EXTRA_CURRENT_GALLERY_POSITION)
if (startingPosition != currentPosition) {
// Current element has changed, need to override previous exit transitions
val newTransitionName = galleryTransitionName(currentPosition)
@@ -435,15 +81,15 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
reenterState = null
}
}
}
}*/
override fun onActivityReenter(resultCode: Int, data: Intent) {
/*override fun onActivityReenter(resultCode: Int, data: Intent) {
// returning to gallery
super.onActivityReenter(resultCode, data)
reenterState = Bundle(data.extras)
reenterState?.let {
val startingPosition = it.getInt(EXTRA_STARTING_GALLERY_POSITION)
val currentPosition = it.getInt(EXTRA_CURRENT_GALLERY_POSITION)
val startingPosition = it.getInt(MapFragment.EXTRA_STARTING_GALLERY_POSITION)
val currentPosition = it.getInt(MapFragment.EXTRA_CURRENT_GALLERY_POSITION)
if (startingPosition != currentPosition) binding.gallery.scrollToPosition(
currentPosition
)
@@ -458,10 +104,5 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
}
})
}
}
companion object {
const val EXTRA_STARTING_GALLERY_POSITION = "extra_starting_item_position"
const val EXTRA_CURRENT_GALLERY_POSITION = "extra_current_item_position"
}
}*/
}

View File

@@ -0,0 +1,422 @@
package com.johan.evmap.fragment
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
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.*
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration
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.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import com.johan.evmap.GalleryActivity
import com.johan.evmap.MapsActivity
import com.johan.evmap.R
import com.johan.evmap.REQUEST_LOCATION_PERMISSION
import com.johan.evmap.adapter.ConnectorAdapter
import com.johan.evmap.adapter.DetailAdapter
import com.johan.evmap.adapter.GalleryAdapter
import com.johan.evmap.api.*
import com.johan.evmap.databinding.FragmentMapBinding
import com.johan.evmap.ui.ClusterIconGenerator
import com.johan.evmap.ui.getBitmapDescriptor
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
class MapViewModel : ViewModel() {
val chargepoints: MutableLiveData<List<ChargepointListItem>> by lazy {
MutableLiveData<List<ChargepointListItem>>().apply {
value = emptyList()
}
}
val charger: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
}
val availability: MutableLiveData<ChargeLocationStatus> by lazy {
MutableLiveData<ChargeLocationStatus>()
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
}
class MapFragment : Fragment(), OnMapReadyCallback {
private lateinit var binding: FragmentMapBinding
private val vm: MapViewModel by viewModels()
private var map: GoogleMap? = null
private lateinit var api: GoingElectricApi
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private var markers: Map<Marker, ChargeLocation> = emptyMap()
private var clusterMarkers: List<Marker> = emptyList()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
binding.lifecycleOwner = this
binding.vm = vm
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
api = GoingElectricApi.create(getString(R.string.goingelectric_key))
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
setupObservers()
setupClickListeners()
setupAdapters()
val navController = findNavController()
val appBarConfiguration = AppBarConfiguration(navController.graph)
view.findViewById<Toolbar>(R.id.toolbar)
.setupWithNavController(navController, appBarConfiguration)
}
private fun setupClickListeners() {
binding.fabLocate.setOnClickListener {
if (!hasLocationPermission()) {
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
} else {
enableLocation(true)
}
}
binding.fabDirections.setOnClickListener {
val charger = vm.charger.value
if (charger != null) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
(requireActivity() as MapsActivity).navigateTo(charger)
}
}
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value
if (charger != null) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https:${charger.url}"))
startActivity(intent)
}
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
}
private fun setupObservers() {
vm.charger.observe(viewLifecycleOwner, object : Observer<ChargeLocation> {
var previousCharger = vm.charger.value
override fun onChanged(charger: ChargeLocation?) {
if (charger != null) {
if (previousCharger == null ||
previousCharger!!.id != charger.id
) {
vm.availability.value = null
bottomSheetBehavior.state =
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
loadChargerDetails()
}
} else {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
}
previousCharger = charger
}
})
vm.chargepoints.observe(viewLifecycleOwner, Observer {
updateMap(it)
})
}
private fun setupAdapters() {
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
override fun onItemClick(view: View, position: Int) {
val photos = vm.charger.value?.photos ?: return
val intent = Intent(context, GalleryActivity::class.java).apply {
putExtra(GalleryActivity.EXTRA_PHOTOS, ArrayList<ChargerPhoto>(photos))
putExtra(GalleryActivity.EXTRA_POSITION, position)
}
// TODO:
/*val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
activity, view, view.transitionName
)
startActivity(intent, options.toBundle())*/
startActivity(intent)
}
}
binding.gallery.apply {
adapter = GalleryAdapter(context, galleryClickListener)
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
addItemDecoration(
DividerItemDecoration(
context, LinearLayoutManager.HORIZONTAL
).apply {
setDrawable(context.getDrawable(R.drawable.gallery_divider)!!)
})
}
binding.detailView.connectors.apply {
adapter = ConnectorAdapter()
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
}
binding.detailView.details.apply {
adapter = DetailAdapter()
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
context,
LinearLayoutManager.VERTICAL
)
)
}
}
override fun onMapReady(map: GoogleMap) {
this.map = map
map.setOnCameraIdleListener {
loadChargepoints()
}
map.setOnMarkerClickListener { marker ->
when (marker) {
in markers -> {
vm.charger.value = markers[marker]
true
}
in clusterMarkers -> {
val newZoom = map.cameraPosition.zoom + 2
map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom))
true
}
else -> false
}
}
map.setOnMapClickListener {
vm.charger.value = null
}
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
map.setMapStyle(
if (mode == Configuration.UI_MODE_NIGHT_YES) {
MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_night_mode)
} else null
)
if (hasLocationPermission()) {
enableLocation(false)
} else {
// center the camera on Europe
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
map.moveCamera(cameraUpdate)
}
}
@SuppressLint("MissingPermission")
private fun enableLocation(animate: Boolean) {
val map = this.map ?: return
map.isMyLocationEnabled = true
vm.myLocationEnabled.value = true
map.uiSettings.isMyLocationButtonEnabled = false
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f)
if (animate) {
map.animateCamera(camUpdate)
} else {
map.moveCamera(camUpdate)
}
}
}
}
private fun hasLocationPermission(): Boolean {
val context = context ?: return false
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) ==
PackageManager.PERMISSION_GRANTED
}
private fun loadChargepoints() {
val map = this.map ?: return
val bounds = map.projection.visibleRegion.latLngBounds
api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = map.cameraPosition.zoom < 12, zoom = map.cameraPosition.zoom,
clusterDistance = 70
).enqueue(object : Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
//TODO: show error message
t.printStackTrace()
}
override fun onResponse(
call: Call<ChargepointList>,
response: Response<ChargepointList>
) {
if (!response.isSuccessful || response.body()!!.status != "ok") {
//TODO: show error message
return
}
vm.chargepoints.value = response.body()!!.chargelocations
}
})
}
private fun loadChargerDetails() {
val charger = vm.charger.value ?: return
api.getChargepointDetail(charger.id).enqueue(object : Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
//TODO: show error message
t.printStackTrace()
}
override fun onResponse(
call: Call<ChargepointList>,
response: Response<ChargepointList>
) {
if (!response.isSuccessful || response.body()!!.status != "ok") {
//TODO: show error message
return
}
vm.charger.value = response.body()!!.chargelocations[0] as ChargeLocation
}
})
lifecycleScope.launch {
loadChargerAvailability(charger)
}
}
private suspend fun loadChargerAvailability(charger: ChargeLocation) {
var availability: ChargeLocationStatus? = null
withContext(Dispatchers.IO) {
for (ad in availabilityDetectors) {
try {
availability = ad.getAvailability(charger)
break
} catch (e: IOException) {
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
e.printStackTrace()
}
}
}
vm.availability.value = availability
}
private fun updateMap(chargepoints: List<ChargepointListItem>) {
val map = this.map ?: return
markers.keys.forEach { it.remove() }
clusterMarkers.forEach { it.remove() }
val context = context ?: return
val iconGenerator = ClusterIconGenerator(context)
val clusters = chargepoints.filterIsInstance<ChargeLocationCluster>()
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
markers = chargers.map { charger ->
map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
getBitmapDescriptor(
R.drawable.ic_map_marker_charging, when {
charger.maxPower >= 100 -> R.color.charger_100kw
charger.maxPower >= 43 -> R.color.charger_43kw
charger.maxPower >= 20 -> R.color.charger_20kw
charger.maxPower >= 11 -> R.color.charger_11kw
else -> R.color.charger_low
}, context
)
)
) to charger
}.toMap()
clusterMarkers = clusters.map { cluster ->
map.addMarker(
MarkerOptions()
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
.icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon(cluster.clusterCount.toString())))
)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_LOCATION_PERMISSION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
enableLocation(true)
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
fun goBack(): Boolean {
if (bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED &&
bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
) {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
return true
} else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) {
vm.charger.value = null
return true
} else {
return false
}
}
companion object {
const val EXTRA_STARTING_GALLERY_POSITION = "extra_starting_item_position"
const val EXTRA_CURRENT_GALLERY_POSITION = "extra_current_item_position"
}
}

View File

@@ -1,111 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout 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">
android:layout_width="match_parent"
android:layout_height="match_parent">
<data>
<import type="com.johan.evmap.MapsActivityViewModel" />
<variable
name="vm"
type="MapsActivityViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/root"
android:layout_width="match_parent"
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
android:layout_width="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MapsActivity" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:elevation="8dp"
app:cardCornerRadius="4dp"
app:layout_behavior="@string/ScrollingAppBarLayoutBehavior"
android:background="@drawable/rounded_rect"
android:backgroundTint="?android:colorBackground">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gallery"
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_height"
android:background="?android:colorBackground"
android:fitsSystemWindows="true"
app:data="@{vm.charger.photos}"
app:invisibleUnless="@{vm.charger.photos != null &amp;&amp; vm.charger.photos.size() > 0}"
app:layout_behavior="@string/BackDropBottomSheetBehavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_locate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_location"
app:backgroundTint="@android:color/white"
app:borderWidth="0dp"
app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior=".ui.HideOnScrollFabBehavior" />
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:fillViewport="true"
android:orientation="vertical"
app:anchorPoint="@dimen/gallery_height"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/peek_height"
app:defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
tools:defaultState="stateCollapsed">
<include
android:id="@+id/detail_view"
layout="@layout/detail_view"
app:charger="@{vm.charger}"
app:availability="@{vm.availability}" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_directions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_directions"
app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior" />
<!--<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/mergedappbarlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"/>-->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -0,0 +1,121 @@
<?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="com.johan.evmap.fragment.MapViewModel" />
<variable
name="vm"
type="MapViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MapsActivity" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:elevation="8dp"
app:cardCornerRadius="4dp"
app:layout_behavior="@string/ScrollingAppBarLayoutBehavior"
android:background="@drawable/rounded_rect"
android:backgroundTint="?android:colorBackground">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/search"
android:inputType="text" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gallery"
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_height"
android:background="?android:colorBackground"
android:fitsSystemWindows="true"
app:data="@{vm.charger.photos}"
app:invisibleUnless="@{vm.charger.photos != null &amp;&amp; vm.charger.photos.size() > 0}"
app:layout_behavior="@string/BackDropBottomSheetBehavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_locate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_location"
app:backgroundTint="@android:color/white"
app:borderWidth="0dp"
app:isFabActive="@{ vm.myLocationEnabled }"
app:layout_behavior=".ui.HideOnScrollFabBehavior" />
<androidx.core.widget.NestedScrollView
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:fillViewport="true"
android:orientation="vertical"
app:anchorPoint="@dimen/gallery_height"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/peek_height"
app:defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
tools:defaultState="stateCollapsed">
<include
android:id="@+id/detail_view"
layout="@layout/detail_view"
app:charger="@{vm.charger}"
app:availability="@{vm.availability}" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_directions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_directions"
app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior" />
<!--<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/mergedappbarlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"/>-->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/mapFragment">
<fragment
android:id="@+id/mapFragment"
android:name="com.johan.evmap.fragment.MapFragment"
android:label="MapFragment">
<action
android:id="@+id/action_mapFragment_to_galleryActivity"
app:destination="@id/galleryActivity" />
</fragment>
<activity
android:id="@+id/galleryActivity"
android:name="com.johan.evmap.GalleryActivity"
android:label="GalleryActivity" />
</navigation>

View File

@@ -21,4 +21,5 @@
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
<string name="go_to_goingelectric">Quelle: goingelectric.de</string>
<string name="search">Suche</string>
</resources>

View File

@@ -20,4 +20,5 @@
<string name="realtime_data_unavailable">Real-time status unavailable</string>
<string name="realtime_data_source">Real-time status source (beta): %s</string>
<string name="go_to_goingelectric">Source: goingelectric.de</string>
<string name="search">Search</string>
</resources>

View File

@@ -10,6 +10,9 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.0.0-beta03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
def nav_version = "2.3.0-alpha04"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}