http://lomohome.com/316 by Geunwon,Mo (mokorean@gmail.com) Android 의 MapView (Google API) 정리하기. 원래하나은행스마트폰뱅킹의위치기반 (LBS) 지점찾기는 WebView 에서 Google Map API 를통하여구현이되어있었다. 아이폰에서는이게잘돌아가는데... 안드로이드에서는기계마다되는것도있고, 안되는것도있고.. 영껄쩍지근했다. ( 사실이번에출시한갤럭시 S 에서안돌아가는이유가가장컸지..) 그래서내친김에 WebView 에서구현하지말고 MapView 로구현해버리기로했다. 이틀정도작업한거라고쳐야할부분도많고 ( 특히 Runnable 로구현한길게누르기는...) 버그도좀있지만일단돌아가니, 이제까지한것을까먹지않으려고블로그에정리를해둔다. MapView 추가하기. AndroidMenifest.xml 을수정한다. <uses-permission android:name="android.permission.internet" /> <uses-permission android:name="android.permission.access_coarse_location" /> <uses-permission android:name="android.permission.access_fine_location" /> 윗줄부터 INTERNET 은구글지도 API 가인터넷연결을통하여데이터를받아오기때문에추가해주어야하고 ACCESS_***_LOCATION 은현재위치를프로바이더 ( 네트웍,GPS) 를통하여받아오기위해추가해준다. 그다음, <application> 태그안쪽에수정되어야할항목이다. 먼저, <!-- 안드로이드맵뷰를사용하려면라이브러리를추가한다. --> <uses-library android:name="com.google.android.maps" /> 라이브러리를사용함을선언해준다. 그리고액티비티선언을하나추가해준다. <!-- 지점찾기맵 --> <activity android:name=".branchmapactivity" android:screenorientation="portrait"> <intent-filter> <category android:name="android.intent.category.launcher" /> </intent-filter> </activity> 다음은레이아웃을그려줄 branchmap.xml 에서쓰인맵뷰부분의선언이다.... <com.google.android.maps.mapview android:id="@+id/mapview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:enabled="true" android:clickable="true" android:apikey="0kim******" /> <!-- API 키를등록해야동작한다. -- >...
위에서쓰인 android:apikey 는각개발머신에따라따로받아서적어넣어야한다. API Key 를넣지않으면동작은하지만지도데이터를받아오지않는다. 여기서따로설명은하지않고, 다음의링크를따라가면 MD5 값을가지고구글 API 키를받아오는법이잘설명이되어있다. http://www.mobileplace.co.kr/1070 참고로나는맥을사용해서개발을진행하였기때문에다음의명령어로 MD5 키를받아왔다. keytool -list -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android 받아온다음 Google Map API 사이트 (http://code.google.com/intl/ko-kr/android/mapsapi-signup.html) 에서 API 를받아와서 XML 에넣어주면된다. 이제맵뷰를표시하는핵심클래스인 BranchMapActivity.java 의내용중맵뷰에관련한부분을정리해본다. public class BranchMapActivity extends MapActivity { 맵을표시하는액티비티는 MapActivity 를상속받아구현한다. 다음은전역변수로사용되어진변수중, 지도의표시에관련한변수들이다. private MapView mapview; // 맵뷰객체 private List<Overlay> listofoverlays; // 맵에표시된오버레이 ( 레이어 ) 들을가지고있는리스트 private String bestprovider; // 현재위치값을가져오기위한프로바이더. (network, gps) private LocationManager locm; // 위치매니저 private LocationListener locl; // 위치리스너
private Location currentlocation; // 현재위치 private MapController mapcontroller; // 맵을줌시키거나, 이동시키는데사용될컨트롤러 private LocationItemizedOverlay overlayhere; // 현재위치마커가표시되어질오버레이 private LocationItemizedOverlay overlaybranch; // 지점위치마커들이표시되어질오버레이 private List<BranchInfoDTO> brlist; // 지점리스트 다음은 oncreate 메소드에서맵뷰에관련한부분이다. @Override protected void oncreate(bundle savedinstancestate) {! super.oncreate(savedinstancestate);!...! setcontentview(r.layout.branchmap); // 맵액티비티 xml 을풀어헤친다.!...! overlayhere = null;! overlaybranch = null; // 각오버레이초기화!...! mapview = (MapView) findviewbyid(r.id.mapview); // 맵뷰객체를가져온다.! mapview.setbuiltinzoomcontrols(true); // 줌인, 줌아웃컨트롤을표시한다.! mapcontroller = mapview.getcontroller(); // 맵컨트롤러를가져온다.! mapcontroller.setzoom(17); // 초기확대는 17 정도로..! // 위치매니저를시스템으로부터받아온다.! locm = (LocationManager) getsystemservice(context.location_service);!! // 사용가능한적절한프로바이더를받아온다.! //network ( 보통 3G 망,Wifi AP 위치정보 ) 또는 gps 둘중하나로설정된다.! bestprovider = locm.getbestprovider(new Criteria(), true);! // 기기에가지고있는마지막위치정보로현재위치를초기설정한다.! currentlocation = locm.getlastknownlocation(bestprovider);!! // 위치리스너초기화! locl = new MyLocationListener();! // 위치매니저에위치리스너를셋팅한다.! // 위치리스너에서 10000ms (10 초 ) 마다 100 미터이상이동이발견되면업데이트를하려한다.! locm.requestlocationupdates(bestprovider, 10000, 100, locl);! // 처음에한번맵뷰에그려준다.! updateoverlay(currentlocation); } 위에서한번언급된 MyLocationListener 는액티비티클래스안에인너클래스로구현한다. 리스너는로케이션매니저에추가되어 GPS 나네트워크로부터위치정보변경되는것을감시하게된다. public class MyLocationListener implements LocationListener {! public void onlocationchanged(location location) {!! // 위치이동이발견되었을때호출될메소드.!! // 위의설정에서 10초마다 100미터이상이동이발견되면호출된다.!! updateoverlay(location);
! public void onproviderdisabled(string provider) {!! Log.d(LOG_TAG, "GPS disabled : " + provider);! public void onproviderenabled(string provider) {!! Log.d(LOG_TAG, "GPS Enabled : " + provider);! public void onstatuschanged(string provider, int status, Bundle extras) {!! Log.d(LOG_TAG, "onstatuschanged : " + provider + " & status = "!!!! + status); } 다음은내가구현한지도그려주기액티비티의꽃이라할수있는 updateoveray 메소드이다. 요청을받으면 Location 객체 ( 위치 ) 를기준으로현재위치마커를찍고, 지점리스트를 HttpClient 를통하여통신해서받아온후지점들의마커를표시하게된다. protected void updateoverlay(location location) {!! // 기존에화면에찍어둔오버레이 ( 마커들 ) 을싹지운다.! listofoverlays = mapview.getoverlays(); // 맵뷰에서오버레이리스트를가져온다.! if (listofoverlays.size() > 0) {!! listofoverlays.clear(); // 오버레이가있을때싹지워준다.!! Log.d(LOG_TAG, "clear overlays : " + listofoverlays.size()); else {!! Log.d(LOG_TAG, "empty overlays");! //Location 객체를가지고 GeoPoint 객체를얻어내는메소드! GeoPoint geopoint = getgeopoint(location);! // 현재위치를표시할이미지! Drawable marker;! // 실제운영소스엔분기하여현재위치와선택위치이미지를변경하게되어있다.! marker = getresources().getdrawable(r.drawable.icon_here);!!! marker.setbounds(0, 0, marker.getintrinsicwidth(), marker.getintrinsicheight());! //LocationItemizedOverlay 를이용하여현재위치마커를찍을오버레이를생성한다.! overlayhere = new LocationItemizedOverlay(marker);! //touch event 의 null pointer 버그를방지하기위해마커를찍고바로 populate 시켜준다.! overlayhere.mpopulate();!! // 현재위치를 GeoCoder 를이용하여대략주소와위, 경도를 Toast 를통하여보여준다.! String geostring = shownowhere(location.getlatitude(), location.getlongitude(), true);! // 현재위치마커정의! OverlayItem overlayitem = new OverlayItem(geoPoint, "here", geostring);! overlayhere.addoverlay(overlayitem); // 현재위치오버레이리스트에현재위치마커를넣는다.! // 지점정보를 HTTP 통신을통해서버에서받아와서전역변수인 brlist ( 지점리스트 ) 에넣는다.! // 성능을고려하여쓰레드로구현이되어있다.
! // 고다음지점리스트오버레이에넣고화면에찍어주는메소드.! showbranchmarker(location.getlatitude(), location.getlongitude(),!!! this.searchtype, SEARCH_RANGE);!! // 맵뷰에서터치이벤트를받을오버레이를추가한다.! // 특정지점을오래눌렀을때특정지점기준으로재검색을하기위하여터치이벤트를받아와야한다.! mapview.getoverlays().add(new MapTouchDetectorOverlay());! // 마지막으로생성된오버레이레이어를맵뷰에추가한다.! mapview.getoverlays().add(overlayhere);! mapview.getcontroller().animateto(geopoint); // 현재위치로화면을이동한다.! mapview.postinvalidate(); // 맵뷰를다시그려준다.!! } 조금복잡하고지저분하게구성되어있어퍼포먼스는조금떨어진다. 개선의여지가있다. 시간나면수정해보자... 다음은 updateoverlay 메소드에서사용되었던 getgeopoint 메소드전문이다. private GeoPoint getgeopoint(location location) {! if (location == null) {!! return null;! Double lat = location.getlatitude() * 1E6;! Double lng = location.getlongitude() * 1E6;! return new GeoPoint(lat.intValue(), lng.intvalue()); } 별것없다. 주의해야할점은 GeoPoint 객체는위도, 경도표시에 1E6 을곱해줘야한다는것이다. 그리고마커를생성하고오버레이에표시, 그리고마커를눌렀을때이벤트를발생시키는클래스이다. 인너클래스로구현하였다. protected class LocationItemizedOverlay extends!! ItemizedOverlay<OverlayItem> {! private List<OverlayItem> overlays;! public LocationItemizedOverlay(Drawable defaultmarker) { // 오버레이생성자!! // 마커이미지의가운데아랫부분이마커에서표시하는포인트가되게한다.!! super(boundcenterbottom(defaultmarker));!! overlays = new ArrayList<OverlayItem>();! protected OverlayItem createitem(int i) {!! return overlays.get(i);! public int size() {!! return overlays.size();! public void addoverlay(overlayitem overlay) {!! overlays.add(overlay);!! //null pointer 버그때문에오버레이아이템추가후가능한빨리 populate 해줘야한 다.!! populate();
! protected boolean ontap(int index) {!! // 마커를눌렀을때발생시킬이벤트메소드이다.!! if ("here".equals(overlays.get(index).gettitle())) {!!! // 현재위치일경우간단한토스트메세지를보여준다.!!! Toast.makeText(getApplicationContext(),!!!!! overlays.get(index).getsnippet(), Toast.LENGTH_SHORT)!!!!!.show();! else {!!! // 지점선택일경우다이얼로그를통하여지점정보를보여준다.!!! // 전화걸기 버튼으로지점으로전화거는기능도추가되어있다.!!! // 맵뷰에관련한소스가아니어서이곳에서는표시하지않는다.!!!...!!! return true;! // 외부에서마커의 populate 를해주기위한메소드.! public void mpopulate() {!! populate(); } 지점정보를 HTTP 통신을통해가져오는메소드이다. HTTP 통신시랙현상을없애기위해쓰레드로구현을해봤다. 근데스레드가생각한대로동작하진않는것같다. 잘못쓰고있는것일까... -_- private void showbranchmarker(double lat, Double lng, String searchtype,!! String searchrange) {! GetMapDataThread excutethread = new GetMapDataThread(getMapdataHandler,!!! lat, lng, searchtype, searchrange);! excutethread.start(); } 실제 HTTP 통신을하는클래스를호출하는쓰레드이다. HTTP 통신부분은지도표시와상관이없기때문에여기서소스를게시하지는않는다. 다만기존에 HTTPConnection 으로구현되어있던 HTTP 통신을 HTTPClient 로변경하니까퍼포먼스도훨신좋아지고불필요한커넥션을줄일수있었다. private class GetMapDataThread extends Thread {! private Handler thandler;! private Double lat, lng;! private String searchtype;! private String searchrange;! public GetMapDataThread(Handler thandler) {!! this.thandler = thandler;! public GetMapDataThread(Handler thandler, Double lat, Double lng,!!! String searhtype, String searchrange) {
!! this(thandler); // 스레드처리완료후지도에가져온지점정보를가지고마커를찍 어줄핸들러!! this.lat = lat; // 위도!! this.lng = lng; // 경도!! this.searchtype = searhtype; // 검색조건 (0 : 지점, 1: ATM)!! this.searchrange = searchrange; // 검색범위단위는 m( 미터 ) 이다.! public void run() { // 스레드실행 ~!! Bundle bundle = new Bundle();!! try {!!! // 전역변수로선언한지점리스트를준비한다. BranchInfoDTO 는도메인이다.!!! brlist = new ArrayList<BranchInfoDTO>();!!! brlist = gda.getmapdata(lat.tostring(), lng.tostring(),!!!!! searchtype, searchrange);!!! //gda 클래스는 HTTP 통신을해서지점정보를가져오는클래스이다.!!! // 여기서는설명하지않았다. oncreate 에서생성했다.!!! bundle.putboolean("success_key", true); // 성공하면번들에성공메세 지셋팅! catch (Exception e) {!!!...!!! bundle.putboolean("success_key", false); // 실패하면 false 이다.!!! // ignore! finally {!!! try {!!!! Message msg = thandler.obtainmessage();!!!! msg.setdata(bundle);!!!! thandler.sendmessage(msg); // 핸들러에메세지를보낸다.!!!! interrupt();!! catch (Exception e) {!!!! // ignore!!! } 스레드에서 HTTP 통신을통하여가져온지점정보를가지고지도에지점마커들을찍어주고오버레이에추 가하는핸들러이다. final Handler getmapdatahandler = new Handler() {! public void handlemessage(message msg) {!! if (msg.getdata().getboolean("success_key")) { // HTTP 통신이성공적으로이루어졌을때.!!! // draw branches!!! Drawable branchmarker;!!! int markertype = 0;
!!! if ("0".equals(searchType)) { // 검색조건에따라마커이미지를지점,ATM 중에선택!!!! markertype = R.drawable.icon_branch;!! else if ("1".equals(searchType)) {!!!! markertype = R.drawable.icon_atm;!!!!! branchmarker = getresources().getdrawable(markertype);!!! branchmarker.setbounds(0, 0, branchmarker.getintrinsicwidth(),!!!!! branchmarker.getintrinsicheight());!!! Double lat, lng;!!! // 지점마커들을그려줄오버레이를준비한다.!!! overlaybranch = new LocationItemizedOverlay(branchMarker);!!! overlaybranch.mpopulate();!!! StringBuilder sb;!!! // 반복문을돌면서마커들을오버레이에추가한다.!!! // 나중에마커를눌렀을때다이얼로그에지점정보를보여주기위해스니펫에몇가 지정보를!!! //string 으로전달한다.!!! for (BranchInfoDTO d : brlist) {!!!! lat = Double.parseDouble(d.getYCord()) * 1E6;!!!! lng = Double.parseDouble(d.getXCord()) * 1E6;!!!! GeoPoint branchgeopoint = new GeoPoint(lat.intValue(),!!!!!! lng.intvalue());!!!! sb = new StringBuilder();!!!! sb.append(d.getbussbrnm()).append(";")!!!!!!.append(d.getbussbrtelno()).append(";")!!!!!!.append(d.getbussbradr()).append(";")!!!!!!.append(d.gettrscdrtm()).append(";")!!!!!!.append(d.getbussbradr2());!!!! // Create new overlay with marker at geopoint!!!! OverlayItem overlayitem = new OverlayItem (branchgeopoint,!!!!!! "branch", sb.tostring());!!!! overlaybranch.addoverlay(overlayitem);!!!!!!! // 마커찍은것이없으면오류메세지를토스트로보여준다.!! if (overlaybranch.size() < 1){!!! Toast.makeText(getApplicationContext(),!!!!!! " 검색결과가없거나통신장애입니다.\n' 메뉴 ' 버튼을눌 러조건을변경하여다시검색해주세요.",!!!!!! Toast.LENGTH_LONG).show();!!! // 지점오버레이를맵뷰오버레이에최종적으로추가해준다.!! if (overlaybranch!= null) {!!! mapview.getoverlays().add(overlaybranch);!!! mapview.postinvalidate();! ; };
토스트메세지로현재주소와위도, 경도를잠시표시해주는메소드. private String shownowhere(double lat, double lng, boolean showoption){! StringBuilder geostring = new StringBuilder();! try {!! Geocoder goecoder = new Geocoder(getApplicationContext(),!!!! Locale.getDefault());!! Address adr = goecoder.getfromlocation(lat,!!!! lng, 1).get(0);!! if (adr.getlocality()!= null) geostring.append(adr.getlocality ()).append(" ");!! if (adr.getthoroughfare()!= null) geostring.append (adr.getthoroughfare());!! if (!"".equals(geostring.tostring())) geostring.append("\n\n");!! catch (Exception e) { }!! geostring.append(" 위도 : ").append(lat).append(", 경도 : ").append(lng);!! if (showoption){!! Toast.makeText(getApplicationContext(), geostring.tostring(),!!!! Toast.LENGTH_SHORT).show();!! return geostring.tostring(); } 캡춰화면에서 서울특별시신천동 과위, 경도가떠있는토스트이다. 그런데 송파구 를어떻게가져오는지모르겠다 -_-;;
이다음은화면에서터치이벤트를받아올오버레이이다. 맵뷰에서특정지점을누르고있으면현재위치가아닌특정지점을기준으로지점정보를검색해오려고만든오버레이인데길게누르는이벤트를받아오는방식이좀어거지이다. 분명이부분은개선이되어야할것이다. public class MapTouchDetectorOverlay extends Overlay implements!! OnGestureListener {! private GestureDetector gesturedetector;! //ontouchevent 의 ACTION_DOWN 등을가지고직접처리하지않고! // 제스처들을쉽게캐치할수있는리스너이다.! private OnGestureListener ongesturelistener;! private static final long LOOOOONG_PRESS_MILLI_SEC = 1500; // 1.5 초정도를길게누름으로인식한다.
! // for touch timer! private Handler mhandler;! private long touchstarttime;! private long longpresstime;! private MotionEvent globalevent;! // 생성자! public MapTouchDetectorOverlay() {!! gesturedetector = new GestureDetector(this);!! init();! public MapTouchDetectorOverlay(OnGestureListener ongesturelistener) {!! this();!! setongesturelistener(ongesturelistener);!! init();! // 생성자들이호출할초기화함수! private void init() {!! mhandler = new Handler();!! globalevent = null;! // 길게누름을감지할스레드! private Runnable looongpressdetector = new Runnable() {!! public void run() {!!! // 화면을누르고있던시간!!! long touchholdtime = longpresstime - touchstarttime;!!! if ((globalevent!= null)!!!!! && (touchholdtime > (LOOOOONG_PRESS_MILLI_SEC - 200))) { // 조건중에 200ms 를빼고검사하는것은기기마다성능이달라서약간의여유를준것이다.!!!! Log.d(LOG_TAG, "loooooong press detected!");!!!!!!!! float x = globalevent.getx();!!!! float y = globalevent.gety(); // 화면에서눌려있던지점을받아 온다.!!!! GeoPoint p = mapview.getprojection().frompixels((int) x,!!!!!! (int) y); // 눌려있던지점을위도경도로바꿔준다.!!!!!!!! Location selectedlocation = new Location (currentlocation);!!!!!!!! selectedlocation.setlatitude((p.getlatitudee6() / 1E6));!!!! selectedlocation.setlongitude((p.getlongitudee6() / 1E6));!!!! currentlocation = selectedlocation;!!!! locm.removeupdates(locl); // 현재위치리스너를잠시없애버린다.!!!! udateoverlay(currentlocation); // 지점재검색및마커다시표 시!!!!!!!! shownowhere((p.getlatitudee6() / 1E6), (p.getlongitudee6() / 1E6), true);!!! ;! public boolean ontouchevent(motionevent event, MapView mapview) {!! if (gesturedetector.ontouchevent(event)) {!!! return true;!
!! onlongpress(event);!! return false;! public boolean ondown(motionevent e) {!! if (ongesturelistener!= null) {!!! return ongesturelistener.ondown(e);! else {!!! // start timer!!! touchstarttime = System.currentTimeMillis();!!! mhandler.postdelayed(looongpressdetector,!!!!! LOOOOONG_PRESS_MILLI_SEC);!!! //1.5초있다가길게누름을체크해본다.!!! return false;! public boolean onfling(motionevent e1, MotionEvent e2, float velocityx,!!! float velocityy) {!! if (ongesturelistener!= null) {!!! return ongesturelistener.onfling(e1, e2, velocityx, velocityy);!!! return false;! public void onlongpress(motionevent e) {!! if (ongesturelistener!= null) {!!! ongesturelistener.onlongpress(e);!!! // 화면을누르고있으면 onlongpress 가호출되는데호출될때마다체크할시간을변수 에넣는다.!! // 이부분이퍼포먼스하락에영향을줄것같다.!! globalevent = e;!! longpresstime = System.currentTimeMillis();! public boolean onscroll(motionevent e1, MotionEvent e2,!!! float distancex, float distancey) {!! if (ongesturelistener!= null) {!!! ongesturelistener.onscroll(e1, e2, distancex, distancey);!!! return false;! public void onshowpress(motionevent e) {!! if (ongesturelistener!= null) {!!! ongesturelistener.onshowpress(e);!! public boolean onsingletapup(motionevent e) {!! if (ongesturelistener!= null) {!!! ongesturelistener.onsingletapup(e);!
!! return false;! public boolean islongpressenabled() {!! return gesturedetector.islongpressenabled();! public void setislongpressenabled(boolean islongpressenabled) {!! gesturedetector.setislongpressenabled(islongpressenabled);! public OnGestureListener getongesturelistener() {!! return ongesturelistener;! public void setongesturelistener(ongesturelistener ongesturelistener) {!! this.ongesturelistener = ongesturelistener; } 완성된지점찾기의동작모습. 액티비티를실행하게되면다음과같이작동한다.