Skip to content

Commit

Permalink
Merge pull request #181 from LJG7123/feature-aos/주소-검색
Browse files Browse the repository at this point in the history
Feature(#34): 주소 검색 및 카메라 이동
  • Loading branch information
LJG7123 authored Nov 29, 2023
2 parents cddce7e + be80d67 commit 53dccf5
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 2 deletions.
10 changes: 10 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
alias(libs.plugins.androidApplication)
Expand Down Expand Up @@ -25,6 +27,7 @@ android {
versionName = "0.1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "apiKey", getApiKey("MAPS_API_KEY"))
}
signingConfigs {
create("release") {
Expand Down Expand Up @@ -53,13 +56,18 @@ android {
buildFeatures{
buildConfig = true
dataBinding = true
buildConfig = true
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.majorVersion
}
}

fun getApiKey(propertyKey: String) : String {
return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
}

dependencies {

implementation(libs.core.ktx)
Expand Down Expand Up @@ -107,6 +115,8 @@ dependencies {
implementation (libs.play.services.maps)
//location
implementation (libs.play.services.location)
//places
implementation (libs.places)

//mockwebserver
testImplementation(libs.mockwebserver)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.boostcampwm2023.snappoint.presentation.main

import android.Manifest
import android.annotation.SuppressLint
import android.location.Geocoder
import android.os.Build
import android.os.Bundle
import android.os.Looper
import android.util.Log
Expand All @@ -23,12 +25,14 @@ import com.boostcampwm2023.snappoint.presentation.base.BaseActivity
import com.boostcampwm2023.snappoint.presentation.model.PostBlockState
import com.boostcampwm2023.snappoint.presentation.model.SnapPointTag
import com.boostcampwm2023.snappoint.presentation.util.Constants
import com.boostcampwm2023.snappoint.presentation.util.Constants.API_KEY
import com.boostcampwm2023.snappoint.presentation.util.PermissionUtil.LOCATION_PERMISSION_REQUEST_CODE
import com.boostcampwm2023.snappoint.presentation.util.PermissionUtil.isMyLocationGranted
import com.boostcampwm2023.snappoint.presentation.util.PermissionUtil.isPermissionGranted
import com.boostcampwm2023.snappoint.presentation.util.PermissionUtil.locationPermissionRequest
import com.boostcampwm2023.snappoint.presentation.util.addImageMarker
import com.boostcampwm2023.snappoint.presentation.util.pxFloat
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
Expand All @@ -45,11 +49,18 @@ import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.PolylineOptions
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest
import com.google.android.libraries.places.api.net.PlacesClient
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.search.SearchView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

@AndroidEntryPoint
class MainActivity :
Expand All @@ -59,6 +70,9 @@ class MainActivity :
{
private val viewModel: MainViewModel by viewModels()
private var googleMap: GoogleMap? = null
private lateinit var placesClient: PlacesClient
private val token = AutocompleteSessionToken.newInstance()
private val geocoder by lazy { Geocoder(applicationContext) }

private val navController: NavController by lazy {
(supportFragmentManager.findFragmentById(R.id.fcv) as NavHostFragment).findNavController()
Expand All @@ -74,6 +88,8 @@ class MainActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initPlacesClient()

initBinding()

initBottomSheetWithNavigation()
Expand All @@ -87,13 +103,17 @@ class MainActivity :
initLocationData()
}

private fun initPlacesClient() {
Places.initializeWithNewPlacesApiEnabled(applicationContext, API_KEY)
placesClient = Places.createClient(this)
}

private fun initLocationData() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

locationCallback = object : LocationCallback() {
override fun onLocationResult(p0: LocationResult) {
super.onLocationResult(p0)
Log.d("TAG", "onLocationResult: ${p0}")
}
}

Expand Down Expand Up @@ -146,6 +166,11 @@ class MainActivity :
}
moveCameraToFitScreen()
}

is MainActivityEvent.MoveCameraToAddress -> {
val address = viewModel.searchViewUiState.value.texts[event.index]
moveCameraToAddress(address)
}
}
}
}
Expand Down Expand Up @@ -303,9 +328,70 @@ class MainActivity :
fab.setOnClickListener {
checkPermissionAndMoveCameraToUserLocation()
}

sv.editText.setOnEditorActionListener { v, _, _ ->
getAddressAutoCompletion(v.text.toString())
true
}

sv.addTransitionListener { _, _, afterState ->
if (afterState == SearchView.TransitionState.HIDDEN) {
viewModel.updateAutoCompleteTexts(emptyList())
}
}
}
}

private fun moveCameraToAddress(address: String) {

with(binding) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder.getFromLocationName(address, 1) { results ->
if (results.size == 0) {
runOnUiThread { showToastMessage(R.string.search_location_fail) }
} else {
val latLng = LatLng(results[0].latitude, results[0].longitude)
runOnUiThread {
googleMap?.moveCamera(CameraUpdateFactory.newLatLng(latLng))
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
sv.hide()
}
}
}
} else {
val results = runBlocking(Dispatchers.IO) { geocoder.getFromLocationName(address, 1) }

if (results == null || results.size == 0) {
showToastMessage(R.string.search_location_fail)
} else {
val latLng = LatLng(results[0].latitude, results[0].longitude)
googleMap?.moveCamera(CameraUpdateFactory.newLatLng(latLng))
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
sv.hide()
}
}
}
}

