Skip to content

Commit 92a7b7e

Browse files
Merge pull request #47 from boostcampwm-2022/45-feature-mission-user
์ง€๋„์— ์‚ฌ์šฉ์ž์˜ ์ด๋™๊ฒฝ๋กœ ๊ทธ๋ฆฌ๊ธฐ, ์ด๋™๊ฒฝ๋กœ๋ฅผ ํ‰๋‚ด๋‚ด๋Š” ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ
2 parents cbfae37 + 402506c commit 92a7b7e

File tree

7 files changed

+409
-1
lines changed

7 files changed

+409
-1
lines changed
Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
11
package com.stop.ui.mission
22

3+
import android.Manifest
4+
import android.content.ContextWrapper
35
import android.os.Bundle
46
import android.view.LayoutInflater
57
import android.view.View
68
import android.view.ViewGroup
9+
import androidx.activity.result.contract.ActivityResultContracts
710
import androidx.fragment.app.Fragment
11+
import androidx.fragment.app.viewModels
12+
import com.stop.R
813
import com.stop.databinding.FragmentMissionBinding
14+
import dagger.hilt.android.AndroidEntryPoint
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.delay
18+
import kotlinx.coroutines.launch
19+
import java.io.BufferedReader
20+
import java.io.InputStreamReader
921

