Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 Android_김현민 2주차 과제 수정 #72

Open
wants to merge 16 commits into
base: khyeonm
Choose a base branch
from
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
# 맵 위치 검색 안드로이드 앱
- 카테캠 Step2 Week2
- 연락처 추가연락처 조회가 가능한 연락처 목록을 통해 연락처를 관리하는 안드로이드 앱
- 장소 검색장소 저장이 가능한 안드로이드 앱

## 기능
**1. 검색 화면**
- 검색어 입력 칸 + 검색어 전체 삭제 버튼
- 검색 결과 표시 레이아웃
- 검색어 x눌러 삭제 가능

**2. 로컬 데이터베이스 생성**

**3. 검색 결과 목록**
- RecyclerView 사용
- 선택하면 검색어 저장 목록에 추가됨
- 검색창과 검색결과 목록 사이

**4. 검색어 저장 목록**
- 가로 스크롤
- x눌러서 삭제 가능
- 앱을 재실행하여도 유지
- 이미 저장된 목록 선택 시 가장 오른쪽에 위치

## 실행
- MainActivity.kt 에서 시작

## 환경
- Kotlin
- Android Studio
- Android Studio
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.activity:activity:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
55 changes: 55 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/Adapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package campus.tech.kakao.map

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class Adapter(private var profiles: List<Profile>) : RecyclerView.Adapter<Adapter.ProfileViewHolder>() {

interface OnItemClickListener {
fun onItemClick(name: String)
}

var listener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
this.listener = listener
}

inner class ProfileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvName: TextView = itemView.findViewById(R.id.tvName)
val tvAddress: TextView = itemView.findViewById(R.id.tvAddress)
val tvType: TextView = itemView.findViewById(R.id.tvType)

init {
itemView.setOnClickListener {
bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { position ->
listener?.onItemClick(profiles[position].name)
}
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.activity_item_view, parent, false)
return ProfileViewHolder(view)
}

override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
val profile = profiles[position]
holder.tvName.text = profile.name
holder.tvAddress.text = profile.address
holder.tvType.text = profile.type
}

override fun getItemCount(): Int {
return profiles.size
}

fun updateProfiles(newProfiles: List<Profile>) {
profiles = newProfiles
notifyDataSetChanged()
}
}

55 changes: 35 additions & 20 deletions app/src/main/java/campus/tech/kakao/map/DBHelper.kt
Original file line number Diff line number Diff line change
@@ -1,61 +1,76 @@
package campus.tech.kakao.map

import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.Parcel
import android.os.Parcelable

private const val TABLE_PLACE = "places"
private const val COLUMN_TYPE = "type" //
private const val COLUMN_TYPE = "type"
private const val COLUMN_NAME = "name"
private const val COLUMN_ADDRESS = "address"

class DBHelper(context: Context): SQLiteOpenHelper(context, "place.db", null, 1) {
class DBHelper(context: Context) : SQLiteOpenHelper(context, "place.db", null, 1) {
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(
"CREATE TABLE $TABLE_PLACE (" +
"$COLUMN_TYPE varchar(30) NOT NULL," +
"$COLUMN_NAME varchar(30) NOT NULL," +
"$COLUMN_ADDRESS varchar(30) NOT NULL" +
"$COLUMN_TYPE VARCHAR(30) NOT NULL," +
"$COLUMN_NAME VARCHAR(30) NOT NULL," +
"$COLUMN_ADDRESS VARCHAR(30) NOT NULL" +
");"
)

insertPharData()
insertCafeData()
insertPharData(db)
insertCafeData(db)
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS $TABLE_PLACE")
onCreate(db)
}

fun insertPharData() {
val db = this.writableDatabase
private fun insertPharData(db: SQLiteDatabase?) {
val type = "약국"
val name = "약국"
val address = "서울 강남구 대치동"

for (i in 1..30) {
val name = "$name$i"
val address = "$address $i"
db.execSQL("INSERT INTO $TABLE_PLACE ($COLUMN_TYPE, $COLUMN_NAME, $COLUMN_ADDRESS) VALUES ('$type', '$name', '$address');")
val nameWithIndex = "$name$i"
val addressWithIndex = "$address $i"
db?.execSQL("INSERT INTO $TABLE_PLACE ($COLUMN_TYPE, $COLUMN_NAME, $COLUMN_ADDRESS) VALUES ('$type', '$nameWithIndex', '$addressWithIndex');")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db가 null 인지 체크를 미리해서, early return 시키거나 null일때는 이 메소드가 호출안되도록 하는게 좋을것 같습니다. 왜냐면, db가 null 일때도 loop를 30회 수행하고 있을것이라 무의미한 연산이 수행될것 같네요.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 insertCafeData 메소드도 동일합니다.

}
}

fun insertCafeData() {
val db = this.writableDatabase
private fun insertCafeData(db: SQLiteDatabase?) {
val type = "카페"
val name = "카페"
val address = "서울 성동구 성수동"

for (i in 1..30) {
val name = "$name$i"
val address = "$address $i"
db.execSQL("INSERT INTO $TABLE_PLACE ($COLUMN_TYPE, $COLUMN_NAME, $COLUMN_ADDRESS) VALUES ('$type', '$name', '$address');")
val nameWithIndex = "$name$i"
val addressWithIndex = "$address $i"
db?.execSQL("INSERT INTO $TABLE_PLACE ($COLUMN_TYPE, $COLUMN_NAME, $COLUMN_ADDRESS) VALUES ('$type', '$nameWithIndex', '$addressWithIndex');")
}
}

fun searchProfiles(query: String): List<Profile> {
val profiles = mutableListOf<Profile>()
val db = this.readableDatabase
val cursor: Cursor = db.rawQuery(
"SELECT * FROM $TABLE_PLACE WHERE $COLUMN_NAME LIKE ? OR $COLUMN_ADDRESS LIKE ? OR $COLUMN_TYPE LIKE ?",
arrayOf("%$query%", "%$query%", "%$query%")
)

if (cursor.moveToFirst()) {
do {
val type = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TYPE))
val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME))
val address = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ADDRESS))
profiles.add(Profile(name, address, type))
} while (cursor.moveToNext())
}
cursor.close()
return profiles
}
}

