Sharingang/Sharingang-Android

View on GitHub
app/src/main/java/com/example/sharingang/ui/fragments/MapFragment.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
package com.example.sharingang.ui.fragments

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.os.Bundle
import android.os.Looper
import android.view.*
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.coroutineScope
import androidx.navigation.fragment.findNavController
import com.example.sharingang.R
import com.example.sharingang.databinding.FragmentMapBinding
import com.example.sharingang.models.Item
import com.example.sharingang.utils.doOrGetPermission
import com.example.sharingang.utils.requestPermissionLauncher
import com.example.sharingang.viewmodels.ItemsViewModel
import com.google.android.gms.location.*
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.*
import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.collections.MarkerManager
import com.google.maps.android.ktx.awaitMap
import com.google.maps.android.ui.IconGenerator
import kotlin.properties.Delegates


const val DEFAULT_ZOOM = 15

/**
 * Fragment to display a map and the items' location on the map.
 */
class MapFragment : Fragment() {
    private lateinit var binding: FragmentMapBinding
    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private val requestPermissionLauncher = requestPermissionLauncher(this) {
        doOrGetPermission(
            this, Manifest.permission.ACCESS_FINE_LOCATION, { startLocationUpdates() }, null
        )
    }
    private var lastLocation: Location? = null
    private val locationCallback: LocationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            lastLocation = locationResult.lastLocation
            if (!hasCameraMovedOnce) {
                moveCameraToLastLocation()
                hasCameraMovedOnce = true
            }
        }
    }

    private var map: GoogleMap? = null
    private var markerManager: MarkerManager? = null
    private var clusterManager: ClusterManager<MapItem>? = null

    private val viewModel: ItemsViewModel by activityViewModels()
    private var hasCameraMovedOnce by Delegates.notNull<Boolean>()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
        hasCameraMovedOnce = false
        binding.linearSearchOnMap.visibility = View.GONE
        viewModel.searchResults.observe(viewLifecycleOwner, {
            addItemMarkers(it)
        })
        setupButtons()
        binding.mapView.onCreate(savedInstanceState)

        initMap()

        return binding.root
    }

    private fun setupButtons() {
        binding.mapGetMyLocation.setOnClickListener {
            if (lastLocation != null) {
                moveCameraToLastLocation()
            }
        }

        binding.mapStartSearch.setOnClickListener {
            if (binding.linearSearchOnMap.visibility == View.VISIBLE) {
                startSearch()
                binding.linearSearchOnMap.visibility = View.GONE
            } else {
                binding.linearSearchOnMap.visibility = View.VISIBLE
            }
        }
    }

    private fun startSearch() {
        viewModel.searchItems(
            binding.searchOnMap.text.toString(),
            binding.mapCategorySpinner.selectedItemPosition,
            false
        )
    }

    private fun addItemMarkers(items: List<Item>) {
        clusterManager?.clearItems()
        clusterManager?.addItems(items.map { MapItem(it) })
        clusterManager?.cluster()
    }

    @SuppressLint("MissingPermission")
    private fun startLocationUpdates() {
        // Show blue dot at current location
        map?.isMyLocationEnabled = true
        // We already have a custom button so we remove the default one
        map?.uiSettings?.isMyLocationButtonEnabled = false

        fusedLocationClient.requestLocationUpdates(
            LocationRequest.create().apply {
                interval = 5000
                fastestInterval = 2500
                priority = LocationRequest.PRIORITY_HIGH_ACCURACY
            },
            locationCallback,
            Looper.getMainLooper()
        )
    }

    private fun moveCameraToLastLocation() {
        map?.animateCamera(
            CameraUpdateFactory.newLatLngZoom(
                LatLng(
                    lastLocation!!.latitude,
                    lastLocation!!.longitude
                ), DEFAULT_ZOOM.toFloat()
            )
        )
    }

    private fun initMap() {
        lifecycle.coroutineScope.launchWhenCreated {
            map = binding.mapView.awaitMap()

            initCluster()

            addItemMarkers(viewModel.searchResults.value ?: listOf())

            doOrGetPermission(
                this@MapFragment,
                Manifest.permission.ACCESS_FINE_LOCATION,
                { startLocationUpdates() },
                requestPermissionLauncher
            )
        }
    }

    private fun initCluster() {
        markerManager = MarkerManager(map)
        clusterManager = ClusterManager<MapItem>(context, map, markerManager)
        clusterManager!!.renderer = ItemRenderer(context, map, clusterManager)
        clusterManager!!.setOnClusterItemClickListener { marker ->
            map!!.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
            findNavController().navigate(
                MapFragmentDirections.actionMapFragmentToDetailedItemFragment(marker.item)
            )
            true
        }

        map!!.setOnCameraIdleListener(clusterManager)
    }

    override fun onResume() {
        binding.mapView.onResume()
        super.onResume()
    }

    override fun onPause() {
        super.onPause()
        binding.mapView.onPause()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    override fun onDestroy() {
        super.onDestroy()
        binding.mapView.onDestroy()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    override fun onLowMemory() {
        super.onLowMemory()
        binding.mapView.onLowMemory()
    }

    /**
     * Class used to represent an item on the map.
     */
    inner class MapItem(val item: Item) : ClusterItem {
        private val position = LatLng(item.latitude, item.longitude)
        private val title: String = item.title
        private val snippet: String = getString(R.string.display_price, item.price)

        override fun getPosition() = position
        override fun getTitle() = title
        override fun getSnippet() = snippet
    }

    inner class ItemRenderer<T : ClusterItem>(
        context: Context?,
        map: GoogleMap?,
        clusterManager: ClusterManager<T>?
    ) : DefaultClusterRenderer<T>(context, map, clusterManager) {
        private val iconGenerator: IconGenerator = IconGenerator(context)

        @SuppressLint("InflateParams")
        private var markerView: View = layoutInflater.inflate(R.layout.item_marker, null)

        init {
            super.setMinClusterSize(3)
            iconGenerator.setContentView(markerView)
        }

        override fun onBeforeClusterItemRendered(item: T, markerOptions: MarkerOptions) {
            markerOptions.icon(getItemIcon(item))
        }

        override fun onClusterItemUpdated(item: T, marker: Marker) {
            marker.setIcon(getItemIcon(item))
        }

        private fun getItemIcon(item: ClusterItem): BitmapDescriptor? {
            markerView.findViewById<TextView>(R.id.titleText).text = item.title
            markerView.findViewById<TextView>(R.id.descriptionText).text = item.snippet
            val icon = iconGenerator.makeIcon()
            return BitmapDescriptorFactory.fromBitmap(icon)
        }
    }
}