본문 바로가기

IT/안드로이드 관련

[안드로이드] AutoCompleteTextView 자동 검색 기능 만들기

안녕하세요 남갯입니다


오늘은 naverApi를 이용하여 Rxjava를 이용하며 autoCompleteTextView를 통한 자동 완성기능을 만드는 방법에 대해 포스팅 해보려고합니다.


naverApi의 영화 검색 api를 이용해서 만들어보려고 합니다.



https://developer.android.com/reference/android/widget/AutoCompleteTextView


공식문서 android 에서는 AutoCompleteTextView 라는것이 있습니다.


자동완성기능을 해주는 녀석인데요?



먼저 검색하는 뷰를 만들어줍니다.


<?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">
<data>
<variable name="viewmodel" type="com.namget.naverapi.ui.search.SearchViewModel"></variable>

</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/searchTitle"
android:textSize="20sp"
android:text="영화검색"
android:textColor="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/searchPlzText"
app:layout_constraintRight_toRightOf="parent"/>

<TextView
android:id="@+id/searchPlzText"
android:textSize="15sp"
android:text="검색어를 입력하세요"
android:textColor="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/searchTitle"
app:layout_constraintRight_toRightOf="parent"/>

<LinearLayout
android:id="@+id/searchMovieName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="@+id/searchButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/searchButton">

<com.google.android.material.textfield.TextInputLayout
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="@+id/autoSearchView"
android:onTextChanged="@{viewmodel::onContentTextChanged}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColorHint="#555555"
android:completionHint="검색어를 입력하세요"
android:completionThreshold="2">
</androidx.appcompat.widget.AppCompatAutoCompleteTextView>

</com.google.android.material.textfield.TextInputLayout>


</LinearLayout>


<Button
android:id="@+id/searchButton"
android:onClick="@{viewmodel::searchClick}"
android:background="@drawable/ripple_custom"
app:layout_constraintTop_toBottomOf="@+id/searchPlzText"
app:layout_constraintLeft_toRightOf="@id/searchMovieName"
app:layout_constraintRight_toRightOf="parent"
android:text="검색"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

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


threshold="2"는 2개이상의 글자가 쳐졌을때 검색 하겠다는 의미입니다.


검색하는 버튼의 레이아웃을 만들어 준 뒤!



class SearchActivity : BaseActivity<ActivitySearchBinding>() {

override val layoutId: Int = R.layout.activity_search
lateinit var searchViewModel: SearchViewModel
lateinit var searchAdapter: SearchAdapter
val searchViewModelFactory: SearchViewModelFactory by inject()
val searchList: ArrayList<String> = arrayListOf()


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
initViewModel()
}

fun initViewModel() {
searchViewModel = ViewModelProviders.of(this, searchViewModelFactory).get(SearchViewModel::class.java)
binding.viewmodel = searchViewModel

searchViewModel.event.observe(this, Observer {
it.getContentIfNotHandled()?.let {
if (it == searchViewModel.SEARCH_CLICK) {
val intent = Intent(this@SearchActivity, MovieListActivity::class.java)
intent.putExtra(Extra.MOVIE_NAME, searchViewModel.titleInputText.value)
startActivity(intent)
}
}
})

searchViewModel.autoText.observe(this, Observer {
setAutoSearchView(it)
})


binding.lifecycleOwner = this
}

fun init() {
searchAdapter = SearchAdapter(this, android.R.layout.simple_dropdown_item_1line, searchList)
autoSearchView.setAdapter(searchAdapter)
autoSearchView.setOnItemClickListener { _, _: View?, p3: Int, _: Long ->
Log.e("test", "" + searchAdapter.getObject(p3))
}


}

fun setAutoSearchView(list: List<String>) {
searchAdapter.setData(list)
searchAdapter.notifyDataSetChanged()
}

}


searchActivity를 구성합니다.


여기서 viewmodel 등등 다른 기능들은 제외하고 


실제로 보셔야할 부분은 


        searchViewModel.autoText.observe(this, Observer {
setAutoSearchView(it)
})


이부분과 autoText를 통해 넘어온 데이터를 받아서 LiveData로 영화리스트 데이터를 받게되면


seAutoSearchView에 리스트를 넘겨주게 됩니다.


 fun init() {
searchAdapter = SearchAdapter(this, android.R.layout.simple_dropdown_item_1line, searchList)
autoSearchView.setAdapter(searchAdapter)
autoSearchView.setOnItemClickListener { _, _: View?, p3: Int, _: Long ->
Log.e("test", "" + searchAdapter.getObject(p3))
}


}