private fun getAddressAutoCompletion(query: String) {
val request = FindAutocompletePredictionsRequest.builder()
.setSessionToken(token)
.setQuery(query)
.build()

placesClient.findAutocompletePredictions(request)
.addOnSuccessListener { response ->
viewModel.updateAutoCompleteTexts(response.autocompletePredictions.map {
it.getFullText(null).toString()
})
}.addOnFailureListener { exception ->
if (exception is ApiException) {
Log.e("TAG", "Place not found: ${exception.statusCode}")
}
}
}

override fun onMapReady(googleMap: GoogleMap) {
this.googleMap = googleMap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ sealed class MainActivityEvent {
data object NavigatePrev: MainActivityEvent()
data object NavigateClose: MainActivityEvent()
data class NavigatePreview(val index: Int): MainActivityEvent()
data class MoveCameraToAddress(val index: Int): MainActivityEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.boostcampwm2023.snappoint.presentation.main

import androidx.lifecycle.ViewModel
import com.boostcampwm2023.snappoint.data.repository.PostRepository
import com.boostcampwm2023.snappoint.presentation.main.search.SearchViewUiState
import com.boostcampwm2023.snappoint.presentation.model.PositionState
import com.boostcampwm2023.snappoint.presentation.model.PostBlockState
import com.boostcampwm2023.snappoint.presentation.model.PostSummaryState
Expand All @@ -27,6 +28,13 @@ class MainViewModel @Inject constructor(
private val _uiState: MutableStateFlow<MainUiState> = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()

private val _searchViewUiState: MutableStateFlow<SearchViewUiState> = MutableStateFlow(
SearchViewUiState(onAutoCompleteItemClicked = { index ->
moveCameraToAddress(index)
})
)
val searchViewUiState: StateFlow<SearchViewUiState> = _searchViewUiState.asStateFlow()

private val _event: MutableSharedFlow<MainActivityEvent> = MutableSharedFlow(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
Expand Down Expand Up @@ -199,4 +207,14 @@ class MainViewModel @Inject constructor(
focusedIndex = snapPointIndex)
}
}

fun updateAutoCompleteTexts(texts: List<String>) {
_searchViewUiState.update {
it.copy(texts = texts)
}
}

private fun moveCameraToAddress(index: Int) {
_event.tryEmit(MainActivityEvent.MoveCameraToAddress(index))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.boostcampwm2023.snappoint.presentation.main.search

import androidx.recyclerview.widget.RecyclerView
import com.boostcampwm2023.snappoint.databinding.ItemSearchAutoCompleteBinding

class AutoCompletionViewHolder(
private val binding: ItemSearchAutoCompleteBinding,
private val onAutoCompleteItemClicked: (Int) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {

fun bind(string: String, index: Int) {

with(binding) {
item = string
root.setOnClickListener { onAutoCompleteItemClicked(index) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.boostcampwm2023.snappoint.presentation.main.search

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.boostcampwm2023.snappoint.databinding.ItemSearchAutoCompleteBinding

class AutoCompletionListAdapter(
private val onAutoCompleteItemClicked: (Int) -> Unit
) : ListAdapter<String, AutoCompletionViewHolder>(diffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoCompletionViewHolder {
val inflater = LayoutInflater.from(parent.context)
return AutoCompletionViewHolder(
binding = ItemSearchAutoCompleteBinding.inflate(inflater, parent, false),
onAutoCompleteItemClicked = onAutoCompleteItemClicked
)
}

override fun onBindViewHolder(holder: AutoCompletionViewHolder, position: Int) {
holder.bind(getItem(position), position)
}

companion object {
val diffUtil = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}

override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}

}
}
}

@BindingAdapter("autoCompleteTexts", "onAutoCompleteItemClick")
fun RecyclerView.bindRecyclerViewAdapter(texts: List<String>, onAutoCompleteItemClicked:(Int) -> Unit) {
if (adapter == null) adapter = AutoCompletionListAdapter(onAutoCompleteItemClicked)
(adapter as AutoCompletionListAdapter).submitList(texts)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.boostcampwm2023.snappoint.presentation.main.search

data class SearchViewUiState(
val texts: List<String> = emptyList(),
val onAutoCompleteItemClicked: (Int) -> Unit = {},
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.boostcampwm2023.snappoint.presentation.util

import com.boostcampwm2023.snappoint.BuildConfig

object Constants {
const val BOTTOM_SHEET_HALF_EXPANDED_RATIO: Float = 0.45f
const val API_KEY = BuildConfig.MAPS_API_KEY
}
12 changes: 11 additions & 1 deletion android/app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_anchor="@id/sb"
android:id="@+id/sv"/>
android:id="@+id/sv">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rcv_search_auto_complete"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
autoCompleteTexts="@{vm.searchViewUiState.texts}"
onAutoCompleteItemClick="@{vm.searchViewUiState.onAutoCompleteItemClicked}"/>

</com.google.android.material.search.SearchView>

<LinearLayout
android:id="@+id/bs"
Expand Down
43 changes: 43 additions & 0 deletions android/app/src/main/res/layout/item_search_auto_complete.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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>

<variable
name="item"
type="String" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:padding="16dp">

<ImageView
android:id="@+id/iv_marker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon_location_pin"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

<TextView
android:id="@+id/tv_auto_complete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item}"
android:textSize="16sp"
android:ellipsize="marquee"
android:maxLines="1"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/iv_marker"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_marker"
app:layout_constraintBottom_toBottomOf="@id/iv_marker"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Loading

0 comments on commit 53dccf5

Please sign in to comment.