1. google place api를 사용하여 주소 자동 완성 기능을 구현하고자한다.
2. 주소 자동 완성 기능은 아래 링크와 같이, 위젯을 띄워서 검색해오는 방법이 있다.
https://developers.google.com/places/?hl=ko
3. 필자는 위젯이 아니라, 커스텀한 화면이 필요했고 구현을 해야했다.
4. 사용하기 앞서, 24시간에 1,000건을 쿼리해 올 수 있도록 제공한다. 그 이상은 유료로 전환해야하며, 카드 결제 정보를 등록시, 무료로 150,000건을 무료로 사용이 가능하다. 그리고 그 후에 건수가 초과되면 초과한 만큼 금액이 카드에서 자동으로 빠져나간다. (2018. 6. 01. 기준)
5. 우선 아래 링크로 이동 후, 사용 가능한 키를 가져온다.
https://developers.google.com/places/android-api/?hl=ko
6. 프로젝트 AndroidManifest.xml으로 가서, 아래와 같이 추가한다.
<application
android:name=".Application"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:theme">
........
<!-- google place api -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="위에서 발급한 키" />
<!-- google service -->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
......
</application>
7. 프로젝트 Gradle (Module : app) 에 api를 사용하기 위해 추가하고, sync now를 해준다.
dependencies {
....
// google place api
implementation 'com.google.android.gms:play-services-places:15.0.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
}
8. Google api console 로 이동해서, 사용할 프로젝트를 선택 후, Places SDK for Android를 사용 설정해준다.
9. 이제 사용할 준비는 다 되었고, 뷰를 만들어야한다.
10. 검색 화면이 있는 뷰를 만든다.
activity_search.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentTop="true"
android:background="@android:color/black"
android:gravity="center"
android:orientation="horizontal">
<EditText
android:id="@+id/ed_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="10dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:layout_weight="1"
android:background="@null"
android:hint="검색어를 입력해주세요."
android:textColor="@android:color/white"
android:textColorHint="@android:color/white"
android:textSize="17dp" />
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/list_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:visibility="visible" />
</LinearLayout>
11. 검색어 입력시, 하단에 검색어들이 나타날 자식 뷰들을 만든다.
item_search.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/predictedRow"
android:layout_width="match_parent"
android:layout_height="65dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@android:color/white"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_weight="1"
android:text="주소 이름"
android:gravity="center_vertical"
android:textColor="#414345"
android:textSize="15sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_alignParentBottom="true"
android:background="#919fa9" />
</RelativeLayout>
12. 검색 결과를 저장할 객체를 만든다.
SearchKeywordBean.class
public class SearchKeywordBean {
String Latitude, Longitude;
public String getLatitude() {
return Latitude;
}
public void setLatitude(String latitude) {
Latitude = latitude;
}
public String getLongitude() {
return Longitude;
}
public void setLongitude(String longitude) {
Longitude = longitude;
}
}
13. 주소 검색시 아래에 나타나는 검색어를 표시해주는 어뎁터 클래스를 만든다.
PlaceAutocompleteAdapter class
import android.content.Context;
import android.graphics.Typeface;
import android.support.v7.widget.RecyclerView;
import android.text.style.CharacterStyle;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.places.AutocompleteFilter;
import com.google.android.gms.location.places.AutocompletePrediction;
import com.google.android.gms.location.places.AutocompletePredictionBuffer;
import com.google.android.gms.location.places.Places;
import com.google.android.gms.maps.model.LatLngBounds;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/**
* 주소 자동검색 어뎁터
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 4:59
**/
public class PlaceAutocompleteAdapter extends RecyclerView.Adapter<PlaceAutocompleteAdapter.PlaceViewHolder> implements Filterable {
public interface PlaceAutoCompleteInterface {
public void onPlaceClick(ArrayList<PlaceAutocomplete> mResultList, int position);
}
Context context;
PlaceAutoCompleteInterface listener;
private static final String TAG = "PlaceAutocompleteAdapter";
private static final CharacterStyle STYLE_BOLD = new StyleSpan(Typeface.BOLD);
ArrayList<PlaceAutocomplete> resultList;
private GoogleApiClient googleApiClient;
private LatLngBounds bounds;
private int layout;
private AutocompleteFilter placeFilter;
public PlaceAutocompleteAdapter(Context context, int resource, GoogleApiClient googleApiClient,
LatLngBounds bounds, AutocompleteFilter filter) {
this.context = context;
this.layout = resource;
this.googleApiClient = googleApiClient;
this.bounds = bounds;
this.placeFilter = filter;
this.listener = (PlaceAutoCompleteInterface) this.context;
}
/**
* 아이템 모두 clear
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 4:59
**/
public void clearList() {
if (resultList != null && resultList.size() > 0) {
resultList.clear();
}
}
/**
* 모든 후속 쿼리에 대한 경계를 설정합니다.
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 5:00
**/
public void setBounds(LatLngBounds bounds) {
this.bounds = bounds;
}
@Override
public Filter getFilter() {
Filter filter = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
// 제약 조건이 주어지지 않으면 자동 완성 쿼리를 건너 뜁니다.
if (constraint != null) {
//(제약) 검색 문자열에 대한 자동 완성 API를 쿼리하십시오.
resultList = getAutocomplete(constraint);
if (resultList != null) {
//API가 성공적으로 결과를 반환했습니다.
results.values = resultList;
results.count = resultList.size();
}
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
//API가 하나 이상의 결과를 반환하고 데이터를 업데이트합니다.
notifyDataSetChanged();
} else {
//API가 결과를 반환하지 않았고 데이터 세트를 무효화
//notifyDataSetInvalidated();
}
}
};
return filter;
}
private ArrayList<PlaceAutocomplete> getAutocomplete(CharSequence constraint) {
if (googleApiClient.isConnected()) {
// 자동 완성 API에 쿼리를 제출하고 PendingResult를 검색합니다.
// 쿼리가 완료되면 결과를 포함합니다.
PendingResult<AutocompletePredictionBuffer> results =
Places.GeoDataApi
.getAutocompletePredictions(googleApiClient, constraint.toString(),
bounds, placeFilter);
// 이 메소드는 기본 UI 스레드에서 호출되어야합니다. API의 결과를 차단하고 최대 60 초 동안 기다립니다.
AutocompletePredictionBuffer autocompletePredictions = results
.await(60, TimeUnit.SECONDS);
// 쿼리가 성공적으로 완료되었는지 확인하고, 그렇지 않으면 null을 반환합니다.
final Status status = autocompletePredictions.getStatus();
if (!status.isSuccess()) {
Log.e("", "Error getting autocomplete prediction API call: " + status.toString());
autocompletePredictions.release();
return null;
}
Log.i("", "Query completed. Received " + autocompletePredictions.getCount()
+ " predictions.");
// 버퍼를 고정 할 수 없으므로 결과를 자체 데이터 구조에 복사합니다.
// AutocompletePrediction 객체는 API 응답 (장소 ID 및 설명)을 캡슐화합니다.
Iterator<AutocompletePrediction> iterator = autocompletePredictions.iterator();
ArrayList resultList = new ArrayList<>(autocompletePredictions.getCount());
while (iterator.hasNext()) {
AutocompletePrediction prediction = iterator.next();
// 세부 정보를 가져 와서 새로운 PlaceAutocomplete 객체로 복사합니다.
resultList.add(new PlaceAutocomplete(prediction.getPlaceId(),
prediction.getFullText(null)));
}
// 모든 데이터가 복사되었으므로 버퍼를 해제
autocompletePredictions.release();
return resultList;
}
Log.e("", "Google API client is not connected for autocomplete query.");
return null;
}
@Override
public PlaceViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
LayoutInflater layoutInflater = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View convertView = layoutInflater.inflate(this.layout, viewGroup, false);
PlaceViewHolder mPredictionHolder = new PlaceViewHolder(convertView);
return mPredictionHolder;
}
@Override
public void onBindViewHolder(PlaceViewHolder mPredictionHolder, final int i) {
mPredictionHolder.address.setText(resultList.get(i).description);
mPredictionHolder.parentLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onPlaceClick(resultList, i);
}
});
}
@Override
public int getItemCount() {
if (resultList != null)
return resultList.size();
else
return 0;
}
public PlaceAutocomplete getItem(int position) {
return resultList.get(position);
}
/**
* 뷰 홀더
*
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 4:57
**/
public class PlaceViewHolder extends RecyclerView.ViewHolder {
public RelativeLayout parentLayout;
public TextView address;
public PlaceViewHolder(View itemView) {
super(itemView);
parentLayout = (RelativeLayout) itemView.findViewById(R.id.predictedRow);
address = (TextView) itemView.findViewById(R.id.tv_address);
}
}
/**
* 지역 정보 소유자 데이터 자동 완성 API 결과입니다.
*
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 4:58
**/
public class PlaceAutocomplete {
public CharSequence placeId;
public CharSequence description;
PlaceAutocomplete(CharSequence placeId, CharSequence description) {
this.placeId = placeId;
this.description = description;
}
@Override
public String toString() {
return description.toString();
}
}
}
14. 메인 클래스를 만든다.
SearchActivity.class
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.location.places.PlaceBuffer;
import com.google.android.gms.location.places.Places;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import java.util.ArrayList;
/**
* 찾기 화면
*
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 12:42
**/
public class SearchActivity extends BaseActivity implements PlaceAutocompleteAdapter.PlaceAutoCompleteInterface, GoogleApiClient.OnConnectionFailedListener,
GoogleApiClient.ConnectionCallbacks, OnClickListener{
private static final int REQ_CODE_SPEECH_INPUT = 100;
/**
* google place api 를 위한 변수들 START
**/
private GoogleApiClient googleApiClient;
private RecyclerView rvAutocomplateKeyword;
private LinearLayoutManager llm;
private PlaceAutocompleteAdapter placeAutocompleteAdapter;
private static final LatLngBounds BOUNDS_INDIA = new LatLngBounds(
new LatLng(-0, 0), new LatLng(0, 0));
private EditText edSearch = null;
/**
* google place api 를 위한 변수들 END
**/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// 초기화
init();
}
/**
* 초기화
*
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 2:31
**/
private void init() {
// 장소 찾기 초기화
initPlace();
}
/**
* 장소 찾기 초기화
*
* @author 임성진
* @version 1.0.0
* @since 2018-06-01 오후 12:55
**/
private void initPlace() {
this.googleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this, 0 /* clientId */, this)
.addApi(Places.GEO_DATA_API)
.build();
this.rvAutocomplateKeyword = (RecyclerView) findViewById(R.id.list_search);
this.rvAutocomplateKeyword.setHasFixedSize(true);
this.llm = new LinearLayoutManager(SearchActivity.this);
this.rvAutocomplateKeyword.setLayoutManager(llm);
this.edSearch = (EditText) findViewById(R.id.ed_search);
this.placeAutocompleteAdapter = new PlaceAutocompleteAdapter(this, R.layout.item_search,
googleApiClient, BOUNDS_INDIA, null);
this.rvAutocomplateKeyword.setAdapter(placeAutocompleteAdapter);
// 글자를 입력하면 place api를 요청한다.
this.edSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count > 0) {
if (placeAutocompleteAdapter != null) {
rvAutocomplateKeyword.setVisibility(View.VISIBLE);
}
} else {
if (placeAutocompleteAdapter != null) {
placeAutocompleteAdapter.clearList();
rvAutocomplateKeyword.setVisibility(View.GONE);
}
}
if (!s.toString().equals("") && googleApiClient.isConnected()) {
placeAutocompleteAdapter.getFilter().filter(s.toString());
} else if (!googleApiClient.isConnected()) {
Log.e("", "NOT CONNECTED");
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
@Override
public void onClick(View v) {
switch (v.getId()) {
}
}
@Override
public void onConnected(Bundle bundle) {
}
@Override
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
}
@Override
public void onPlaceClick(ArrayList<PlaceAutocompleteAdapter.PlaceAutocomplete> resultList, int position) {
if (resultList != null) {
try {
final String placeId = String.valueOf(resultList.get(position).placeId);
PendingResult<PlaceBuffer> placeResult = Places.GeoDataApi
.getPlaceById(googleApiClient, placeId);
placeResult.setResultCallback(new ResultCallback<PlaceBuffer>() {
@Override
public void onResult(PlaceBuffer places) {
if (places.getCount() == 1) {
// 이곳에서 키워드를 선택한 데이터를 처리한다.
Location location = new Location(places.get(0).getName().toString());
location.setLatitude(places.get(0).getLatLng().latitude);
location.setLongitude(places.get(0).getLatLng().longitude);
} else {
Toast.makeText(getApplicationContext(), "something went wrong", Toast.LENGTH_SHORT).show();
}
}
});
} catch (Exception e) {
} finally {
runOnUiThread(new Runnable() {
@Override
public void run() {
placeAutocompleteAdapter.clearList();
rvAutocomplateKeyword.setVisibility(View.GONE);
}
});
}
}
}
@Override
public void onStart() {
this.googleApiClient.connect();
super.onStart();
}
@Override
public void onStop() {
this.googleApiClient.disconnect();
super.onStop();
}
}
15. AndroidManifest.xml에 SearchActivity 을 꼭 추가하고, import가 안된 부분은 import 해주고 빌드한다.
16. 실행화면
'Programming > Android' 카테고리의 다른 글
[Android]RecyclerView 안에 ViewPager 여러개 쓰는 방법 (0) | 2019.02.14 |
---|---|
[Android]Raw 폴더에 위치한 영상, Thumbnail 가져오기 (0) | 2018.07.30 |
[Android] SurfaceView 그레이디언트(gradient) 이미지 깨짐 현상 (0) | 2018.04.26 |
Android 전화 상태 리시버(Receiver) (0) | 2018.04.05 |
Android 8.0(O) TYPE_SYSTEM_ALERT (0) | 2018.04.05 |