fun setAutoSearchView(list: List<String>) {
searchAdapter.setData(list)
searchAdapter.notifyDataSetChanged()
}


이부분입니다.


그후에 어뎁터를 데이터를 받아 어뎁터의 데이터를 갱신해주는 역할을 하게됩니다.



class SearchAdapter(val mContext: Context, val resource: Int, val list: MutableList<String>) :
ArrayAdapter<String>(mContext, resource, list) , Filterable{

override fun getCount(): Int = list.size
override fun getItem(position: Int): String? = list[position]
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(mContext).inflate(R.layout.item_search_auto_complete, null, false)
val textView: TextView = view.findViewById(R.id.autoCompleteItem)
Log.e("test", "getview : ${list[position]}")
textView.text = list[position]
return view
}
override fun getFilter() = mfilter

private var mfilter: Filter = object : Filter() {

override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
val results = FilterResults()

val query = if (constraint != null && constraint.isNotEmpty()) autocomplete(constraint.toString())
else arrayListOf()

results.values = query
results.count = query.size

return results
}


private fun autocomplete(input: String): MutableList<String> {
val results = arrayListOf<String>()

for (i in list) {
results.add(i)
}

return results
}

override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
if (results.count > 0) notifyDataSetChanged()
else notifyDataSetInvalidated()
}
}


fun getObject(position: Int) = list[position]

fun setData(changedList: List<String>) {
list.clear()
list.addAll(changedList)
}

}


어뎁터는 위와같이 커스텀을 통해 구현하였습니다.


기본 ArrayAdapter에  Filterable 인터페이스를 상속받아 filter를 구현하면


동작순서는


1. 위에서 제가 threshold = 2 로 설정 했으니까 2글자가 넘어갈경우


2. notifydataserChanged가 불리면 getCount를 불러서 다시 리스팅을 진행하게 됩니다.


3. performFiltering , puplishResult가 순차적으로 불리게 됩니다.



class SearchViewModel(val repository: Repository) : BaseViewModel() {

private val _event: MutableLiveData<Event<String>> = MutableLiveData()
val event: LiveData<Event<String>> get() = _event
private val _titleInputText: MutableLiveData<String> = MutableLiveData()
val titleInputText: LiveData<String> get() = _titleInputText

private val _autoText: MutableLiveData<List<String>> = MutableLiveData()
val autoText: LiveData<List<String>> get() = _autoText


val autoTextBehaviorSubject = BehaviorSubject.create<String>()
val SEARCH_CLICK = "SEARCH_CLICK"


init {
addDisposable(
autoTextBehaviorSubject.subscribeOn(Schedulers.newThread())
.throttleLast(3500, TimeUnit.MILLISECONDS)
.concatMap {
Log.e("test", "test ${it}")
repository.getMovieTitleList(it, null).toObservable()
}
.map { it.map { it.title.htmlToString() } }
.subscribe({
Log.e("test", "test ${it}")
_autoText.postValue(it)
}, {
Log.e("test", "test ${it}")
}, {

})
)
}


//내용 textwatcher
fun onContentTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
_titleInputText.value = s.toString()
autoTextBehaviorSubject.onNext(_titleInputText.value ?: "")
}

fun searchClick(view: View) {
_event.value = Event(SEARCH_CLICK)
}


}


텍스트를 입력하게 되면 onContentTextChanged를 통해 텍스트가 넘어오게 되고

1. behaviorSubject를 통해 onNext를 넘겨줍니다.

2 . 여기서 받은 데이터를 throttleLast를 통해 3.5초간 입력이 없을경우 마지막의 데이터만을 넘겨주게 되는데

3. 여기서 받은 데이터를 Single타입으로 만든 retrofit movieList에 넘겨주고 이를 concatMap을 통해 순서를 정해줍니다.

4. 이렇게 받은 데이터는 이전 포스팅에서 만든 htmlToString 이라는 확장함수를 통해 


<b>와 같은 html 스트링을 없애고 제목만을 추출해서 map해줍니다.

5. 이렇게 메인스레드가 아닌 서브스레드를 이용해 받은 데이터는 postValue를 통해 데이터를 주게 되면 이전에 설명했던 autoText.Observe쪽에 타게되고 이전설명의 로직을 타게 됩니다.




완성된 샘플 소스 


https://github.com/namget/myNaverMovieApi


를 통해 확인이 가능합니다


잘보셨다면 github star 부탁드립니다.