151 changes: 148 additions & 3 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,160 @@
package campus.tech.kakao.map

import android.database.Cursor
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.HorizontalScrollView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.json.JSONArray

class MainActivity : AppCompatActivity() {
lateinit var adapter: Adapter
lateinit var dbHelper: DBHelper
lateinit var tvNoResult: TextView
lateinit var llSave: LinearLayoutCompat
lateinit var hScrollView: HorizontalScrollView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val dbHelper = DBHelper(this)
val db = dbHelper.writableDatabase
dbHelper = DBHelper(this)

val etSearch = findViewById<EditText>(R.id.etSearch)
tvNoResult = findViewById(R.id.tvNoResult)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
val btnClose = findViewById<Button>(R.id.btnClose)
llSave = findViewById(R.id.llSave)
hScrollView = findViewById(R.id.hScrollView)

adapter = Adapter(emptyList())
recyclerView.adapter = adapter

tvNoResult.visibility = TextView.VISIBLE

etSearch.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val search = s.toString()
searchProfiles(search)
}
})

adapter.setOnItemClickListener(object : Adapter.OnItemClickListener {
override fun onItemClick(name: String) {
if (isProfileInSearchSave(name)) {
removeSavedItem(name)
}
addSavedItem(name)
}
})

btnClose.setOnClickListener {
etSearch.text.clear()
}
loadSavedItems()
}

override fun onPause() {
super.onPause()
saveSavedItems()
}

fun saveSavedItems() {
val sharedPreferences = getSharedPreferences("SavedItems", MODE_PRIVATE)
val editor = sharedPreferences.edit()
val savedNames = JSONArray()
for (i in 0 until llSave.childCount) {
val savedView = llSave.getChildAt(i) as? ConstraintLayout
val tvSaveName = savedView?.findViewById<TextView>(R.id.tvSaveName)
if (tvSaveName != null) {
savedNames.put(tvSaveName.text.toString())
}
}
editor.putString("savedNames", savedNames.toString())
editor.apply()
}

fun loadSavedItems() {
val sharedPreferences = getSharedPreferences("SavedItems", MODE_PRIVATE)
val savedNamesString = sharedPreferences.getString("savedNames", "[]")
val savedNames = JSONArray(savedNamesString)
for (i in 0 until savedNames.length()) {
val name = savedNames.getString(i)
addSavedItem(name)
}
}

fun searchProfiles(query: String) {
if (query.isEmpty()) {
tvNoResult.visibility = TextView.VISIBLE
adapter.updateProfiles(emptyList())
} else {
val profileList = dbHelper.searchProfiles(query)
if (profileList.isEmpty()) {
tvNoResult.visibility = TextView.VISIBLE
} else {
tvNoResult.visibility = TextView.GONE
}
adapter.updateProfiles(profileList)
}
}

fun addSavedItem(name: String) {
val savedView = LayoutInflater.from(this)
.inflate(R.layout.search_save, llSave, false) as ConstraintLayout

val tvSaveName = savedView.findViewById<TextView>(R.id.tvSaveName)
val ivDelete = savedView.findViewById<ImageView>(R.id.ivDelete)

tvSaveName.text = name
ivDelete.setOnClickListener {
llSave.removeView(savedView)
}

llSave.addView(savedView)
hScrollView.visibility = View.VISIBLE
scrollToEndOfSearchSave()
}

fun removeSavedItem(name: String) {
for (i in 0 until llSave.childCount) {
val savedView = llSave.getChildAt(i) as? ConstraintLayout
val tvSaveName = savedView?.findViewById<TextView>(R.id.tvSaveName)
if (tvSaveName?.text.toString() == name) {
llSave.removeViewAt(i)
break
}
}
}

fun isProfileInSearchSave(name: String): Boolean {
for (i in 0 until llSave.childCount) {
val savedView = llSave.getChildAt(i) as? ConstraintLayout
val tvSaveName = savedView?.findViewById<TextView>(R.id.tvSaveName)
if (tvSaveName?.text.toString() == name) {
return true
}
}
return false
}

fun scrollToEndOfSearchSave() {
hScrollView.post {
hScrollView.fullScroll(View.FOCUS_RIGHT)
}
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/Profile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package campus.tech.kakao.map

data class Profile (
val name: String,
val address: String,
val type: String
)
Binary file added app/src/main/res/drawable/location.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading