简体   繁体   English

FusedLocationProviderClient 和 LocationCallback() 对象查询

[英]FusedLocationProviderClient and LocationCallback() object queries

To sum up the issue, I was implementing the google maps API to make a run tracker and was also implementing MVVM architecture at the same time.总结一下这个问题,我正在实现 google maps API 来制作运行跟踪器,同时也在实现 MVVM 架构。 However, when I managed to configure my code to place the LocationCallback object inside my viewModel and my fusedLocationProviderClient, my LocationCallback object is not able to be called at all ever after I visited the fragment I was trying to implement it.但是,当我设法配置我的代码以将 LocationCallback 对象放置在我的 viewModel 和我的 fusedLocationProviderClient 中时,我的 LocationCallback 对象在我访问我试图实现它的片段之后根本无法被调用。 I tried to Log.d anything inside the object but nothing would show up.我试图 Log.d 对象内的任何内容,但什么都不会出现。 Was wondering if I implemented it wrong.想知道我是否执行错了。 I have already requested for permissions in my main activity.我已经在我的主要活动中请求了权限。

RunSessionViewModel.kt RunSessionViewModel.kt

package com.example.myfit_exercisecompanion.ui.viewModels

import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build

import android.os.Looper
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts

import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

import com.example.myfit_exercisecompanion.repository.RunSessionRepository
import com.example.myfit_exercisecompanion.ui.MainActivity
import com.google.android.gms.location.*
import com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.SphericalUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.roundToInt

@HiltViewModel
class RunSessionViewModel @Inject constructor(
    private val runSessionRepository: RunSessionRepository,
    application: Application
): AndroidViewModel(application)  {

    private val _locationResults = MutableLiveData<LocationResult>()
            val locationResults: LiveData<LocationResult>
            get() = _locationResults

    val context = application

    val mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context.applicationContext)

    val ui = MutableLiveData(Ui.EMPTY)

    private val _locations = mutableListOf<LatLng>()
        val locations: List<LatLng>
        get() = _locations
    private var distance = 0

    private val _liveLocations = MutableLiveData<List<LatLng>>()
    val liveLocations: MutableLiveData<List<LatLng>>
        get() = _liveLocations

    private val _liveDistance = MutableLiveData<Int>()
    val liveDistance: MutableLiveData<Int>
        get() = _liveDistance

    private val _liveLocation = MutableLiveData<LatLng>()
    val liveLocation: MutableLiveData<LatLng>
        get() = _liveLocation


    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            super.onLocationResult(result)

            val currentLocation = result.lastLocation
            val latLng = LatLng(currentLocation!!.latitude, currentLocation.longitude)

            val lastLocation = _locations.lastOrNull()

            if (lastLocation != null) {
                distance += SphericalUtil.computeDistanceBetween(lastLocation, latLng).roundToInt()
                _liveDistance.value = distance
            }

            _locations.add(latLng)
            _liveLocations.value = locations
        }
    }

    @SuppressLint("MissingPermission")
    fun getUserLocation() {
        mFusedLocationProviderClient.lastLocation.addOnSuccessListener { location ->
            val latLng = LatLng(location.latitude, location.longitude)
            _locations.add(latLng)
            _liveLocation.value = latLng
        }
            .addOnFailureListener { failure ->
                Timber.d("lastLocation failure ${failure.message}")
            }
    }

    @SuppressLint("MissingPermission")
    private fun trackUser() {
        val locationRequest = LocationRequest.create().apply {
            priority = PRIORITY_HIGH_ACCURACY
            interval = 5000L
            fastestInterval = 2000L
        }
        mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
    }

    fun stopTracking() {
        mFusedLocationProviderClient.removeLocationUpdates(locationCallback)
        _locations.clear()
        distance = 0
        RPMLiveData().unloadStepCounter()
    }

    fun startTracking() {
        trackUser()

        val currentUi = ui.value
        ui.value = currentUi?.copy(
            formattedPace = Ui.EMPTY.formattedPace,
            formattedDistance = Ui.EMPTY.formattedDistance
        )
    }


    private val _liveSteps = MutableLiveData<Int>()
    val liveSteps:MutableLiveData<Int>
        get() = _liveSteps


    val rpmLiveData = RPMLiveData()

    // inner class just to have access to application
    inner class RPMLiveData : LiveData<String>(), SensorEventListener {
        private val sensorManager by lazy {
            context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        }

        private val stepCounterSensor: Sensor? by lazy {
            sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
        }

        private var initialSteps = -1

        fun setupStepCounter() {
            if (stepCounterSensor != null) {
                sensorManager.registerListener(this, stepCounterSensor,
                    SensorManager.SENSOR_DELAY_FASTEST
                )
            }
        }

        override fun onSensorChanged(event: SensorEvent) {
            event.values.firstOrNull()?.toInt()?.let { newSteps ->
                if (initialSteps == -1) {
                    initialSteps = newSteps
                }

                val currentSteps = newSteps - initialSteps

                _liveSteps.value = currentSteps
            }
        }

        fun unloadStepCounter() {
            if (stepCounterSensor != null) {
                sensorManager.unregisterListener(this)
            }
        }

        override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
    }
}