10-
class MissionFragment : Fragment() {
22+
@AndroidEntryPoint
23+
class MissionFragment : Fragment(), TMapHandler {
1124

1225
private var _binding: FragmentMissionBinding? = null
1326
private val binding: FragmentMissionBinding
1427
get() = _binding!!
1528

29+
private val viewModel: MissionViewModel by viewModels()
30+
31+
private lateinit var tMap: TMap
32+
33+
private val permissions = arrayOf(
34+
Manifest.permission.ACCESS_FINE_LOCATION,
35+
Manifest.permission.ACCESS_COARSE_LOCATION
36+
)
37+
38+
private val requestPermissionsLauncher = registerForActivityResult(
39+
ActivityResultContracts.RequestMultiplePermissions()
40+
) { permissions ->
41+
if (permissions.entries.any { it.value }) {
42+
tMap.setTrackingMode()
43+
}
44+
}
45+
1646
override fun onCreateView(
1747
inflater: LayoutInflater, container: ViewGroup?,
1848
savedInstanceState: Bundle?
@@ -21,9 +51,108 @@ class MissionFragment : Fragment() {
2151
return binding.root
2252
}
2353

54+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55+
super.onViewCreated(view, savedInstanceState)
56+
57+
setDataBinding()
58+
initViewModel()
59+
initTMap()
60+
setObserve()
61+
}
62+
2463
override fun onDestroyView() {
2564
_binding = null
2665

2766
super.onDestroyView()
2867
}
68+
69+
override fun alertTMapReady() {
70+
requestPermissionsLauncher.launch(permissions)
71+
mimicUserMove()
72+
}
73+
74+
private fun mimicUserMove() {
75+
val lines = readFromAssets()
76+
77+
CoroutineScope(Dispatchers.IO).launch {
78+
lines.forEach { line ->
79+
val (longitude, latitude) = line.split(",")
80+
tMap.moveLocation(longitude, latitude)
81+
delay(500)
82+
}
83+
}
84+
}
85+
86+
/**
87+
* ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ assets ํด๋”์— ์ขŒํ‘œ๊ฐ€ longitude,latitude๋กœ ๋‚˜์—ด๋˜์–ด ์žˆ๋Š” txt ํŒŒ์ผ์ด
88+
* ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
89+
* ํŒŒ์ผ์˜ ์ด๋ฆ„์€ ์•„๋ž˜ companion object์— ์žˆ๋Š” FAKE_USER_FILE_PATH ๋ณ€์ˆซ๊ฐ’๊ณผ ๋™์ผํ•˜๊ฒŒ ํ•ด์ฃผ์„ธ์š”.
90+
*/
91+
private fun readFromAssets(): List<String> {
92+
val reader =
93+
BufferedReader(InputStreamReader(requireContext().assets.open(FAKE_USER_FILE_PATH)))
94+
val lines = arrayListOf<String>()
95+
var line = reader.readLine()
96+
while (line != null) {
97+
lines.add(line)
98+
line = reader.readLine()
99+
}
100+
reader.close()
101+
return lines
102+
}
103+
104+
private fun setDataBinding() {
105+
binding.lifecycleOwner = viewLifecycleOwner
106+
binding.viewModel = viewModel
107+
}
108+
109+
private fun initTMap() {
110+
tMap = TMap((requireContext() as ContextWrapper).baseContext, this)
111+
tMap.init()
112+
113+
binding.constraintLayoutContainer.addView(tMap.getTMapView())
114+
}
115+
116+
private fun initViewModel() {
117+
viewModel.setDestination(DESTINATION)
118+
viewModel.countDownWith(LEFT_TIME)
119+
}
120+
121+
private fun setObserve() {
122+
val shortAnimationDuration =
123+
resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
124+
125+
viewModel.timeIncreased.observe(viewLifecycleOwner) {
126+
val sign = if (it > 0) {
127+
PLUS
128+
} else {
129+
MINUS
130+
}
131+
with(binding.textViewChangedTime) {
132+
text = resources.getString(R.string.number_format).format(sign, it)
133+
visibility = View.VISIBLE
134+
this.animate().alpha(ALPHA_VISIBLE)
135+
.setDuration(shortAnimationDuration)
136+
.withEndAction {
137+
animate().alpha(ALPHA_INVISIBLE)
138+
.setDuration(shortAnimationDuration)
139+
.withEndAction {
140+
visibility = View.INVISIBLE
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
companion object {
148+
private const val DESTINATION = "๊ตฌ๋กœ3๋™ํ˜„๋Œ€์•„ํŒŒํŠธ"
149+
private const val PLUS = "+"
150+
private const val MINUS = ""
151+
private const val LEFT_TIME = 60
152+
153+
private const val FAKE_USER_FILE_PATH = "fake_user_path.txt"
154+
155+
private const val ALPHA_VISIBLE = 1f
156+
private const val ALPHA_INVISIBLE = 0f
157+
}
29158
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.stop.ui.mission
2+
3+
import androidx.lifecycle.LiveData
4+
import androidx.lifecycle.MutableLiveData
5+
import androidx.lifecycle.Transformations
6+
import androidx.lifecycle.ViewModel
7+
import androidx.lifecycle.viewModelScope
8+
import kotlinx.coroutines.delay
9+
import kotlinx.coroutines.launch
10+
import kotlin.random.Random
11+
12+
class MissionViewModel : ViewModel() {
13+
14+
private val random = Random(System.currentTimeMillis())
15+
16+
private val _destination = MutableLiveData<String>()
17+
val destination: LiveData<String>
18+
get() = _destination
19+
20+
private val _timeIncreased = MutableLiveData<Int>()
21+
val timeIncreased: LiveData<Int>
22+
get() = _timeIncreased
23+
24+
private val _estimatedTimeRemaining = MutableLiveData<Int>()
25+
val estimatedTimeRemaining: LiveData<Int>
26+
get() = _estimatedTimeRemaining
27+
28+
val leftMinute: LiveData<String> = Transformations.switchMap(estimatedTimeRemaining) {
29+
MutableLiveData<String>().apply {
30+
value = (it / TIME_UNIT).toString().padStart(TIME_DIGIT, '0')
31+
}
32+
}
33+
34+
val leftSecond: LiveData<String> = Transformations.switchMap(estimatedTimeRemaining) {
35+
MutableLiveData<String>().apply {
36+
value = (it % TIME_UNIT).toString().padStart(TIME_DIGIT, '0')
37+
}
38+
}
39+
40+
fun setDestination(inputDestination: String) {
41+
_destination.value = inputDestination
42+
}
43+
44+
fun countDownWith(startTime: Int) {
45+
_estimatedTimeRemaining.value = startTime
46+
var leftTime = startTime
47+
viewModelScope.launch {
48+
while (leftTime > TIME_ZERO) {
49+
delay(DELAY_TIME)
50+
leftTime -= ONE_SECOND
51+
if (leftTime <= TIME_ZERO) {
52+
break
53+
}
54+
_estimatedTimeRemaining.value = leftTime
55+
}
56+
}
57+
58+
viewModelScope.launch {
59+
while (leftTime > TIME_ZERO) {
60+
delay(DELAY_TIME)
61+
if (random.nextInt(ZERO, RANDOM_LIMIT) == ZERO) {
62+
val increasedTime = random.nextInt(-RANDOM_LIMIT, RANDOM_LIMIT)
63+
if (increasedTime == ZERO) {
64+
continue
65+
}
66+
leftTime += increasedTime
67+
_timeIncreased.value = increasedTime
68+
}
69+
if (leftTime < TIME_ZERO) {
70+
leftTime = 0
71+
}
72+
_estimatedTimeRemaining.value = leftTime
73+
}
74+
}
75+
}
76+
77+
companion object {
78+
private const val DELAY_TIME = 1000L
79+
private const val TIME_ZERO = 0
80+
private const val TIME_UNIT = 60
81+
private const val TIME_DIGIT = 2
82+
private const val ONE_SECOND = 1
83+
private const val RANDOM_LIMIT = 5
84+
private const val ZERO = 0
85+
}
86+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.stop.ui.mission
2+
3+
import android.content.Context
4+
import android.location.Location
5+
import androidx.core.content.ContextCompat
6+
import androidx.core.graphics.drawable.toBitmap
7+
import com.skt.tmap.TMapGpsManager
8+
import com.skt.tmap.TMapPoint
9+
import com.skt.tmap.TMapView
10+
import com.skt.tmap.overlay.TMapMarkerItem
11+
import com.skt.tmap.overlay.TMapPolyLine
12+
import com.stop.BuildConfig
13+
import com.stop.R
14+
15+
class TMap(
16+
private val context: Context,
17+
private val tMapHandler: TMapHandler,
18+
) {
19+
20+
private lateinit var tMapView: TMapView
21+
private var isTracking = true
22+
private var lineNum = 0
23+
private val mockLocation = Location("")
24+
25+
private val onLocationChangeListener = TMapGpsManager.OnLocationChangedListener { location ->
26+
if (location != null && checkKoreaLocation(location)) {
27+
val beforeLocationPoint = tMapView.locationPoint
28+
drawMoveLine(location, beforeLocationPoint)
29+
movePin(location)
30+
}
31+
}
32+
33+
private fun checkKoreaLocation(location: Location): Boolean {
34+
return location.longitude in KOREA_LONGITUDE_MIN..KOREA_LONGITUDE_MAX
35+
&& location.latitude in KOREA_LATITUDE_MIN..KOREA_LATITUDE_MAX
36+
}
37+
38+
fun init() {
39+
tMapView = TMapView(context)
40+
tMapView.setSKTMapApiKey(BuildConfig.TMAP_APP_KEY)
41+
tMapView.setOnMapReadyListener {
42+
tMapView.setVisibleLogo(false)
43+
tMapView.mapType = TMapView.MapType.DEFAULT
44+
tMapView.zoomLevel = DEFAULT_ZOOM_LEVEL
45+
46+
tMapHandler.alertTMapReady()
47+
}
48+
tMapView.setOnEnableScrollWithZoomLevelListener { _, _ ->
49+
isTracking = false
50+
}
51+
}
52+
53+
fun setTrackingMode() {
54+
val manager = TMapGpsManager(context).apply {
55+
minDistance = RECOGNIZABLE_MINIMUM_DISTANCE_CHANGE
56+
provider = TMapGpsManager.PROVIDER_GPS
57+
openGps()
58+
provider = TMapGpsManager.PROVIDER_NETWORK
59+
openGps()
60+
}
61+
62+
manager.setOnLocationChangeListener(onLocationChangeListener)
63+
}
64+
65+
fun getTMapView(): TMapView {
66+
return tMapView
67+
}
68+
69+
private fun drawMoveLine(location: Location, beforeLocationPoint: TMapPoint) {
70+
val points = arrayListOf(
71+
beforeLocationPoint,
72+
TMapPoint(location.latitude, location.longitude)
73+
)
74+
val polyLine = TMapPolyLine("line$lineNum", points)
75+
lineNum += 1
76+
tMapView.addTMapPolyLine(polyLine)
77+
}
78+
79+
private fun movePin(location: Location) {
80+
val marker = TMapMarkerItem().apply {
81+
id = "marker_person_pin"
82+
icon = ContextCompat.getDrawable(context, R.drawable.ic_person_pin)?.toBitmap()
83+
setTMapPoint(location.latitude, location.longitude)
84+
}
85+
86+
tMapView.removeTMapMarkerItem("marker_person_pin")
87+
tMapView.addTMapMarkerItem(marker)
88+
// tMapView.setLocationPoint(location.latitude, location.longitude)
89+
90+
if (isTracking) {
91+
tMapView.setCenterPoint(location.latitude, location.longitude, true)
92+
}
93+
}
94+
95+
fun moveLocation(longitude: String, latitude: String) {
96+
mockLocation.latitude = latitude.toDouble()
97+
mockLocation.longitude = longitude.toDouble()
98+
99+
val beforeLocationPoint = tMapView.locationPoint
100+
drawMoveLine(mockLocation, beforeLocationPoint)
101+
movePin(mockLocation)
102+
tMapView.setLocationPoint(latitude.toDouble(), longitude.toDouble())
103+
}
104+
105+
companion object {
106+
private const val KOREA_LATITUDE_MIN = 32.814978
107+
private const val KOREA_LATITUDE_MAX = 39.036253
108+
109+
private const val KOREA_LONGITUDE_MIN = 124.661865
110+
private const val KOREA_LONGITUDE_MAX = 132.550049
111+
112+
private const val DEFAULT_ZOOM_LEVEL = 16
113+
private const val RECOGNIZABLE_MINIMUM_DISTANCE_CHANGE = 2.5f
114+
}
115+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.stop.ui.mission
2+
3+
interface TMapHandler {
4+
5+
fun alertTMapReady()
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="75dp"
3+
android:height="75dp"
4+
android:tint="#000000"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24">
7+
<path
8+
android:fillColor="@android:color/white"
9+
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z" />
10+
</vector>

0 commit comments

Comments
ย (0)