반응형

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. 실행화면








반응형