data class Ui(
    val formattedPace: String,
    val formattedDistance: String,
    val currentLocation: LatLng?,
    val userPath: List<LatLng>
) {

    companion object {
        val EMPTY = Ui(
            formattedPace = "",
            formattedDistance = "",
            currentLocation = null,
            userPath = emptyList()
        )
    }
}

RunTrackerFragment.kt RunTrackerFragment.kt

package com.example.myfit_exercisecompanion.ui.fragments

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import com.example.myfit_exercisecompanion.R
import com.example.myfit_exercisecompanion.databinding.FragmentRunTrackerBinding
import com.example.myfit_exercisecompanion.ui.MainActivity
import com.example.myfit_exercisecompanion.ui.viewModels.RunSessionViewModel
import com.example.myfit_exercisecompanion.ui.viewModels.Ui
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.LatLng
import com.google.android.gms.maps.model.PolylineOptions
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject

@AndroidEntryPoint
class RunTrackerFragment : Fragment(R.layout.fragment_run_tracker), OnMapReadyCallback {

    private lateinit var map: GoogleMap
    private var _binding: FragmentRunTrackerBinding? = null
    // This property is only valid between onCreateView and
// onDestroyView.
    private val binding get() = _binding!!

    private val viewModel: RunSessionViewModel by viewModels()

    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient

    var initialSteps = -1


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentRunTrackerBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        Timber.d("fusedLocationProviderClient not being a bitch ${viewModel.mFusedLocationProviderClient}")

        val mapFragment = childFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

        binding.btnStartStop.setOnClickListener {
            if (binding.btnStartStop.text == getString(R.string.start_label)) {
                startTracking()
                binding.btnStartStop.setText(R.string.stop_label)
            } else {
                stopTracking()
                binding.btnStartStop.setText(R.string.start_label)
            }
        }
        viewModel.liveLocations.observe(viewLifecycleOwner) { locations ->
            val current = viewModel.ui.value
            viewModel.ui.value = current?.copy(userPath = locations)
        }

        viewModel.liveLocation.observe(viewLifecycleOwner) { currentLocation ->
            val current = viewModel.ui.value
            viewModel.ui.value = current?.copy(currentLocation = currentLocation)
        }

        viewModel.liveDistance.observe(viewLifecycleOwner) { distance ->
            val current = viewModel.ui.value
            val formattedDistance = requireContext().getString(R.string.distance_value, distance)
            viewModel.ui.value = current?.copy(formattedDistance = formattedDistance)
        }

        viewModel.liveSteps.observe(viewLifecycleOwner) { steps ->
            val current = viewModel.ui.value
            viewModel.ui.value = current?.copy(formattedPace = "$steps")
        }
    }

    private val locationPermissionProviderContract = activity?.registerForActivityResult(
        ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            viewModel.getUserLocation()
            Timber.d("Pass 1")
        }
    }

    private val activityRecognitionPermissionProviderContract = activity?.registerForActivityResult(
        ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) {
            viewModel.RPMLiveData().setupStepCounter()
            Timber.d("Pass 2")
        }
    }

    fun requestUserLocation() {
        locationPermissionProviderContract?.launch(Manifest.permission.ACCESS_FINE_LOCATION)
    }

    fun requestActivityRecognition() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            activityRecognitionPermissionProviderContract?.launch(Manifest.permission.ACTIVITY_RECOGNITION)
        } else {
            viewModel.RPMLiveData().setupStepCounter()
        }
    }

    fun onMapLoaded(){
        requestUserLocation()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onMapReady(googleMap: GoogleMap) {
        map = googleMap
//        presenter = (activity as MainActivity).presenter

        viewModel.ui.observe(viewLifecycleOwner) { ui ->
            Log.d("ui instantiated", viewModel.ui.value.toString())
            updateUi(ui)
        }

        onMapLoaded()
        map.uiSettings.isZoomControlsEnabled = true
    }

    private fun startTracking() {
        binding.container.txtPace.text = ""
        binding.container.txtDistance.text = ""
        binding.container.txtTime.base = SystemClock.elapsedRealtime()
        binding.container.txtTime.start()
        map.clear()
        Log.d("type:", "${binding.container.txtTime.base}")

        viewModel.startTracking()
        requestActivityRecognition()
    }

    private fun stopTracking() {
        viewModel.stopTracking()
        binding.container.txtTime.stop()
    }

    @SuppressLint("MissingPermission")
    private fun updateUi(ui: Ui) {
        if (ui.currentLocation != null && ui.currentLocation != map.cameraPosition.target) {
            map.isMyLocationEnabled = true
            map.animateCamera(CameraUpdateFactory.newLatLngZoom(ui.currentLocation, 14f))
        }
        binding.container.txtDistance.text = ui.formattedDistance
        binding.container.txtPace.text = ui.formattedPace
        drawRoute(ui.userPath)
    }

    private fun drawRoute(locations: List<LatLng>) {
        val polylineOptions = PolylineOptions()

        map.clear()

        val points = polylineOptions.points
        points.addAll(locations)

        map.addPolyline(polylineOptions)
    }

}

