와챠의 우당탕탕 개발 기록장
[안드로이드] 사용자 위치 가져와서 날씨 정보 설정하기 본문
https://min-wachya.tistory.com/163
[안드로이드] 최신 기상청 단기예보 API 활용하기(초단기예보, Json)
올해 1학기 때 과제로 기상청 동네 예보 API를 사용한 적이 있었는데... 7월 초에 이런 메일이 왔다. 동네 예보 API가 종료된다고... 물론 단기 예보 단위가 상세화 되는 건 좋다~ 기존에 있던 코드
min-wachya.tistory.com
이전 게시글에 이어서 이번엔 사용자의 현재 위치를 가져오는 것을 추가해봤다.
여기에 생략한 내용들 다 위에 있음
위치 가져올 때 권한 설정하는 것도 필요해서 스플래시도 넣었다.
스플래시 화면에서 권한 설정하게 하고 권한 얻었을 때만 메인 액티비티로 넘어가는 식이다.
api 불러올 때 위치 인자만 바뀐 거라 크게 눈에띄게 달라진 점은 없다.

build.gradle 설정
내 위치 찾기할 때 권한 필요해서build.gradle에
권한 설정 하기 위한 라이브러리 추가
implementation "io.github.ParkSangGwon:tedpermission:2.3.0"
AndroidManifest 설정
위치 가져오기 위한 라이브러리 추가
implementation 'com.google.android.gms:play-services-location:18.0.0'
대략적인 위치, 상세 위치 얻기 위한 권한 추가
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
실시간 위치 정보 엑세스 권한 추가
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Splash.kt : 스플래시 화면 및 권한 묻기
아래 포스트 참고
https://min-wachya.tistory.com/144
[안드로이드] 스플래시 1초간 보여주기
1, 스플래시 액티비티(Splash.kt) 생성 2, AndroidMAnifest.xml에 등록 스플래시 액티비티 제일 먼저 방문하기위해 android:name=".Splash" android:theme="@style/SplashTheme"> 로 수정 및 추가 추가 3, res > v..
min-wachya.tistory.com
package com.example.myweathertest2 | |
import android.app.AlertDialog | |
import android.content.ActivityNotFoundException | |
import android.content.Intent | |
import android.net.Uri | |
import java.util.concurrent.TimeUnit | |
import android.os.Bundle | |
import android.provider.Settings | |
import android.widget.Toast | |
import androidx.appcompat.app.AppCompatActivity | |
import androidx.core.content.ContextCompat | |
import com.gun0912.tedpermission.PermissionListener | |
import com.gun0912.tedpermission.TedPermission | |
import java.util.concurrent.Executor | |
import java.util.concurrent.Executors | |
import java.util.concurrent.ScheduledExecutorService | |
// 스플래시 | |
class Splash : AppCompatActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
// 권한 설정하기 | |
setPermission() | |
} | |
// tedpermission 권한 설정 | |
private fun setPermission() { | |
// 권한 묻는 팝업 만들기 | |
val permissionListener = object : PermissionListener { | |
// 설정해놓은 권한을 허용됐을 때 | |
override fun onPermissionGranted() { | |
// 1초간 스플래시 보여주기 | |
val backgroundExecutable : ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() | |
val mainExecutor : Executor = ContextCompat.getMainExecutor(this@Splash) | |
backgroundExecutable.schedule({ | |
mainExecutor.execute { | |
// MainActivity 넘어가기 | |
var intent = Intent(this@Splash, MainActivity::class.java) | |
startActivity(intent) | |
finish() | |
} | |
}, 1, TimeUnit.SECONDS) | |
Toast.makeText(this@Splash, "권한 허용", Toast.LENGTH_SHORT).show() | |
} | |
// 설정해놓은 권한을 거부됐을 때 | |
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) { | |
// 권한 없어서 요청 | |
AlertDialog.Builder(this@Splash) | |
.setMessage("권한 거절로 인해 일부 기능이 제한됩니다.") | |
.setPositiveButton("권한 설정하러 가기") { dialog, which -> | |
try { | |
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) | |
.setData(Uri.parse("package:com.example.myweathertest2")) | |
startActivity(intent) | |
} catch (e : ActivityNotFoundException) { | |
e.printStackTrace() | |
val intent = Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS) | |
startActivity(intent) | |
} | |
} | |
.show() | |
Toast.makeText(this@Splash, "권한 거부", Toast.LENGTH_SHORT).show() | |
} | |
} | |
// 권한 설정 | |
TedPermission.with(this@Splash) | |
.setPermissionListener(permissionListener) | |
.setRationaleMessage("정확한 날씨 정보를 위해 권한을 허용해주세요.") | |
.setDeniedMessage("권한을 거부하셨습니다. [앱 설정]->[권한] 항목에서 허용해주세요.") | |
.setPermissions(android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION) // 필수 권한만 문기 | |
.check() | |
} | |
} |
MainActivity.kt : 현재 사용자의 위치를 위경도로 가져와서 격자 좌표로 변환한 후 날씨 정보 불러오기
package com.example.myweathertest2 | |
import android.graphics.Point | |
import androidx.appcompat.app.AppCompatActivity | |
import android.os.Bundle | |
import android.os.Looper | |
import android.util.Log | |
import android.view.View | |
import android.widget.Button | |
import android.widget.TextView | |
import android.widget.Toast | |
import androidx.recyclerview.widget.LinearLayoutManager | |
import androidx.recyclerview.widget.RecyclerView | |
import com.example.myweathertest2.Adapter.WeatherAdapter | |
import com.example.myweathertest2.Model.ModelWeather | |
import com.google.android.gms.location.LocationCallback | |
import com.google.android.gms.location.LocationRequest | |
import com.google.android.gms.location.LocationResult | |
import com.google.android.gms.location.LocationServices | |
import retrofit2.Call | |
import retrofit2.Response | |
import java.text.SimpleDateFormat | |
import java.util.* | |
// 메인 액티비티 | |
class MainActivity : AppCompatActivity() { | |
lateinit var weatherRecyclerView : RecyclerView | |
lateinit var tvDate : TextView | |
lateinit var base_date : String // 발표 일자 | |
lateinit var base_time : String // 발표 시각 | |
private var curPoint : Point? = null // 현재 위치의 격자 좌표를 저장할 포인트 | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
tvDate = findViewById(R.id.tvDate) // 오늘 날짜 텍스트뷰 | |
weatherRecyclerView = findViewById(R.id.weatherRecyclerView) // 날씨 리사이클러 뷰 | |
val btnRefresh = findViewById<Button>(R.id.btnRefresh) // 새로고침 버튼 | |
// 리사이클러 뷰 매니저 설정 | |
weatherRecyclerView.layoutManager = LinearLayoutManager(this@MainActivity) | |
// 내 위치 위경도 가져와서 날씨 정보 설정하기 | |
requestLocation() | |
// <새로고침> 버튼 누를 때 위치 정보 & 날씨 정보 다시 가져오기 | |
btnRefresh.setOnClickListener { | |
requestLocation() | |
} | |
} | |
// 날씨 가져와서 설정하기 | |
private fun setWeather(nx : Int, ny : Int) { | |
// 준비 단계 : base_date(발표 일자), base_time(발표 시각) | |
// 현재 날짜, 시간 정보 가져오기 | |
val cal = Calendar.getInstance() | |
base_date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time) // 현재 날짜 | |
val timeH = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time) // 현재 시각 | |
val timeM = SimpleDateFormat("HH", Locale.getDefault()).format(cal.time) // 현재 분 | |
// API 가져오기 적당하게 변환 | |
base_time = Common().getBaseTime(timeH, timeM) | |
// 현재 시각이 00시이고 45분 이하여서 baseTime이 2330이면 어제 정보 받아오기 | |
if (timeH == "00" && base_time == "2330") { | |
cal.add(Calendar.DATE, -1).toString() | |
base_date = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(cal.time) | |
} | |
// 날씨 정보 가져오기 | |
// (한 페이지 결과 수 = 60, 페이지 번호 = 1, 응답 자료 형식-"JSON", 발표 날싸, 발표 시각, 예보지점 좌표) | |
val call = ApiObject.retrofitService.GetWeather(60, 1, "JSON", base_date, base_time, nx, ny) | |
// 비동기적으로 실행하기 | |
call.enqueue(object : retrofit2.Callback<WEATHER> { | |
// 응답 성공 시 | |
override fun onResponse(call: Call<WEATHER>, response: Response<WEATHER>) { | |
if (response.isSuccessful) { | |
// 날씨 정보 가져오기 | |
val it: List<ITEM> = response.body()!!.response.body.items.item | |
// 현재 시각부터 1시간 뒤의 날씨 6개를 담을 배열 | |
val weatherArr = arrayOf(ModelWeather(), ModelWeather(), ModelWeather(), ModelWeather(), ModelWeather(), ModelWeather()) | |
// 배열 채우기 | |
var index = 0 | |
val totalCount = response.body()!!.response.body.totalCount - 1 | |
for (i in 0..totalCount) { | |
index %= 6 | |
when(it[i].category) { | |
"PTY" -> weatherArr[index].rainType = it[i].fcstValue // 강수 형태 | |
"REH" -> weatherArr[index].humidity = it[i].fcstValue // 습도 | |
"SKY" -> weatherArr[index].sky = it[i].fcstValue // 하늘 상태 | |
"T1H" -> weatherArr[index].temp = it[i].fcstValue // 기온 | |
else -> continue | |
} | |
index++ | |
} | |
// 각 날짜 배열 시간 설정 | |
for (i in 0..5) weatherArr[i].fcstTime = it[i].fcstTime | |
// 리사이클러 뷰에 데이터 연결 | |
weatherRecyclerView.adapter = WeatherAdapter(weatherArr) | |
// 토스트 띄우기 | |
Toast.makeText(applicationContext, it[0].fcstDate + ", " + it[0].fcstTime + "의 날씨 정보입니다.", Toast.LENGTH_SHORT).show() | |
} | |
} | |
// 응답 실패 시 | |
override fun onFailure(call: Call<WEATHER>, t: Throwable) { | |
val tvError = findViewById<TextView>(R.id.tvError) | |
tvError.text = "api fail : " + t.message.toString() + "\n 다시 시도해주세요." | |
tvError.visibility = View.VISIBLE | |
Log.d("api fail", t.message.toString()) | |
} | |
}) | |
} | |
// 내 현재 위치의 위경도를 격자 좌표로 변환하여 해당 위치의 날씨정보 설정하기 | |
private fun requestLocation() { | |
val locationClient = LocationServices.getFusedLocationProviderClient(this@MainActivity) | |
try { | |
// 나의 현재 위치 요청 | |
val locationRequest = LocationRequest.create() | |
locationRequest.run { | |
priority = LocationRequest.PRIORITY_HIGH_ACCURACY | |
interval = 60 * 1000 // 요청 간격(1초) | |
} | |
val locationCallback = object : LocationCallback() { | |
// 요청 결과 | |
override fun onLocationResult(p0: LocationResult?) { | |
p0?.let { | |
for (location in it.locations) { | |
// 현재 위치의 위경도를 격자 좌표로 변환 | |
curPoint = Common().dfs_xy_conv(location.latitude, location.longitude) | |
// 오늘 날짜 텍스트뷰 설정 | |
tvDate.text = SimpleDateFormat("MM월 dd일", Locale.getDefault()).format(Calendar.getInstance().time) + "날씨" | |
// nx, ny지점의 날씨 가져와서 설정하기 | |
setWeather(curPoint!!.x, curPoint!!.y) | |
} | |
} | |
} | |
} | |
// 내 위치 실시간으로 감지 | |
locationClient?.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper()) | |
} catch (e : SecurityException) { | |
e.printStackTrace() | |
} | |
} | |
} |
Common.kt : baseTime 생성하는 함수, 위경도를 격자 좌표로 변환하는 함수가 공통 함수여서 여기 넣어둠
package com.example.myweathertest2 | |
import android.graphics.Point | |
class Common { | |
// baseTime 설정하기 | |
fun getBaseTime(h : String, m : String) : String { | |
var result = "" | |
// 45분 전이면 | |
if (m.toInt() < 45) { | |
// 0시면 2330 | |
if (h == "00") result = "2330" | |
// 아니면 1시간 전 날씨 정보 부르기 | |
else { | |
var resultH = h.toInt() - 1 | |
// 1자리면 0 붙여서 2자리로 만들기 | |
if (resultH < 10) result = "0" + resultH + "30" | |
// 2자리면 그대로 | |
else result = resultH.toString() + "30" | |
} | |
} | |
// 45분 이후면 바로 정보 받아오기 | |
else result = h + "30" | |
return result | |
} | |
// 위경도를 기상청에서 사용하는 격자 좌표로 변환 | |
fun dfs_xy_conv(v1: Double, v2: Double) : Point { | |
val RE = 6371.00877 // 지구 반경(km) | |
val GRID = 5.0 // 격자 간격(km) | |
val SLAT1 = 30.0 // 투영 위도1(degree) | |
val SLAT2 = 60.0 // 투영 위도2(degree) | |
val OLON = 126.0 // 기준점 경도(degree) | |
val OLAT = 38.0 // 기준점 위도(degree) | |
val XO = 43 // 기준점 X좌표(GRID) | |
val YO = 136 // 기준점 Y좌표(GRID) | |
val DEGRAD = Math.PI / 180.0 | |
val re = RE / GRID | |
val slat1 = SLAT1 * DEGRAD | |
val slat2 = SLAT2 * DEGRAD | |
val olon = OLON * DEGRAD | |
val olat = OLAT * DEGRAD | |
var sn = Math.tan(Math.PI * 0.25 + slat2 * 0.5) / Math.tan(Math.PI * 0.25 + slat1 * 0.5) | |
sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(sn) | |
var sf = Math.tan(Math.PI * 0.25 + slat1 * 0.5) | |
sf = Math.pow(sf, sn) * Math.cos(slat1) / sn | |
var ro = Math.tan(Math.PI * 0.25 + olat * 0.5) | |
ro = re * sf / Math.pow(ro, sn) | |
var ra = Math.tan(Math.PI * 0.25 + (v1) * DEGRAD * 0.5) | |
ra = re * sf / Math.pow(ra, sn) | |
var theta = v2 * DEGRAD - olon | |
if (theta > Math.PI) theta -= 2.0 * Math.PI | |
if (theta < -Math.PI) theta += 2.0 * Math.PI | |
theta *= sn | |
val x = (ra * Math.sin(theta) + XO + 0.5).toInt() | |
val y = (ro - ra * Math.cos(theta) + YO + 0.5).toInt() | |
return Point(x, y) | |
} | |
} |
다음에 해볼 것
사용자 현재 위치 가져와서 위경도 값을 직접 좌표 값으로 변환- 위젯 만들기
- 가로로 슬라이드하는 리사이클러뷰(?) 만들어보기
'코딩 일기장 > Android(Kotlin)' 카테고리의 다른 글
[안드로이드] 메일 보내기 (4) | 2021.08.13 |
---|---|
[안드로이드] 가로 슬라이드 리사이클러뷰 (0) | 2021.07.31 |
[안드로이드] 최신 기상청 단기예보 API 활용하기(초단기예보, Json) (26) | 2021.07.30 |
[안드로이드] 갤러리에서 사진 가져오고 크롭하기(크기 조절하기) (6) | 2021.06.24 |
[안드로이드] 한국관광공사 Tour API 활용하기(지역기반 관광정보조회) - XML 데이터 가져오기 (0) | 2021.06.23 |