because of the way my code is structured, the map appears on my fragment but not at my current location.由于我的代码的结构方式,地图出现在我的片段上,但不在我当前的位置。 Problem问题

The guide I was trying to follow was actually this link here and I wanted to convert it into a MVVM architecture.我试图遵循的指南实际上是这里的链接,我想将其转换为 MVVM 架构。 https://www.raywenderlich.com/28767779-how-to-make-an-android-run-tracking-app https://www.raywenderlich.com/28767779-how-to-make-an-android-run-tracking-app

The first question on this platform and relatively new to android development so pardon me if I had posted this wrongly or if my code is extremely inconsistent.这个平台上的第一个问题,对于 android 开发来说相对较新,如果我发错了或者我的代码非常不一致,请原谅我。

I use class LocationLiveData in my application, which extends MutableLiveData and it works perfect.我在我的应用程序中使用类 LocationLiveData,它扩展了 MutableLiveData 并且它工作得很好。 See code bellow:见下面的代码:

class LocationLiveData private constructor(context: Context) : MutableLiveData<Location>() {

    private val client: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)

    private var minAccuracy: Float = 0.toFloat()
    private var stopWhenCatch: Boolean = false
    private var stopped: Boolean = false

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            locationResult?.lastLocation?.let { location ->
                if (location.accuracy <= minAccuracy) {
                    checkLocationAccuracy()

                    value = location
                    if (stopWhenCatch) {
                        stopped(true)
                        stopWatch()
                    }
                }
            }
        }
    }

    override fun onActive() {
        if (value == null || stopped) {
            startWatch()
        }
    }

    override fun onInactive() {
        stopWatch()
    }

    fun stopped(stopped: Boolean) {
        this.stopped = stopped
    }

    @SuppressLint("MissingPermission")
    private fun checkLocationAccuracy() {
        if (minAccuracy == DEFAULT_LOW_ACCURACY) {
            minAccuracy = DEFAULT_HIGH_ACCURACY
        }
    }

    @SuppressLint("MissingPermission")
    private fun startWatch() {
        client.requestLocationUpdates(createLocationHighAccuracyRequest(), locationCallback, Looper.myLooper())
    }

    private fun stopWatch() {
        client.removeLocationUpdates(locationCallback)
    }

    private fun createLocationHighAccuracyRequest() =
        LocationRequest
            .create()
            .setInterval(INTERVAL)
            .setFastestInterval(FASTEST_INTERVAL)
            .setSmallestDisplacement(DISTANCE)
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)

    class Builder(private val mContext: Context) {

        private var minAccuracy: Float = DEFAULT_LOW_ACCURACY
        private var stopWhenCatch: Boolean = false

        fun minAccuracy(minAccuracy: Float): Builder {
            this.minAccuracy = minAccuracy
            return this
        }

        fun stopWhenCatch(stopWhenCatch: Boolean): Builder {
            this.stopWhenCatch = stopWhenCatch
            return this
        }

        fun build() = LocationLiveData(mContext).apply {
            minAccuracy = this@Builder.minAccuracy
            stopWhenCatch = this@Builder.stopWhenCatch
        }
    }

    companion object {
        private const val DEFAULT_HIGH_ACCURACY = 50f
        private const val DEFAULT_LOW_ACCURACY = 10_000f // low accuracy for fast finding location on start application, 10km
        private const val INTERVAL = 10_000L
        private const val FASTEST_INTERVAL = 5_000L
        private const val DISTANCE = 5f
    }
}

Code from ViewModel:来自 ViewModel 的代码:

private var locationLiveData: LocationLiveData? = null
fun subscribeLocationLiveData(context: Context): LocationLiveData {
    locationLiveData = LocationLiveData.Builder(context)
        .build()
    return locationLiveData!!
}
    
// call where you need to stop getting location
fun unsubscribeLocationLiveData() {
    locationLiveData?.stopped(true)
}

Code from fragment:片段代码:

private fun subscribeLocationUpdate() {
    viewModel.subscribeLocationLiveData(requireContext()).observe(
        viewLifecycleOwner,
        Observer { location ->
            // here will be user location
        }
    )
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM