아래 방법은 SD카드 가 아닌 device 의 메모리에 이미지를 저장, 로드 , 삭제 하는 방식이다.

SD카드에 하려면 경로명을 정확히 넣어주고 메니페스트 파일에

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

이내용을 추가함을 잊지말자

 

 

1. Bitmap 저장

 

     imgview = (ImageView)findViewById(R.id.imageView1);       
     Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.comp1_16);     
     imgview.setImageBitmap(bm);     
     

try{
     
     File file = new File("test.png");          
     FileOutputStream fos = openFileOutput("test.png" , 0);          
     bm.compress(CompressFormat.PNG, 100 , fos);          
     fos.flush();     
     fos.close();
     
     Toast.makeText(this, "file ok", Toast.LENGTH_SHORT).show();     
     }catch(Exception e) { Toast.makeText(this, "file error", Toast.LENGTH_SHORT).show();}
     

 

2. 저장된 Bitmap 불러오기

 특이한것은 저장할때는 파일 이름만 있어도 알아서 app 의 data 폴더에 저장되지만 불러올때는

 전체 패스를 다 적어줘야한다.

try{    
    imgview = (ImageView)findViewById(R.id.imageView1);       
       String imgpath = "data/data/com.test.SDCard_Ani/files/test.png";
       Bitmap bm = BitmapFactory.decodeFile(imgpath);       
       imgview.setImageBitmap(bm);
       Toast.makeText(getApplicationContext(), "load ok", Toast.LENGTH_SHORT).show();
    }catch(Exception e){Toast.makeText(getApplicationContext(), "load error", Toast.LENGTH_SHORT).show();}

    

 

3. 저장된 Bitmap 파일 삭제하기

try{
    File file = new File("data/data/com.test.SDCard_Ani/files/");
    File[] flist = file.listFiles();
    Toast.makeText(getApplicationContext(), "imgcnt = " + flist.length, Toast.LENGTH_SHORT).show();
    for(int i = 0 ; i < flist.length ; i++)
    {
     String fname = flist[i].getName();
     if(fname.equals("test.png"))
     {
      flist[i].delete();
     }
    }
    }catch(Exception e){Toast.makeText(getApplicationContext(), "파일 삭제 실패 ", Toast.LENGTH_SHORT).show();}
    

구글마켓

http://market.android.com/publish/

개발자 등록비  25$ 필요

한국쪽은 유료결제 불가

해외쪽으로 가능 고로 해외 계좌 필요

수익분배 7:3

 

티스토어

http://dev.tstore.co.kr/

범용공인인증서 필요

수익분배 7:3

 

 

쇼스토어

http://appstoreseller.show.co.kr

서류 몇개 요청 개인일때는 신분증 , 통장사본 요구

수익분배 7:3

 

오즈스토어

개발중

수익분배  100%

 

 

 

애플앱스토어

개발자 등록비  년99$ 필요

수익분배 7:3

출처 : http://www.androidpub.com/748389
질답에 세번 질문을 하면서..동시에 구글링과 야후 검색을 며칠동안 한 결과..
Dialog 를 상속 받아서 customizing 해야 한다는 결론을 얻었습니다.
외국의 어느 커뮤니티에 누군가 남긴 상속 받아 재정의한 코드를 얻었구요.
가져올 때 링크를 기록하지 않았더니...다시 찾아보려고 노력해도...
찾을 수가 없네요. 

여기에 감사의 글과 함께..링크를 올리고 싶었지만...다시 못찾은 관계로...패스~ 
감사의 마음만...가득합니다.

우선.. 상속 받은 코드는 질답게시판에 댓글로도 올렸지만.
정리하는 차원에서 여기 다시 올립니다.

아래는 Dialog 를 상속 받은 클래스입니다.
01.class MyProgressDialog extends Dialog {
02. 
03. 
04.public static MyProgressDialog show(Context context, CharSequence title,
05.CharSequence message) {
06.return show(context, title, message, false);
07.}
08. 
09.public static MyProgressDialog show(Context context, CharSequence title,
10.CharSequence message, boolean indeterminate) {
11.return show(context, title, message, indeterminate, falsenull);
12.}
13. 
14.public static MyProgressDialog show(Context context, CharSequence title,
15.CharSequence message, boolean indeterminate, boolean cancelable) {
16.return show(context, title, message, indeterminate, cancelable, null);
17.}
18. 
19. 
20.public static MyProgressDialog show(Context context, CharSequence title,
21.CharSequence message, boolean indeterminate,
22.boolean cancelable, OnCancelListener cancelListener) {
23.MyProgressDialog dialog = new MyProgressDialog(context);
24.dialog.setTitle(title);
25.dialog.setCancelable(cancelable);
26.dialog.setOnCancelListener(cancelListener);
27./* The next line will add the ProgressBar to the dialog. */
28.dialog.addContentView(new ProgressBar(context), new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
29.dialog.show();
30. 
31.return dialog;
32.}
33. 
34.public MyProgressDialog(Context context) {
35.super(context, R.style.NewDialog);
36.}
37.}

위의 코드에서 참조하는 NewDialog 의 스타일은 다음과 같습니다. res/values/styles.xml 을 생성 시키고 
아래의 코드를 넣습니다.


01.<resources>
02.<style name="NewDialog">
03.<item name="android:windowFrame">@null</item>
04.<item name="android:windowBackground">@android:color/transparent</item>
05.<item name="android:windowIsFloating">true</item>
06.<item name="android:windowContentOverlay">@null</item>
07.<item name="android:windowTitleStyle">@null</item>
08.<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
09.<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
10.<item name="android:backgroundDimEnabled">false</item>
11.<item name="android:background">@android:color/transparent</item>
12.</style>
13. 
14.</resources>

Dialog 를 이용하기 때문에.. 그리고.. WebView 에서 PageFinished 이벤트가 있기 때문에,
쓰레드를 사용할 필요가 없다는 것을 알게 되었습니다.
1.package pkg.WebViewTest;
01.import android.app.Activity;
02.import android.app.Dialog;
03.import android.content.Context;
04.import android.os.Bundle;
05.import android.os.Handler;
06.import android.os.Message;
07.import android.util.Log;
08.import android.view.MotionEvent;
09.import android.view.View;
10.import android.view.ViewGroup.LayoutParams;
11.import android.webkit.WebView;
12.import android.webkit.WebViewClient;
13.import android.widget.Button;
14.import android.widget.ProgressBar;
15.import android.widget.TextView;
1.public class WebViewTest extends Activity {
2./** Called when the activity is first created. */
3. 
4.public MyProgressDialog progressDialog;
1.@Override
2.public void onCreate(Bundle savedInstanceState) {
3.super.onCreate(savedInstanceState);
4. 
5.setContentView(R.layout.main);
6.wvc = (WebView)findViewById(R.id.WebView01);
7.wvc.getSettings().setJavaScriptEnabled(true);
01.wvc.loadUrl("http://m.naver.com");        
02.progressDialog = MyProgressDialog.show(this,"","",true,true,null);
03. 
04.wvc.setWebViewClient(new WebViewClient()
05.{
06.@Override
07.public void onPageFinished(WebView view, String url)
08.{
09.wvc.setVisibility(View.VISIBLE);
10.if (progressDialog!=null)
11.progressDialog.dismiss();
12.}
13.});
14.}
1.}

출처 : http://blog.naver.com/lowmans?Redirect=Log&logNo=100115258620

참고 : 
http://www.inter-fuser.com/2010/01/android-coverflow-widget.html
 
Android Gallery를 이뿌게~? 작업하기 위해서 구글 할부지께 물어본 결과 위에 링크한 CoverFlow를 알게 되었다
Gallery를 커스터 마이징한 소스인데 여간 잔망스러운게 아니여서.. 나름 깔끔하게 다듬어 본 소스를 공개한다.
 
불필요한 작업을 피하기 위해 바로 Gallery를 상속 받고
protected boolean getChildStaticTransformation(View child, Transformation t)
를 override 하여 작업하면 된다
 
getChildStaticTransformation 는 List에 연결된(gallery view 안에서 스크롤 할때 ) child가 어디에 위치 했는지 알고 싶을때 사용하는 method이다
 
metrix 는 child 의 bitmap 정보를  camera는  원근 효과를 주는 클래스들인데 이를  사용하여 gallery가 스크롤시 원근 효과를 주어 마치 스페이스 공간에서 움직이는 듯한 effect를 주었다 ..
 
view 를 click 할경우 animation 효과를 주어 나름 신경좀 써 봤지만.. 오히려 더 지저분한 느낌도 든다(순수한 나의 생각이지만  --;)
 
 
===========================================================================================================================
 
package lowmans.test;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Animation.AnimationListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
public class GalleryViewTest extends Activity implements AnimationListener{
 private MyGallery mGallery;
 Animation a;
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  mGallery = (MyGallery)findViewById(R.id.Gallery01);
  mGallery.setAdapter(new ImageAdapter(this));
  mGallery.setOnItemClickListener(new OnItemClickListener(){
   public void onItemClick(AdapterView<?> parent, View view,int position, long id) {
     Animation animation = AnimationUtils.loadAnimation(GalleryViewTest.this, R.anim.anim);
     animation.setAnimationListener(GalleryViewTest.this);
     view.startAnimation(animation);
   }
  });
 }
 @Override
 public void onAnimationEnd(Animation animation) {
  Log.i("GalleryViewTest" , "onAnimationEnd");
 }
 @Override
 public void onAnimationRepeat(Animation animation) {}
 @Override
 public void onAnimationStart(Animation animation) {}
}
 
===========================================================================================================================
 
package lowmans.test;
import android.content.Context;
import android.graphics.Camera;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Transformation;
import android.widget.Gallery;

public class MyGallery extends Gallery{
 private final static String TAG = "MyGallery";
 private Context mContext;
 private static Camera mCamera; 
 public MyGallery(Context context) {
   this(context, null); 
 }
 public MyGallery(Context context, AttributeSet attrs) {
   this(context, attrs, 0); 
 }
 public MyGallery(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   mContext = context;
   mCamera = new Camera();
   setSpacing(-30);  // child view 의 간격을 줄여 겹치는 듯한 효과를 준다
    }
  protected boolean getChildStaticTransformation(View child, Transformation t) {
  
  final int mCenter =(getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft();
  final int childCenter = child.getLeft() + child.getWidth() / 2;
  final int childWidth = child.getWidth();
  
  t.clear();
  t.setTransformationType(Transformation.TYPE_MATRIX);
  float rate = Math.abs((float)(mCenter - childCenter)/ childWidth);
  
  mCamera.save();
  final Matrix matrix = t.getMatrix();
  float zoomAmount = (float) (rate * 200.0);
  mCamera.translate(0.0f, 0.0f, zoomAmount);        
  mCamera.getMatrix(matrix);    
  matrix.preTranslate(-(childWidth/2), -(childWidth/2));   
  matrix.postTranslate((childWidth/2), (childWidth/2));
  mCamera.restore();
  return true;
    }

}
 
===========================================================================================================================
package lowmans.test;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.Gallery.LayoutParams;
 
public class ImageAdapter extends BaseAdapter {
    int mGalleryItemBackground;
    private Context mContext;
    private ImageView[] iv;
 
    private Integer[] mImageIds = {
         R.drawable.back_1 ,
         R.drawable.back_2 ,
         R.drawable.back_3 ,
         R.drawable.image ,
         R.drawable.back_1 ,
         R.drawable.back_2 ,
         R.drawable.back_3 ,
         R.drawable.image ,
        };
        
    private int cnt;
    public ImageAdapter(Context c) {
        mContext = c;
        cnt = mImageIds.length;
        iv = new ImageView[cnt];
       
       
        for(int i=0; i<cnt; i++){
          iv[i] = new ImageView(mContext);
          iv[i].setImageResource(mImageIds[i]);
          iv[i].setScaleType(ImageView.ScaleType.FIT_XY);
          iv[i].setLayoutParams(new Gallery.LayoutParams(200, 150));
        }
    }
    public int getCount() {
        return cnt;
    }
    public Object getItem(int position) {
        return position;
    }
    public long getItemId(int position) {
        return position;
    }
    public View getView(int position, View convertView, ViewGroup parent) {
         return iv[position];
    }
}
=========================================================================================================================== 
출처 : http://karukaru22.blog.me/140123913153

 style.xml

 

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NewDialog">
     <item name="android:windowFrame">@null</item>
     <item name="android:windowBackground">@android:color/transparent</item>
     <item name="android:windowIsFloating">true</item>
     <item name="android:windowContentOverlay">@null</item>
     <item name="android:windowTitleStyle">@null</item>
     <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
     <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
     <item name="android:backgroundDimEnabled">false</item>
     <item name="android:background">@android:color/transparent</item>
 </style>
 
<style name="Theme.CustomDialog" parent="android:style/Theme.Dialog">
        <item name="android:windowBackground">@android:color/transparent</item>
    </style> 
 
</resources>

 public class ProgressActivity extends Activity {
    Context mContext = this;
    Dialog dilog ;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
      
       Circle c = new Circle();
        new Thread(c).start();
        dilog = new Dialog(this,R.style.NewDialog);
        //dilog =ProgressDialog.show(mContext, "", "잠시만 기달려주세요");
        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        View v = inflater.inflate(R.layout.progress_circle, null);
        dilog.setContentView(v);
        dilog.show();
    }
   
   
    private class Circle implements Runnable {
     
  @Override
  public void run() {

  try {
   Thread.sleep(10000);
   handler.sendEmptyMessage(0);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
   }
  }
     
    }
 
    private Handler handler = new Handler(){
     public void handleMessage(Message msg){
      if (msg.what == 0){
       dilog.dismiss();
       Toast.makeText(ProgressActivity.this, "완료~", 0).show();
      }
     }
    };
   
}

 

다이얼로그에 창을 투명하게 하고 거기다가 프레그시바를 붙인겁니다~

 

주의깊게 볼것은 다이얼로그에 스타일을 적용햇으며 스타일.xml에 보시면 transparent는 투명하게 해준다는 뜻입니다..

 

이걸 응용해서 액티비티 자체도 투명하게 할수 있습니다..

 


 

AndroidMainfest.xml에서

 

투명하게 원하는 액티비티에

 

  android:theme="@style/Theme.CustomDialog" 이렇게 주면된다.. 그리고 위에 style.xml도 마지막라인줄에 것을 적용시켜야함

 

 

출처 : http://blog.naver.com/lowmans/100121671992
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at


     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

    <item android:state_window_focused="false" android:state_enabled="true"
        android:drawable="@drawable/btn_default_normal" />
    <item android:state_window_focused="false" android:state_enabled="false"
        android:drawable="@drawable/btn_default_normal_disable" />
    <item android:state_pressed="true" 
        android:drawable="@drawable/btn_default_pressed" />
    <item android:state_focused="true" android:state_enabled="true"
        android:drawable="@drawable/btn_default_selected" />
    <item android:state_enabled="true"
        android:drawable="@drawable/btn_default_normal" />
    <item android:state_focused="true"
        android:drawable="@drawable/btn_default_normal_disable_focused" />
    <item
         android:drawable="@drawable/btn_default_normal_disable" />
</selector>


[펌] http://monodream77.blog.me/130088948287

안드로이드 플랫폼에서의 일관성 있는 아이콘 제작을 위해 
Android developers site에서  제공하는 가이드라인입니다. 
안드로이드 어플리케이션에 사용되는 아이콘을 디자인을 할 때 가이드라인에서 명시하는 기본적인 요소들을 고려해야 안드로이드 플랫폼 안에서 일관성 있는 어플리케이션의 디자인을 만들 수 있을 것 같습니다. 가이드라인에서는 그림자의 거리나 사이즈까지 디테일하게 수치를 명시하고 있지만 어플리케이션 디자인에 따라 디테일한 부분은 충분히 디자이너의 의도에 따라 달라져도 될 것 같습니다. 실제로 요즘 나오는 안드로이드 어플리케이션들을 보면 어플리케이션의 컨셉에 따라 안드로이드의 가이드라인을 따르지 않은 경우도 많이 있습니다. 하지만 제품의 독특한 컨셉이나 사용성의 목적상 필요한 부분이 아니라면 안드로이드의 가이드라인을 따르는 것이 안드로이드 폰 사용자에게 일관성 있는 느낌과 사용하기 쉬운 UI를 제공할 수 있을 것 같습니다. 어느 부분까지 가이드를 따르고 어느 부분에서 차별화 되게 디자인을 할 지는 디자이너들이 판단할 몫인 것 같습니다.

 

※ 원문에서 중요한 부분만 번역하였으며 필자의 의견은 다른 컬러로 표기되었습니다.

 

원문 출처 :  Icon Design Guidelines, Android 2.0

 

 

 

 

Icon Design Guidelines, Android 2.0

 

 

Providing Density-Specific Icon Sets

 

안드로이드는 여러가지 해상도를 가진 단말에서 동작하게 되어 있기 때문에 어플리케이션이 어느 단말에서건 돌아갈 수 있다는 것을 염두해 두어야 한다. 그래서 단말의 모든 스크린 사이즈(물리적 크기)나 해상도(가로/세로 픽셀수)에 적당한 아이콘을 제공해야 한다.

 

안드로이드를 지원하는  스크린의 밀도는 아래와 같이 Low density(ldpi), Medium density(mdpi), High density(hdpi)의 세 가지로 나뉘고 아이콘의 크기도 세 가지로 제작 되어야 합니다. 여기서 주목할 점은 VGA급의 스크린이 스크린의 사이즈(대각선 길이)에 따라 mdpi에 속할 수도 있고 hdpi에 속할 수도 있다는 것입니다.

같은 VGA급의 해상도라 하더라도 4.8"~5.8"에 적용되면 mdpi 크기의 중간 크기 아이콘을 적용하고, 3.3"~4.0"(갤럭시S WVGA, 4")에 적용되면 hdpi의 큰 아이콘을 적용해야 한다는 것이죠. 그렇게 해야 스크린에 나타나는 버튼이나 아이콘의 물리적 크기는 동일하게 보일 것입니다. 같은 mdpi사이즈의 아이콘(예: 48x48크기의 런쳐 아이콘)을 320x480, 3.0" 스크린에서 보이는 것과, 480x800, 4.8"스크린에서 보이는 것은 동일한 물리적인 크기(스크린에서 아이콘을 자로 재었을 때 동일한 크기) 가진다는 것입니다.

물리적인 아이콘의 크기는 해상도에 반비례하고 스크린 사이즈에 비례한다고 보시면 됩니다. 물리적인 아이콘의 크기가 중요한 이유는 가시성은 물론이고 충분한 터치영역을 확보하기 위해서 입니다.

아직 안드로이드를 탑재한 S-pad(480x800, 7"~10" 예상)와 같은 태블릿 PC에는 어느 기준을 적용해야 할지 명시가 되어있지는 않습니다.

 

안드로이드에서 제공하는 해상도에 따른 정책은 Supporting Multiple Screens부분을 참고하시기 바랍니다.

Low density (120), ldpi

Medium density (160), mdpi

High density (240), hdpi

Small screen

  • QVGA (240x320), 2.6"-3.0" diagonal

Normal screen

  • WQVGA (240x400), 3.2"-3.5" diagonal

  • FWQVGA (240x432), 3.5"-3.8" diagonal

  • HVGA (320x480), 3.0"-3.5" diagonal

  • WVGA (480x800), 3.3"-4.0" diagonal

  • FWVGA (480x854), 3.5"-4.0" diagonal

Large screen

  • WVGA (480x800), 4.8"-5.5" diagonal

  • FWVGA (480x854), 5.0"-5.8" diagonal

   Examples of device screens supported by Android. 

 

 

 

안드로이드의 기본 해상도는 Medium density(mdpi)이며 아래의 기준에 따라 아이콘이 제작되어야 한다.

 

1. 먼저 기본 density(mdpi)에 따라 아이콘을 디자인

 

2. 기본 drawable resources폴더에 아이콘을 위치하고 안드로이드 가상머신이나 T-Mobile G1과 같은 HVGA단말에서 테스트

 

3. 테스트를 하며 기본 density아이콘 수정

 

4. 테스트 및 수정/보완이 완료되면 다른 사이즈의 아이콘 제작

  - high density 아이콘은 150%로 확대

  - low density 아이콘은 75%로 축소

 

 5. 아이콘을 각각의 density에 맞는 폴더에 저장

  - Medium density 아이콘은 res/drawable-mdpi/ 폴더 또는 res/drawable/ 폴더에 저장

  - High density 아이콘은 res/drawable-hdpi 폴더

  - Low density 아이콘은 res/drawable-ldpi 폴더

 6. high/low density 단말에서 아이콘이 잘 보이는지 각각 테스트

 

mdpi 한 가지의 아이콘만 있어도 ldpi, hdpi의 아이콘을 코드상에서 늘이고 줄여서 적용되게 하는 방법도 있다고 합니다. 하지만 강제 확대/축소가 되기 때문에 이미지가 조금 거칠어 보이고 각각의 아이콘이 제공되는 것보다 연산이 들어간다고 하네요. 주의할 점은 두 가지 방법의 코딩 방식이 다르기 때문에 개발 초기에 세 가지 사이즈 아이콘을 모두 제공할지, mdpi한가지만 제공하고 나머지는 확대/축소할 지 개발자와 협의를 하고 진행을 해야 한다는 것입니다. 이상적인 방법은 hdpi의 가장 큰 아이콘을 제작하고, mdpi, ldpi의 아이콘을 축소하여 제작하는 것입니다. 비트맵으로 제작되었을 경우 큰 아이콘을 작은 아이콘으로 축소하는 것이 덜 깨지고 다듬기도 더 용이하기 때문입니다.

 

 

Table 1. 아이콘 종류에 따른 세가지 density에서의 아이콘 크기.

Icon Type

Standard Asset Sizes (in Pixels), for Generalized Screen Densities

Low density screen (ldpi)

Medium density screen (mdpi)

High density screen (hdpi)

Launcher

36 x 36 px

48 x 48 px

72 x 72 px

Menu

36 x 36 px

48 x 48 px

72 x 72 px

Status Bar

24 x 24 px

32 x 32 px

48 x 48 px

Tab

24 x 24 px

32 x 32 px

48 x 48 px

Dialog

24 x 24 px

32 x 32 px

48 x 48 px

List View

24 x 24 px

32 x 32 px

48 x 48 px


 

 

 

Launcher Icon

 

런쳐 아이콘은 단말의 홈스크린이나 런쳐 윈도우에서 어플리케이션을 실행시키는 아이콘을 말합니다.

 

 

Style

런쳐 아이콘은 아래와 같은 스타일을 따라야 한다. 이 가이드라인은 아이콘 디자인을 제한한다기 보다는 다른 아이콘과의 통일성을 위한 것이다. 

 

Clean and contemporary - 깔끔하고, 현대적으로

일반적으로 통용되는 컨셉으로 할 수도 있고 독특하게 할 수도 있다. 그러나 구식이거나 낡아보여서는 안된다.

 

Simple and iconic - 단순하고 상징적으로

- 자연스러운 캐리커쳐 스타일; 형태는 단순하지만 특징을 부각시켜서 작은 크기에서도 잘 보여야 한다. 복잡해서는 안됨

- 어플리케이션의 일부를 표현함으로서 전체를 상징하도록 (예를들면 뮤직 아이콘은 스피커로 표현하고 있음.)

- 자연스러운 기하학적이고 유기적인 선과 형태를 이용여 현실적으로 표현(사진처럼 현실적이여서는 안됨.)

- 큰 이미지의 잘린 일부분으로 표현해서는 안됨.

 

Tactile and textured - 촉감이 느껴지는 질감

광택이 나지 않는, 질감을 살린 소재로 표현.

 

Forward-facing and top-lit - 정면뷰와 상단 라이팅

안드로이드2.0 이상에서는 약간의 투시가 있는 정면 뷰와 상단 라이팅을 사용해야 함

 

 

 

 

 

Figure 1. 런쳐 아이콘 스타일

 

 

 

 

Do's and Don'ts

 

- 안드로이드 아이콘은 Modern, minimal, matte, tactile, and textured 해야 함.

- 정면 뷰와 상단 라이팅을 사용해야 함

- 앤틱(Antique)하거나 복잡하거나 광택이 나거나 단순도형(flat vector)이면 안된다.

- 회전이 되었거나, 잘렸거나, 채도가 과하게 높으면 안된다.

 

 

 

Figure 2. 런쳐 아이콘의 잘된 예와 잘못된 예

 

 

 

Materials and colors - 질감과 색

 

런쳐 아이콘은 촉감이 느껴지고 상단 라이팅을 사용하고 질감이 느껴지는 소재를 이용하여 만들어야 한다. 단순한 형태의 아이콘이라도, 실제 존재하는 소재로부터 만들어진 것처럼 보이도록 해야 한다. 안드로이드 플랫폼의 기본 아이콘은 Figure 3의 소재들을 사용하고 있다. 아이콘을 만들 때 이 소재를 사용해도 되고, 직접 만들어도 된다.

 

안드로이드 런처 아이콘은 보통 큰 배경 형태 위에 작은 형태가 올라와있는 형태와 강조색과 중간색이 결합된 컬러로 구성된다. 아이콘은 중간색의 결합된 형태를 사용할 수도 있지만, 강한 대비는 유지되어야 한다. 가능하면 아이콘에서 강조색은 한가지만 사용되어야 한다.

 

런쳐아이콘은 중간색과 강조색의 범위를 포함하는 제한된 컬러팔레트를 사용해야 한다. 아이콘은 채도가 과하게 표현되어서는 안된다.

 

런쳐 아이콘에서 추천하는 컬러 팔레트는 figure 4에 있는 것들이다. 기본색과 강조색의 팔레트를 모두 사용할 수 있다. 컬러 팔레트에  white-black의 수직 그라데이션을 오버레이시켜 사용할 수도 있다.

 

 

 

 

Figure 3. 아이콘을 만드는데 사용할 수 있는 소재들

 

 

 

 

 

Figure 4. 추천하는 기본색과 강조색으로 이루어진 소재들

 

 

 

 

추천하는 팔레트의 강조색을 사용하여 위와 같은 소재를 만들 때, Figure 5에 있는 컬러를 조합하여 만들 수 있다. icons pack에 기본 소재, 컬러, 그라데이션을 제공하는 포토샵 템플릿 파일(Launcher-icon-template.psd)이 들어 있다.

 

 

 

 

 

 

Figure 5. 아이콘을 위한 추천 컬러 팔레트

 

 

 

 

 

Size and positioning - 크기와 위치

 

런쳐 아이콘은 다양한 모양과 형태로 제작되지만, 크기와 위치를 조정하여 일정한 크기로 보여야 한다.

Figure 6 은 아이콘 파일 안에 아이콘을 위치하는 여러가지 방법을 보여준다. 아래에 명시된 것 처럼, 일정한 크기로 보이고 그림자들 포함하기 위해 아이콘 파일의 실제 테두리보다 아이콘은 작게 들어가야 한다. 아이콘이 사각형이거나 사각형에 가깝다면, 아이콘은 더 작게 조절되어야 한다.

 

 

- 아이콘 파일 전체의 테두리는 붉은색으로 표시

- 실제 아이콘이 들어갈 공간은 파란색으로 표시. 아이콘이 들어갈 공간은 그림자나 다른 효과를 포함시키기 위해 실제 아이콘 파일의 크기보다 작게 되어있다.

- 사각 형태의 아이콘이 들어갈 공간은 오랜지컬러로 표시. 사각형태의 아이콘이 들어갈 공간은 다른 아이콘들과 크기가 비슷하게 보이기 위해 다른 아이콘들의 공간보다 작게 되어 있다.

 

 

 

                                                                                     Figure 6. 아이콘 파일 안에서의 아이콘 크기와 위치

 

 

 

 

Using the Launcher Icon Template - 런쳐 아이콘 템플릿 사용

 

 

Android Icon Templates Pack 2.0에는 기본 아이콘의 소재와 컬러가 들어있다. 템플릿은 포토샵이나 다른 이미지 에디터에서 사용할 수 있도록 .psd파일로 제공된다. Launcher-icon-template.psd에 있는 질감과 컬러를 런쳐 아이콘을 만드는데 사용할 수 있다.

 

아이콘을 만든 다음에는, 아래에 아이콘 크기별로 명시된 대로 그림자를 만들 수 있다.

 

 

 

 

아이콘은 투명도가 있는 PNG파일로 저장하라. hdpi 스크린에서는 72x72px, mdpi 스크린에서는 48x48px의 크기로 저장되어야 한다

출처 : http://spitfire.tistory.com/78
안드로이드 기본 SDK에서 제공하는 widget... MSDN수준의 문서화를 기대하는 건 아니지만 아무래도 API설명이 좀 부실하다 보니 있는 기능을 몰라서 안 쓰거나 없는 기능인데 찾느라 한참 헤메는 경우가 있는 것 같더군요.

뭐, 기본제공하는 ImageView에 없는 기능인 Touch Scroll/ Pinch to zoom 되게 하느라 좀 헤메서 결과물은 아무도 안 오는 블로그에라도 좀 올려놓으면 편할 듯 하네요.

아무런 설명 없이 달랑 클래스만 올려놓기는 그러니 조금 설명하자면...

일단 android.widget.ImageView 클래스를 상속받아 만든 클래스입니다. 당연히 ImageView의 속성들은 다 가지고 있고, 터치 이벤트 처리를 위해서 onTouchEvent 메서드가 오버라이드 되어 있습니다.

(사실 안드로이드 UI에서 터치이벤트를 가장 처음 받는 것은 그 view이기 때문에, view내에서 자체적으로 이벤트를 처리한다면 굳이 onTouchListener를 통해서 이벤트를 넘겨줄 필요가 없습니다)

대부분의 코드는

http://flysky.thoth.kr/?document_srl=4547416

이 주소에서 가져왔고, 디버그용 dumpEvent 메서드, onTouch 대신 onTouchEvent사용, 이미지 매트릭스 초기화 메서드 약간 수정(개선인지 개악인지는 모르겠지만...) 정도가 변경사항입니다.

사용법은 그냥 안드로이드 이미지 뷰 쓰듯이 쓰면 됩니다. 

사실 지금 쓰고 있는 건 안드로이드 프로젝트의 갤러리 부분 소스코드를 가져와서 약간 개량한 걸 사용중인데, 그것도 독자적으로 쓸만한 수준이 되면 공개해 보려고 합니다.아무래도 motionevent같은 걸 쓰는 게 좀 더 편할 듯 해서 말이지요. 2.2에서는 멀티터치 이벤트 처리도 gesturedetector를 쓸 수 있으니 지금 구현하기 좀 귀찮은 멀티터치 이벤트들도 쉽게 처리할 수 있겠지요.
출처 : http://taehoonkoo.tistory.com/143

오늘 제가 해볼 것은 Preference Category를 Customizing 해보는것입니다. 
우선 결과 부터 보여드리면, 


이런식으로 Category Title을 변경해보는것이죠.

위와 같은 Layout의 Preference XML은 아래와 같습니다. 

<?xml version="1.0" encoding="utf-8"?>


<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"

        android:title="@string/app_name">

    <PreferenceCategory

     android:title="@string/category_name">

    <Preference android:key="app_name"

        android:persistent="false"

        android:title="@string/app_name"

    />

    </PreferenceCategory>  

</PreferenceScreen>


PreferenceScreen에 PreferenceCategory를 추가하고, 
PreferenceCategory에 Preference Item이 하나 있는 형태 이지요, 

여기서, PreferenceCategory의 Layout을 Customizing하는게 목적입니다.

Preference Item의 Layout을 변경할때 사용하는것은,  android:layout 속성을 정의 하는것입니다. 


android:layout="@layout/my_preference_category" 뭐 이런식으로 추가 해주는것지요 

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

  xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="horizontal"

  android:layout_width="fill_parent"

  android:layout_height="25dip"

  android:gravity="center_vertical"

  android:paddingLeft="5dip"

  android:background="#323331">

  

  <ImageView 

  android:layout_width="wrap_content"

  android:layout_height="fill_parent"

  android:src="@android:drawable/ic_menu_info_details"/>

  <TextView

  android:textStyle="bold"

  android:id="@android:id/title"

  android:layout_width="wrap_content"

  android:layout_height="fill_parent" 

  android:text="test"

  android:gravity="center"

  android:textColor="#FF0000"

  />

  

</LinearLayout>


여기서 부터, 안드로이드 개발의 짜증나는 점이 나타나는데, 
PreferenceCategory에서 지정한 title 값과, 내가 만든 Layout의 TextView가 연결되어야 하는데, 
id값을 어떻게 지정해주어야 한다는 정보가, developer.android.com을 아무리 뒤져 봐도 없습니다.

결국 Android Source코드를 보고 나서야 
@android:id/title로 줘야 한다는걸 알게 되었고,  이렇게 해주고 나니 정상적으로  동작합니다.

android:layout은 PreferenceCategory의 XML Attribute가 아니라, Preference 아이템 클래스들의 최상위 클래스인 Preference Class의 
XML Attribute입니다. 

즉, PreferenceCategory뿐 아니라, CheckBoxPreference 등등 모든 Preference들을 이와 같은 원리로 Customizing할 수 있습니다. 

출처 : http://blog.naver.com/man8408?Redirect=Log&logNo=110104491800

안드로이드의 스크린 가로, 세로 모드를 고정시키는 방법은 2가지입니다.

 

1. source에서 수정

- Activity를 상속받은 클래스에서 onCreate() 안에 super.onCreate()전에 다음 함수를 실행시켜주면 간단하게 고정이 됩니다.

 setRequestedOrientation(Activity.SCREEN_ORIENTATION_LANDSCAPE);


2. AndroidManifest.xml에서 수정

- 가로 : landscape, 세로 : portrait 둘중 하나로 설정하면 된다.

<activity android:screenOrientation="landscape"

android:configChanges="keyboardHidden|orientation" />

 

3. 결과화면

   

package org.kodejava.example.util;

import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;

public class PropertiesToMap {
    public static void main(String[] args) {
        //
        // Create a new instance of Properties.
        //
        Properties properties = new Properties();

        //
        // Populate properties with a dummy application information
        //
        properties.setProperty("app.name", "HTML Designer");
        properties.setProperty("app.version", "1.0");
        properties.setProperty("app.vendor", "HTML Designer Inc");

        //
        // Create a new HashMap and pass an instance of Properties. Properties
        // is an implementation of a Map which keys and values stored as in a
        // string.
        //
        Map<String, String> map = new HashMap<String, String>((Map) properties);

        //
        // Get the entry set of the Map and print it out.
        //
        Set propertySet = map.entrySet();
        for (Object o : propertySet) {
            Map.Entry entry = (Map.Entry) o;
            System.out.printf("%s = %s%n", entry.getKey(), entry.getValue());
        }
    }
}
출처 : http://mylovely1004.tistory.com/11

ListView의 배경색을 변경 하였을때, 스크롤 할 경우 배경색이 검은색으로 변하게 됩니다.

이럴 경우 아래 항목을 xml 내에 추가하면 됩니다.

 

android:cacheColorHint="#00000000"

 

위와 같이 하면 스크롤 할 떄 배경색이 변하지 않습니다.

출처 : http://androidside.com/bbs/board.php?bo_table=B46&wr_id=13459

TITLE : Android Simple Comic Book App

Date : 2010/06/24

Writen by Dayyoung

Description :

This is Source that make Comic book App by extends Webview.

Download Link : CustomWebView

Reference Site: http://nocivus.posterous.com/double-clicktap-detection-on-a

안드로이드 에서 확대/축소를 가진 기능의 일반View가 없는 것으로 알고있다.

ImageView 같은 경우도 확대 축소에 대해서 Draw이벤트에서 직접 구현해 줘야하는데 상당히 번거롭다.

설령 확대축소 기능을 구현했다 하더라도  일반View의 경우 멀티스크롤을 지원하지 않기때문에 상당히 번거롭다.

특히 웹뷰를 이용할 경우 멀티터치가 지원되는 폰이라면 멀티터치 기능도 가능하다니 ~

그래서 웹뷰를 사용자 정의해서 쓰면 어떨까하는 생각에 구현해 보았고, 실행결과 생각보다 퍼포먼스가 좋아서 실무에 사용할 수 있을 듯 하다.

 오늘은 WebView를 이용한 안드로이드 만화책 APP 만들기 !

 1. 사용하고 싶은 이미지가 Embeded된 html 파일을 작성해서 Asset폴더에 넣는다.

2. MainActivity의 Oncreate부분에 WebView 부라우져를 호출한다. 

        browser = (WebView) findViewById(R.id.webView);   
        browser.getSettings().setJavascriptEnabled(true);    // 자바스크립트 사용 시 설정       
        browser.loadUrl(“file:///android_asset/test.htm“);  // 로컬 이미지 html 호출                    
        browser.setInitialScale(20); // 초반 스케일 사이즈

3.browser.setOnTouchListener에 더블클릭 리스너를 구현해서 ZoomIn() 호출한다. 

if( event.getAction() == MotionEvent.ACTION_DOWN)
   { //클릭 이벤트 발생
    
         long thisTime = System.currentTimeMillis(); 
         //눌린 시간
         if (thisTime – lastTouchTime < 250) {
          // Double tap 눌린시간 – 예전시간이 250ms 이내일 경우 더블클릭으로 인식      
          browser.zoomIn();
          browser.zoomIn();
          browser.zoomIn();
          //브라우져 확대 (3배확대)
          Toast.makeText(getApplicationContext(), “Zoom in”, 1).show();
         }          
         else {
          // Too slow :)           
         }

 

4.   browser.setOnLongClickListener에 롱프레스 이벤트를  구현해서 ZoomOut() 호출

@Override
   public boolean onLongClick(View v) {
    // TODO Auto-generated method stub    
      browser.zoomOut();
      browser.zoomOut();
      browser.zoomOut();
         //브라우져 축소 (3배축소)
      Toast.makeText(getApplicationContext(), “Zoom out!”, 1).show();
    return false;
   }
  });

여러 장으로 HTML 파일로 구성된 App을 구현할 경우 카탈로그,뷰어 등 다양한 용도로 응용이 가능할 것이


이미지 버튼 작성시 그냥 src하면 뒤에 테두리가 보인다..

그래서 지금까진 백그라운드로 걍 처리했었지만..

누르는 효과나 그런걸 기대못했기 때문에 짜증났었는데..

왠걸...걍 뒤에 투명색으로 하면 된다네...우왕..짜증남..ㅋㅋ

src="이미지 주소" 
background = "#00000000"

난 바보였어...T.T 
제목에 재사용이라고 썼지만 사실 재사용을 안하는 이야기를 하려고 합니다-_-a 대부분의 책들이 ProgressDialog의 경우 재사용 하는 예제를 싣고 있고 물론 작동을 잘 하고 사용에 아무런 문제가 없습니다.

하지만 여기서 겪는 문제가 있습니다. 스타일을 ProgressDialog.STYLE_SPINNER 쓸때 보면 다이얼로그의 재사용시에 스피너가 회전을 하지 않습니다. 이것 참 난감하더군요.

여기에 대해 다양한 방법론이 제시되곤 합니다. 쓰레드등을 사용하여 비동기로 띄운다거나..하지만 잘 생각해 보면 그냥 재사용 안하면 됩니다; 우리 Dalvik VM의 GC를 믿어보는거죠;

@Override
protected Dialog onCreateDialog(int id)
{
 
switch(id)
 
{
 
case DEFAULT_PROGRESS_BAR:
    dlgProgress
= new ProgressDialog(this);
    dlgProgress
.setMessage("Loading...");
    dlgProgress
.setIndeterminate(true);
    dlgProgress
.setProgressStyle(ProgressDialog.STYLE_SPINNER);
    dlgProgress
.setCancelable(false);
   
return dlgProgress;
 
}
 
 
return super.onCreateDialog(id);
}

이제 다이얼로그를 띄울때는 다음과 같이 showDialog를 사용하여 띄우면 되겠죠.

showDialog(DEFAULT_PROGRESS_BAR);

이제 다이얼로그를 닫을때 dismiss를 사용하실텐데 다음과 같이 removeDialog도 해주시기 바랍니다.
dlgProgress.dismiss();
removeDialog
(DEFAULT_PROGRESS_BAR);

이제 평소에 비해 좀 더 좋아보이시나요?
팁 참고 : http://crayon2k.blog.me/100118794092


출처 : http://godofcode.tistory.com/305

public
class MyCustomButton extends ImageButton {

final static int DEFAULT_IMAGE = R.drawable.default_button;

final static int DEFAULT_CLICK_IMAGE = R.drawable.default_button_click;

Drawable buttonImage; // 버튼 기본 이미지

Drawable clickImage; // 클릭 모션 이미지

public MyCustomButton(Context context, AttributeSet attrs) {

super(context, attrs);

buttonImage = getResources().getDrawable(DEFAULT_IMAGE);

clickImage = getResources().getDrawable(DEFAULT_CLICK_IMAGE);

setImageDrawable(buttonImage);

this.setOnTouchListener(new OnTouchListener(){

public boolean onTouch(View v, MotionEvent event) {

switch ( event.getAction() ){

case MotionEvent.ACTION_DOWN :

setImageDrawable( clickImage );

break;

case MotionEvent.ACTION_UP :

setImageDrawable ( buttonImage );

break;

}

return false;

}); 

}


public void SetButton( int buttonImageID , int clickImageID){

this.buttonImage = getResources().getDrawable(buttonImageID);

this.clickImage = getResources().getDrawable(clickImageID);

setImageDrawable(buttonImage);

}

}


<com.shinrolen.kr.MyCustomButton 

 android:paddingRight="10sp" 

 android:layout_width="fill_parent" 

 android:layout_height="wrap_content" 

 android:text="" 

 android:id="@+id/btn_exit" 

 android:background="@drawable/chat_out1" 

 android:scaleType="fitXY"

 android:gravity="center_horizontal"

/>


MyCustomButton connect_btn = (MyCustomButton)findViewById(R.id.btn_exit);

        connect_btn.SetButton(R.drawable.chat_out1, R.drawable.chat_out2);

        connect_btn.setOnClickListener(new View.OnClickListener(){


        @Override

public void onClick(View v) {

        if(v.getId() == R.id.btn_exit){

        Log.i(TAG2, "CustomButton");

        }

}

        });

출처 : http://mygirl2.blog.me/40105201905

 출처 : http://androidcore.com/index.php?option=com_content&view=article&id=235&Itemid=106

 




Making a custom Android button using a custom view

 

Creating a custom view is as simple as inheriting from the View class and overriding the methods that need to be overridden. In this example, a custom button is implemented in this way. The button shall feature a labelled image (i.e. an image with text underneath).

 

사용자 정의 뷰(View)를 이용하여 사용자 정의 버튼(안드로이드용) 만들어 보기

 

사용자 정의 뷰(View)를 생성하는 것은 뷰 클래스를 상속하는 것만큼 쉽다. 그리고 오버라이드(Override) 해야 할 메소드를 오버라이드하는 것 만큼...

의역하면, 뷰 클래스를 상속하고 오버라이드 해줘야 할 부분만 오버라이드 해주면 되니까 사용자 정의 뷰를 만드는 것이 굉장히 쉽다는 얘기이다.

예를 들면, 사용자 정의 버튼은 이런 방식으로 구현이 되는데... 다음의 예제에서는 사용자 정의 버튼이 라벨이 표시되는 이미지와 밑에 텍스트가 추가되는 특징을 갖게 된다. 그럼 예제를 보자...



1   public class CustomImageButton extends View {
2       private final static int WIDTH_PADDING = 8;
3       private final static int HEIGHT_PADDING = 10;
4       private final String label;
5       private final int imageResId;
6       private final Bitmap image;
7       private final InternalListener listenerAdapter = new InternalListener();
8
 

The constructor can take in the parameters to set the button image and label.

 

생성자는 버튼 이미지와 라벨을 설정하기 위해 파라미터를 받을 수 있다.


9       /**
10       * Constructor.
11       *

// 생성자


12       * @param context
13       *        Activity context in which the button view is being placed for.

 

// 버튼 뷰가 배치되는 액티비티 문맥(context)

 

14       *
15       * @param resImage
16       *        Image to put on the button. This image should have been placed
17       *        in the drawable resources directory.

// 버튼에 이미지를 추가하기. 이 이미지는 drawable 리소스 디렉토리에 배치(보관, 저장?)되어야 한다.

 

18       *
19       * @param label
20       *        The text label to display for the custom button.
21       */

 

// 사용자 정의 버튼에서 표시될 텍스트 라벨


22      public CustomImageButton(Context context, int resImage, String label)
23       {
24           super(context);
25           this.label = label;
26           this.imageResId = resImage;
27           this.image = BitmapFactory.decodeResource(context.getResources(),
28                  imageResId);
29 
30           setFocusable(true);
31           setBackgroundColor(Color.WHITE);
32
33           setOnClickListener(listenerAdapter);
34           setClickable(true);
35       }
36
 

With the constructor defined, there are a number of methods in the View class that needs to be overridden to this view behave like a button. Firstly, the onFocusChanged gets triggered when the focus moves onto or off the view. In the case of our custom button, we want the button to be “highlighted” when ever the focus is on the button.

 

생성자가 정의 되고 나서 뷰 클래스는 몇가지 메소드들이 있다는 것도 고려해야 한다. 그것은 뷰 클래스에서 버튼과 같은 뷰가 가져야 할 것을 오버라이드해줘야 한다는 것이다. (위에 서두에 언급했던 내용...)

 

첫번째로 onFocusChanged 메소드는 발동을 언제하냐면, 포커스가 On되거나 Off될때이다. 뷰에서 대한 포커스 말이다.

여기에서 언급되는 사용자 정의 버튼의 경우, 버튼이 하이라이트(highlighted)되는 기능이 필요한데, 포커스가 버튼에 맞춰졌을때(Focus On 되었을때) 말이다.

 


49          {
50              this.setBackgroundColor(Color.WHITE);
51          }
52      }
53     
 

The method responsible for rendering the contents of the view to the screen is the draw method. In this case, it handles placing the image and text label on to the custom view.

 

setBackgroundColor() 메소드는 뷰의 내용들을 표현하는 메소드인데, 스크린에 표현하게 된다. 이 메소드는 draw 메소드 계열이다.

여기서 소개된 이 메소드는 이미지와 텍스트 라벨을 사용자 정의 뷰에 배치하는 것을 조정(컨트롤)한다.

 


54      /**
55       * Method called on to render the view.
56       */
57      protected void onDraw(Canvas canvas)
58      {
59          Paint textPaint = new Paint();
60          textPaint.setColor(Color.BLACK);
61          canvas.drawBitmap(image, WIDTH_PADDING / 2, HEIGHT_PADDING / 2, null);
62          canvas.drawText(label, WIDTH_PADDING / 2, (HEIGHT_PADDING / 2) +
63                  image.getHeight() + 8, textPaint);
64      }
65     
 

For the elements to be displayed correctly on the screen, Android needs to know the how big the custom view is. This is done through overriding the onMeasure method. The measurement specification parameters represent dimension restrictions that are imposed by the parent view.

 

요소들(elements)들이 화면에 제대로 표현되기 위해서 안드로이드는 사용자 정의 뷰의 크기가 얼마나 되는지에 대한 정보를 필요로 한다.

아래의 경우 onMeasure 메소드를 오버라이딩한 것이다. measurement(측정) 상세 내용(스펙) 파라미터는 부모 뷰(View)로 부터 결정된(강요된? 강제로 결정된?) 치수 제한을 나타낸다.

 


 66        @Override
 67        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
 68        {
 69         setMeasuredDimension(measureWidth(widthMeasureSpec),
 70                 measureHeight(heightMeasureSpec));
 71        }
 72   
 

The call to setMeasuredDimension in the onMeasure method is important. The documentation states that the call is necessary to avoid a IllegalStateException.

 

onMeasure 메소드 내에서 setMeasuredDimension() 메소드를 호출하는 것은 중요하다. 그 이유는 illegalStateException이 발생하는 것을 피하기 위해서이다. 이 메소드의 호출이 말이다. (문서에 표시된 내용 - 어떤 문서?)

 


 73     private int measureWidth(int measureSpec)
 74     {
 75         int preferred = image.getWidth() * 2;
 76         return getMeasurement(measureSpec, preferred);
 77     }
 78   
 79     private int measureHeight(int measureSpec)
 80     {
 81         int preferred = image.getHeight() * 2;
 82         return getMeasurement(measureSpec, preferred);
 83     }
 84   
 

To calculate the width and height measurements, I’ve chosen to keep the logic simple by using a simple formula to calculate the dimensions. This simple formula computes the dimensions based on the dimensions of the image. The measureSpec parameter specifies what restrictions are imposed by the parent layout.

 

가로폭과 세로높이 측정값을 계산하기 위해서, 이 글을 쓴 사람은 치수를 계산 하기 위한(할 수 있는) 간단한 공식을 이용했고 그렇게 한 이유는 프로그램 로직을 간결하게 유지하기 위한 방법이라고 생각했기 때문이다.

이 간단한 공식은 이미지의 치수에 기초(기반)을 둔 치수를 산출한다.(compute 뜻 까먹었었는데, 긴가민가했는데 찍었는데 맞췄다. ㅇㅇ)

measureSpec 파라미터는 부모 레이아웃으로 강요된 제한들을 명시한다.

 


 85     private int getMeasurement(int measureSpec, int preferred)
 86     {
 87         int specSize = MeasureSpec.getSize(measureSpec);
 88         int measurement = 0;
 89       
 90         switch(MeasureSpec.getMode(measureSpec))
 91         {
 92             case MeasureSpec.EXACTLY:
 93                 // This means the width of this view has been given.
 

// 이것은 주어진 뷰(View)의 가로폭을 의미한다.

 

 94                 measurement = specSize;
 95                 break;
 96             case MeasureSpec.AT_MOST:
 97                 // Take the minimum of the preferred size and what
 98                 // we were told to be. 

 

// 원하는 크기(size)의 최소값을 얻어라

// 뭔가 되기 위해서? 되어햐 할 것들에 대해 듣는 것.. (코드를 봐야 이해할 것 같다. 일단 나중에..)

 

 99                 measurement = Math.min(preferred, specSize);
100                 break;
101             default:
102                 measurement = preferred;
103                 break;
104         }
105   
106         return measurement;
107     }
108
 

To make the customised button useful, it needs to trigger some kind of action when it is clicked (i.e. a listener). The view class already defines methods for setting the listener, but a more specialised listener could be better suited to the custom button. For example, the specialised listener could pass back information on the instance of the custom button.

 

사용자 정의 버튼을 유용하게 만들기 위해서는 사용자 버튼에 몇가지 종류의 액션(Action)을 발동 시키는 것이 필요한데, 예를들어 리스너(listener)등으로 클릭했을 때 말이다.

뷰(View) 클래스의 경우 이미 리스너 설정에 필요한 메소드들을 정의되어 있다.

하지만 좀 더 특별하고 뛰어나다고 할까? 이러한 리스너는 사용자 정의 버튼에 좀 더 최적화 될 수 있다.

예를 들면, 이 특별한 리스너는 정보를 돌려줄 수 있는데, 사용자 정의 버튼의 객체(instance)에 말이다.


109     /**
110      * Sets the listener object that is triggered when the view is clicked.

 

// 리스너 객체를 설정하라. 뷰가 클릭되었을때 발동되는 리스너 객체 말이다.

 

111      *
112      * @param newListener
113      *        The instance of the listener to trigger.

 

// 발동되는 리스너 객체..

 

114      */
115     public void setOnClickListener(ClickListener newListener)
116     {
117         listenerAdapter.setListener(newListener);
118     }
119   
 

If the custom listener passes information about this instance of the custom button, it may as well have accessors so listener implementation can get useful information about this custom button.

 

사용자 정의 리스너가 사용자 정의 버튼 객체에 대한 정보를 제공한다면, 사용자 정의 리스너는 접근자를 가질지도 모른다. 리스너 구현하는 것이 사용자 정의 버튼에 대한 유용한 정보를 제공 할 수 있도록 말이다.

(의역하면, 사용자 정의 리스너가 사용자 정의 버튼 객체에 대해 뭔가를 제공한다면 전달하거나... 그러면 구현하는 리스너가 사용자 정의 버튼에 대해 유용한 정보를 가질 수 있도록 접근자를 가질지도 모른다는 의미가 될 것 같다. 코드를 안 보고 번역하니 헤깔린다.)


120     /**
121      * Returns the label of the button.

 

// 라벨 버튼을 반환하기


122      */
123     public String getLabel()
124     {
125         return label;
126     }
127   
128     /**
129      * Returns the resource id of the image.
130      */

 

// 이미지의 리소스 아이디(id)를 반환하기


131     public int getImageResId()
132     {
133         return imageResId;
134     }
135   
 

Finally, for our custom button class that is using a custom listener, the custom listener class needs to be defined.

 

마지막으로 사용자 정의 리스너를 사용하고 있는 여기에 소개된 사용자 정의 버튼에 대해 사용자 정의 리스너 클래스가 정의되어야 한다는 것을 말해두고 싶다.

 


136     /**
137      * Internal click listener class. Translates a view’s click listener to
138      * one that is more appropriate for the custom image button class.

 

/* 내부의 클릭 리스너 클래스(이것이 익명 클래스라고 불리기도 하는건가? 안 찾아봐서 잘 모르겠다.)

뷰(View)의 클래스를 전환(변환)해야 하는데, 사용자 정의 버튼 클래스에 좀 더 적합한 것으로 말이다.

(위에 설명된 translate라는 단어는 번역이라기 보다는 전환(변환)이 맞을 것 같다.) */

 

 

139      *
140      * @author Kah
141      */
142     private class InternalListener implements View.OnClickListener
143     {
144         private ClickListener listener = null;
145   
146         /**
147          * Changes the listener to the given listener.

 

리스너를 주어진 리스너로 변경해라


148          *
149          * @param newListener
150          *        The listener to change to.

 

바꿀 리스너

 

151          */
152         public void setListener(ClickListener newListener)
153         {
154             listener = newListener;
155         }
156       
157         @Override
158         public void onClick(View v)
159         {
160             if (listener != null)
161             {
162                 listener.onClick(CustomImageButton.this);
163             }
164         }
165     }
166    }
167  

 

Source: kahdev blog

{mos_fb_discuss:18}

 

소스 : 카데브 블로그(kahdev blog)

private Bitmap overlayMark(Bitmap bmp1, Bitmap bmp2) 
{
   
Bitmap bmOverlay = Bitmap.createBitmap(bmp1.getWidth(), bmp1.getHeight(), bmp1.getConfig());
   
Canvas canvas = new Canvas(bmOverlay);
    canvas
.drawBitmap(bmp1, 0, 0, null);
    canvas
.drawBitmap(bmp2, distanceLeft, distanceTop, null);
   
return bmOverlay;
}

출처 : http://susemi99.tistory.com/683
 
C2DM 소개


http://code.google.com/intl/ko-KR/android/c2dm/
 



등록

http://code.google.com/intl/ko-KR/android/c2dm/signup.html 에서 가입을 하고나면 해당 메일 주소로 메일이 하나 온다.



AUTH 정보 받기

?
Auth_android.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
 
 
public class Auth_android
{
    private static String HOST = "https://www.google.com/accounts/ClientLogin";
    private static String EMAIL = "메일주소";
    private static String PASS = "메일주소 비밀번호";
    private static String SOURCE = "알아보기 쉽게 적힌 프로그램 정보(예 : company-project-version. apple-iphone-4.2.1)";
     
 
    public  static void main( String[] args ) throws Exception
    {
        try {
            StringBuffer postDataBuilder = new StringBuffer();
            postDataBuilder.append("Email=" + EMAIL);
            postDataBuilder.append("&Passwd=" + PASS);
            postDataBuilder.append("&accountType=GOOGLE");
            postDataBuilder.append("&source=" + SOURCE);
            postDataBuilder.append("&service=ac2dm");
             
            byte[] postData = postDataBuilder.toString().getBytes("UTF8");
             
            URL url = new URL(HOST);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
             
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            conn.setRequestProperty("Content-Length",Integer.toString(postData.length));
            OutputStream out = conn.getOutputStream();
            out.write(postData);
            out.close();
            BufferedReader in = new BufferedReader(   new InputStreamReader(conn.getInputStream()));
 
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
             System.out.println(inputLine);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }   
}
 
 
 
 
// 결과
SID=DQAAAL0AAADVvGAVXO.......
LSID=DQAAAL8AAAAp5iSaE8h.........
Auth=DQAAAL8AAADrYkFbucd............// 요것만 사용함



registration_id 받기

?
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="utf-8"?>
      package="mint.TestC2dm"
      android:versionCode="1"
      android:versionName="1.0"
      android:minSdkVersion="8">
       
    <permission android:name="mint.TestC2dm.permission.C2D_MESSAGE" android:protectionLevel="signature" />
    <uses-permission android:name="mint.TestC2dm.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     
     
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".TestC2dm"
                android:theme="@android:style/Theme.Black.NoTitleBar"
                android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 
        <receiver android:name=".C2DMReceiver"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="mint.TestC2dm" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="mint.TestC2dm" />
            </intent-filter>
        </receiver>
         
        <activity android:name=".showMsg"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent"
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.AlertDialogs" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>



?
TestC2dm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package mint.TestC2dm;
 
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
 
public class TestC2dm extends Activity {
     
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
     
    // 등록
    public void onRegist(View v)
    {
        Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
        registrationIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0));
        registrationIntent.putExtra("sender", "메일주소");
        startService(registrationIntent);
    }
  
    // 해지  
    public void onUnregist(View v)
    {
        Intent unregIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
        unregIntent.putExtra("app", PendingIntent.getBroadcast(this, 0, new Intent(), 0));
        startService(unregIntent);
    }
}



?
main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="가입"
        android:onClick="onRegist"/>
    <Button 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="해지"
        android:onClick="onUnregist"/>
</LinearLayout>l



C2DMReceiver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package mint.TestC2dm;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
 
public class C2DMReceiver extends BroadcastReceiver {
 
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e("###############", "onReceive");
        if (intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")) {
            handleRegistration(context, intent);
        } else if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) {
            handleMessage(context, intent);
         }
     }
 
    private void handleRegistration(Context context, Intent intent) {
        Log.e("###############", "handleRegistration");
        String registration = intent.getStringExtra("registration_id");
        if (intent.getStringExtra("error") != null) {
            // Registration failed, should try again later.
        } else if (intent.getStringExtra("unregistered") != null) {
            Log.e("@@@@@@@@unregistered", "unregistered");
        } else if (registration != null) {
           // Send the registration ID to the 3rd party site that is sending the messages.
           // This should be done in a separate thread.
           // When done, remember that all registration is done.
            Log.e("@@@@@@@@registration_id", registration);
        }
    }
     
    private void handleMessage(Context context, Intent intent) {
        Log.e("###############", "handleMessage");
        String title = intent.getStringExtra("title");
        String msg = intent.getStringExtra("msg");
         
    // 화면 깨우기
        PushWakeLock.acquireCpuWakeLock(context);
 
        Intent i = new Intent(context, showMsg.class);
        Bundle b = new Bundle();
        b.putString("title", title);
        b.putString("msg", msg);
        i.putExtras(b);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(i);
    }
}



PushWakeLock.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package mint.TestC2dm;
 
import android.app.KeyguardManager;
import android.content.Context;
import android.os.PowerManager;
import android.util.Log;
 
class PushWakeLock {
 
    private static PowerManager.WakeLock sCpuWakeLock;
    private static KeyguardManager.KeyguardLock mKeyguardLock;
    private static boolean isScreenLock;
 
    static void acquireCpuWakeLock(Context context) {
        Log.e("PushWakeLock", "Acquiring cpu wake lock");
        if (sCpuWakeLock != null) {
            return;
        }
 
        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
 
        sCpuWakeLock = pm.newWakeLock(
                PowerManager.SCREEN_BRIGHT_WAKE_LOCK |
                PowerManager.ACQUIRE_CAUSES_WAKEUP |
                PowerManager.ON_AFTER_RELEASE, "I'm your father");
        sCpuWakeLock.acquire();
         
//        KeyguardManager km = (KeyguardManager)context.getSystemService(context.KEYGUARD_SERVICE);
//        mKeyguardLock = km.newKeyguardLock("key guard");
//        if (km.inKeyguardRestrictedInputMode()) {
//        mKeyguardLock.disableKeyguard();
//          isScreenLock = true;
//        } else {
//          isScreenLock = false;
//        }
 
    }
 
    static void releaseCpuLock() {
        Log.e("PushWakeLock", "Releasing cpu wake lock");
//        if (isScreenLock) {
//          mKeyguardLock.reenableKeyguard();
//          isScreenLock = false;
//      }
 
        if (sCpuWakeLock != null) {
            sCpuWakeLock.release();
            sCpuWakeLock = null;
        }
    }
}

?
showMsg.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package mint.TestC2dm;
 
 
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
 
public class showMsg extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
         
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
         
        String title, msg;
        Bundle bun = getIntent().getExtras();
        title = bun.getString("title");
        msg = bun.getString("msg");
         
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(showMsg.this);
         
        alertDialog.setPositiveButton("닫기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                PushWakeLock.releaseCpuLock();
                showMsg.this.finish();
            }
        });
         
        alertDialog.setNegativeButton("보기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                startActivity(new Intent().setClassName(getPackageName(), getPackageName()+".TestC2dm"));
                PushWakeLock.releaseCpuLock();
                showMsg.this.finish();
            }
        });
         
        alertDialog.setTitle(title);
        alertDialog.setMessage(msg);
        alertDialog.show();
 
        // 폰 설정의 조명시간을 가져와서 해당 시간만큼만 화면을 켠다.
        int defTimeOut = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_OFF_TIMEOUT, 15000);
        TimerTask task = new TimerTask() {
                 @Override
                public void run() {
                        PushWakeLock.releaseCpuLock();
                }
        };
             
        Timer timer = new Timer();
        timer.schedule(task, defTimeOut);
    }
}

?
등록 결과
1
2
3
###############: onReceive
###############: handleRegistration
registration_id: APA91bGxex5sJi5hbeQkGUaURZo8.......





메세지 보내기

?
Push.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
 
 
public class Push
{
    private static String HOST = "http://android.apis.google.com/c2dm/send";
    private static String AUTH = "DQAAAL8AAADrYkFbucd............";
     
    // DB에 저장된 디바이스 토큰 목록
    private static String[] arrId =
    {
        "APA91bGxex5sJi5hbeQkGUaURZo8......."
//      "APA91bHCRg6NhgMYv8Rbb2LVCoj4al......."
    };
     
 
    public  static void main( String[] args ) throws Exception
    {
        for (int i=0; i<arrId.length; i++)
        {
            androidPush(arrId[i], "제목", "내용");
        }
    }
     
    public static void androidPush(String  regId, String title, String msg) throws Exception {
        try {
            StringBuffer postDataBuilder = new StringBuffer();
            postDataBuilder.append("registration_id=" + regId); // 등록ID
            postDataBuilder.append("&collapse_key=1");
            postDataBuilder.append("&delay_while_idle=1");
            postDataBuilder.append("&data.title=" + URLEncoder.encode(title, "UTF-8")); // 제목
            postDataBuilder.append("&data.msg=" + URLEncoder.encode(msg, "UTF-8")); // 내용
             
            byte[] postData = postDataBuilder.toString().getBytes("UTF8");
             
            URL url = new URL(HOST);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
             
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            conn.setRequestProperty("Content-Length",Integer.toString(postData.length));
            conn.setRequestProperty("Authorization", "GoogleLogin auth="+AUTH);
            OutputStream out = conn.getOutputStream();
            out.write(postData);
            out.close();
            conn.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }   
}



결과




문제점

- 메세지는 한번에 하나의 폰에만 전송 가능(해당 registration_id를 가진 폰에만 전송)=>그래서 loop를 돌아야한다
- 가끔씩 엄청 느리게 오는 메세지가 있다.(잘 되다가도 10분후에 메세지가 오는 경우도 있다)
- 폰에 구글계정을 등록해놔야한다


장점

- 한번 구독을 하면 앱이 실행되고있지 않아도 구독이 가능(재부팅 후 앱 실행없이도 정상 작동)

정말 초간단한 구글 위치 찾기 입니다.

보통 몇시간이면 만들수 있는 어플이라 백업용으로 올림

 

출처 : http://lomohome.com/316

원래 하나은행 스마트폰 뱅킹의 위치기반(LBS) 지점찾기는 WebView 에서 Google Map API 를 통하여 구현이 되어있었다.

아이폰에서는 이게 잘 돌아가는데... 안드로이드에서는 기계마다 되는것도 있고, 안되는것도 있고..

영 껄쩍지근 했다. (사실 이번에 출시한 갤럭시 S 에서 안돌아가는 이유가 가장 컸지..)


그래서 내친김에 WebView 에서 구현하지말고 MapView 로 구현해버리기로 했다.

이틀정도 작업한거라 고쳐야할 부분도 많고 (특히 Runnable 로 구현한 길게 누르기는...) 버그도 좀 있지만

일단 돌아가니, 이제까지 한것을 까먹지 않으려고 블로그에 정리를 해 둔다.


* OSX 의 Pages 를 이용하여 블로그 글을 정리했는데.. 웹으로 카피하니까, 이게 폰트 색 정의 해둔것이 죄다 깨졌다.

  감안해서 참고하시길.. 마지막에 PDF 로 첨부해둔다..



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/maps-api-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 {


@Override

public void onLocationChanged(Location location) {

//위치 이동이 발견되었을때 호출될 메소드.

//위의 설정에서 10초마다 100미터 이상 이동이 발견되면 호출된다.

updateOverlay(location);

}


@Override

public void onProviderDisabled(String provider) {

Log.d(LOG_TAG"GPS disabled : " + provider); 

}


@Override

public void onProviderEnabled(String provider) {

Log.d(LOG_TAG"GPS Enabled : " + provider);

}


@Override

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<OverlayItemoverlays;


public LocationItemizedOverlay(Drawable defaultMarker) { //오버레이 생성자

//마커 이미지의 가운데 아랫부분이 마커에서 표시하는 포인트가 되게 한다.

super(boundCenterBottom(defaultMarker)); 

overlays = new ArrayList<OverlayItem>();

}


@Override

protected OverlayItem createItem(int i) {

return overlays.get(i);

}


@Override

public int size() {

return overlays.size();

}


public void addOverlay(OverlayItem overlay) {

overlays.add(overlay);

//null pointer 버그때문에 오버레이 아이템 추가후 가능한 빨리 populate 해줘야한다.

populate(); 

}


@Override

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 latlng;

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(미터)이다.

}


@Override

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);

}

}

};


@Override

public boolean onTouchEvent(MotionEvent event, MapView mapView) {

if (gestureDetector.onTouchEvent(event)) {

return true;

}


onLongPress(event);

return false;

}


@Override

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;

}


@Override

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,

float velocityY) {

if (onGestureListener != null) {

return onGestureListener.onFling(e1, e2, velocityX, velocityY);

}

return false;

}


@Override

public void onLongPress(MotionEvent e) {

if (onGestureListener != null) {

onGestureListener.onLongPress(e);

}


//화면을 누르고 있으면 onLongPress 가 호출되는데 호출될때마다 체크할 시간을 변수에 넣는다.

//이부분이 퍼포먼스 하락에 영향을 줄 것 같다.

globalEvent = e;

longPressTime = System.currentTimeMillis();


}


@Override

public boolean onScroll(MotionEvent e1, MotionEvent e2,

float distanceX, float distanceY) {

if (onGestureListener != null) {

onGestureListener.onScroll(e1, e2, distanceX, distanceY);

}

return false;

}


@Override

public void onShowPress(MotionEvent e) {

if (onGestureListener != null) {

onGestureListener.onShowPress(e);

}

}


@Override

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;

}


}


완성된 지점찾기의 동작모습.

액티비티를 실행하게 되면 다음과 같이 작동한다.


실행하게 되면 인트로로 다이얼로그를 하나 띄워준다.



현재 위치가 표시되고 현위치 주변의 지점들을 마커로 표시해준다.



확대 축소 컨트롤은 기기에 마다 내장되어있는 디자인에 다르게 표시된다.



마커를 누르게 되면 간단한 지점 정보 다이얼로그가 뜬다.



메뉴 버튼을 누르면 지점, ATM 찾기를 선택할수 있고, 현위치 메뉴를 선택하면 화면을 다시 현위치로 옮겨준다.



화면을 줌아웃 시키고, ATM 찾기로 옵션을 변경시켜보았다.



화면의 특정지점을 누르고 있으면 그 지점을 기준으로 다시 검색을 해온다.



마지막으로 우리동네도 한번 검색해봤다.





* 추가. 2010-7-13

더블탭시 화면 확대와 화면 스크롤시 길게 누르기 취소를 하기 위하여 다음 부분을 추가.


public class MapTouchDetectorOverlay extends Overlay implements
            OnGestureListener , OnDoubleTapListener{


....


@Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            
            // for Cancle detect loooong touch
            touchStartTime = System.currentTimeMillis() + 5000;
            
            if (onGestureListener != null) {
                onGestureListener.onScroll(e1, e2, distanceX, distanceY);
            }
            return false;
        }


.....


@Override
        public boolean onDoubleTap(MotionEvent e) {
            mapController.zoomIn();
            return false;
        }


.....


}



* 메일주소 등을 적으면서 소스를 달라고 하는 리플들을 보면 눈살이 많이 찌푸려집니다.

본문을 찬찬히 읽어보시고 궁금한점이나 보완해야할점, 토의하고 싶으신 점이 있다면 저도 즐겁게 리플을 달겠지요..

앞으로 소스를 달라고 하는류의 리플은 그냥 제 블로그에서 삭제하도록 하겠습니다.

모쪼록 양해 부탁드립니다.

구글맵용 파라미터를 만들어서 (http://www.querystring.org/google-maps/google-maps-query-string-parametersprivate )List<NameValuePair> params;
   params.add(new BasicNameValuePair("near", "서울특별시 어쩌구 저쩌동");
   params.add(new BasicNameValuePair("q", "철물점"));
   params.add(new BasicNameValuePair("radius", "2.485484")); //마일로 표시된 반경
   params.add(new BasicNameValuePair("mrt", "yp")); //검색오션. 업체
   params.add(new BasicNameValuePair("start", start)); //검색결과중 처음에 보여줄 시작페이지
등등등...

위에서 만든 파라미터를 넣어 uri를 만들고
URI uri = URIUtils.createURI("http", "maps.google.co.kr", -1, "/m/local", URLEncodedUtils
    .format(params, "UTF-8"), null); //  "/m/local" 는 모바일용 지역정보를 결과로 받겠다는. js는 자바스크립용.  kml은 구글어스용.

uri를 이용하여 결과를 스트링형태로 얻어서
HttpGet getMethod=new HttpGet();
getMethod.setURI(uri);
ResponseHandler<String> responseHandler = new BasicResponseHandler();
  String responseBody = client.execute(getMethod, responseHandler);

결과로 받은 스트링( responseBody )에서 정규식으로 원하는 부분(전화번호, 업체명 주소 등등) 을 찾아 이용한다.

출처 : http://moonset.tistory.com/102

이번 Project를 하면서 Open Source를 많이 이용하게 됐다.

( = 저가의 생산비용으로 개발자 힘들게 하는 일.)


Linux , Apache, Weblogic, Oracle

파일 업로드 컴포넌트도 없고, 에디터 또한 FCKEditor라는 처음 들어본 것을 사용했다.


이러한 상황에서...

지도 및 차트를 그리는 부분이 있었는데..

고객같은 PM님께서 꼭 Google Code API를 사용해야 한다고 주장하셨다.


Map API는 네이버, 다음, 구글 등에서 제공하고 있다.

구글은 오래되긴 했지만, 영어라는 한계가 있고, 로컬(미국)에서만 되는 기능들이 있다.

(place.AddressDetails.Country.AdministrativeArea 등으로 이루어진 상세 주소를 제공하지 않는다. 

따라서 zipcode 및 기타 정보를 가져올 수 없었다. 

Zipcode는 DB에서 가져왔으며, 기타 정보는 for문 돌려서 잘랐다.)

그런 기능을 제외하면 무난하게 사용할 수 있다.

(단, 가이드가 영어라 읽어 내려가기 귀찮은 부분 있음)



* 시작하기 *

1. Google Map API에서 '키'를 받아야 한다.

   (http://code.google.com/intl/ko-KR/apis/maps/index.html)


2. google 계정이 있으면 쉽게 받을 수 있으나, 클릭수 제한이 있다.

   (50,000번으로 개인이 사용하기에는 무리 없음)


3. 해당 서비스는 무료로 제공하고, 로고 및 맵 지우면 안된다.


4. 또한 사용하는 url에 따라 받아야 하므로, 생각해서 받으시길...

(localhost 와 test.com 다른 key 필요)



* 사용하기 *

검색하면 해당 내용으로 블로깅 한 곳이 많으니 참고하면 된다.


- referencd -

http://code.google.com/intl/ko-KR/apis/maps/documentation/javascript/v2/reference.html

http://code.google.com/intl/ko-KR/apis/maps/documentation/javascript/reference.html


- geocode -

http://code.google.com/intl/ko-KR/apis/maps/documentation/mapplets/services.html


- geocode  detail-

http://www.developer.com/tech/article.php/3615681/Introducing-Googles-Geocoding-Service.htm


- db에서 가져오기 -

http://code.google.com/intl/ko/apis/maps/articles/phpsqlajax.html



* 활용하기 *


- 주소 찾기 -

http://maps.google.com/maps?q=37.484530070872005,127.04534322023392

(latlng로 찾기)


http://maps.google.com/maps?q=37.484530070872005,127.04534322023392&gl=KR&output=xml&key=본인키값넣기&oe=utf-8

(본인키값 및 latlng로 찾기)


http://maps.google.com/maps/api/geocode/json?latlng=37.484530070872005,127.04534322023392 

(json으로 받기)


http://maps.google.com/maps/api/geocode/xml?latlng=37.484530070872005,127.04536736011505&sensor=false&oe=utf-8

(xml로 받기)



* Sample *


간단하게 주소입력 페이지를 만들었다. (IE만 지원됨. Firefox 및 safari 지원X)

zipcode만 추가하면... 클릭하여 입력가능한 페이지가 생성된다.

소스는 첨부파일 참고 (key는 localhost로 받았음) 

 test.html



- 사용 (Firefox에서는 map 부분 이상이 있으므로 IE를 권장) -

방법 1. 각 항목에 주소를 넣고 '검색'을 누른다.

방법 2. 지도를 클릭한다.


 * gCode 확인 *

시 / 도

경기도

구 / 군

구리시

동(읍/면/리)

인창동

기타주소

678-3 검색

 

Longitude

127.1439294

Latitude

37.6031857





* JAVA로는 어떻게? *


사실, 우리가 필요한 정보는 x,y좌표다.

그리고 일을 하면서 꼭 javascript로만 처리할수도 없다.

(file upload를 이용하여 해당 내용을 읽어 처리하는 경우가 생겨버렸다.)


그렇게 삽질은 시작되었고.. 찾았다.

'개발(開)'을 '개발()'로 해서... 소스는 정제되니 않았으니, 필요한 부분을 수정필요하다.

 (소스는 아래 참고)

 AddressBean.java



사용방법은 javascript와 별반 다르지 않다.

다만... 해당 값을 가져오는데 시간이 필요하기도 하고, 오류 정보를 받는대도 시간이 걸린다.

1. time wait를 건다.

2. 해당 값 체크를 하여 Retry 해야한다.



############################################################################

###############                                                                ##################

###############   1. 파일은 UTF-8로 저장되어 있음.              ##################

###############   2. Key 는 'localhost'로 받았음.                 ##################

###############   3. time out 및 retry 고려되지 않음.          ##################

###############                                                                 ##################

############################################################################

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.net.URL;

import java.net.URLEncoder;

import java.util.HashMap;

import org.json.JSONArray;

import org.json.JSONObject;

import com.ucube.common.util.Log;


public class AddressBean {

 public HashMap getLatLngByAddress(String address, String gmapKey)throws Exception {

  HashMap addrHashmap = new HashMap();

  

  //String address = "서울 강남구 도곡동 41";

  //String gmapKey = "ABQIAAAAzr2EBOXUKnm_jVnk0OJI7xSosDVG8KKPE1-m51RBrvYughuyMxQ-i1QfUnH94QxWIa6N4U6MouMmBA";  

  String BASE_GEOCODER_URL = "http://maps.google.com/maps/geo?";

  String ENCODING = "UTF-8";

  

  String GOOGLE_MAPS_KEY = gmapKey;

  String OUTPUT_FORMAT = "json";

  String GEOCODER_REQUEST = 

            BASE_GEOCODER_URL +

            "oe=utf-8&hl=ko&q=" + URLEncoder.encode(address, ENCODING) +

            "&key=" + GOOGLE_MAPS_KEY +

            "&output=" + OUTPUT_FORMAT;    

   

        BufferedReader reader = new BufferedReader(

                new InputStreamReader(

                    new URL(GEOCODER_REQUEST).openStream()));  

        String line = "";

        String jsondata = "";

        

        while ((line = reader.readLine()) != null) {

         jsondata += line;         

        }

        Log.debug.printDesc("@@@@@@@@@@@@@@  AddressBean getLatLngByAddress  jsondata  @@@@@@@@@@@@@@" + jsondata );

        try{

         JSONObject jsonObj2 = new JSONObject(jsondata);

         JSONArray jarray = jsonObj2.getJSONArray("Placemark");

          

        JSONArray jsonObj5 = jarray.getJSONObject(0).getJSONObject("Point").getJSONArray("coordinates");

        Log.debug.printDesc("@@@@@@@@@@@@@@  AddressBean getLatLngByAddress  jsonObj5.getString(0)   =    "    +  jsonObj5.getString(0));

        Log.debug.printDesc("@@@@@@@@@@@@@@  AddressBean getLatLngByAddress  jsonObj5.getString(1)   =    "    +  jsonObj5.getString(1));

        addrHashmap.put("x", jsonObj5.getString(0));

        addrHashmap.put("y", jsonObj5.getString(1));

        addrHashmap.put("result","1");

        }catch(Exception e){

         System.out.println("AddressBean getLatLngByAddress  Exception  = ============  " + e.toString());

         addrHashmap.put("result","0");

        }finally{

         return addrHashmap;

        }

}

출처 : http://gxgsung.blog.me/140120805038

안드로이드 SDK 를 이용하여 GPS 위치정보를 쉽게 구하는 방법입니다.

LocationManager 를 호출하여 GPS와 기지국에서 제공하는 위치정보를 얻어옵니다.

Geocoder 를 활용하여 주소를 가져옵니다.

이 예제는 소스코드가 첨부되어 있습니다.

 

Java 소스

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // LocationListener의 핸들을 얻음
        locManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

        // GPS로 부터 위치정보를 업데이트 요청
        locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
        // 기지국으로 부터 위치정보를 업데이트 요청
        //locManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 0, this);

        // 주소를 확인하기 위한 Geocoder KOREA 와 KOREAN 둘다 가능
        geoCoder = new Geocoder(this, Locale.KOREAN);
   }

 

   public void onLocationChanged(Location location) {
       Log.d("location", "location changed");
       myLocation = location;
   }

 

   public void GetLocations() {

      StringBuffer juso = new StringBuffer();

      latPoint = myLocation.getLatitude();
      lngPoint = myLocation.getLongitude();
      speed = (float)(myLocation.getSpeed() * 3.6);

      try {
         // 위도,경도를 이용하여 현재 위치의 주소를 가져온다.
         List<Address> addresses;
         addresses = geoCoder.getFromLocation(latPoint, lngPoint, 1);
         for(Address addr: addresses){
           int index = addr.getMaxAddressLineIndex();
           for(int i=0;i<=index;i++){
             juso.append(addr.getAddressLine(i));
             juso.append(" ");
           }
           juso.append("\n");
         }
      } catch (IOException e) {
        e.printStackTrace();
      }

   }

 

 

main.xml

  <TextView 
     android:id="@+id/lblLatitude"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:text="Latitude:" />
 <TextView
     android:id="@+id/tvLatitude" 
     android:layout_width="fill_parent"
     android:layout_height="wrap_content" />
 <TextView 
     android:id="@+id/lblLongitude"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:text="Longitude:" />
 <TextView 
     android:id="@+id/tvLongitude"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"/>    

 

AndroidManifest.xml

  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
  <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"></uses-permission>

* 위치정보를 사용하기위해서는 위와같이 권한을 주어야 합니다.

 

 

에뮬레이터로 테스트 하려면 이클립스의 Open Perspective 에서 DDMS 를 선택하고


 

좌표를 입력후 Send 버튼을 눌러주면 애뮬레이터로 좌표가 전송됩니다.


 

실제폰에서도 좌표가 정상 표시됩니다.



 

첨부해놓은 LocationDemo.apk 파일을 안드로이드 단말에 설치하여 테스트해 보실수 있습니다.

안드로이드 2.1 이상의 OS 에서 동작합니다.

출처 : http://blog.naver.com/hyuki0920?Redirect=Log&logNo=140111581692

이번 안드로이드 프로젝트를 진행하면서 필요한 기능 중 하나가 사용자 주변검색을 하는 기능이다.

이 기능을 구현하기 위해 목표로 한 것이 구글에서 제공하는 지역검색을 사용하여 지역정보를 얻어 오는 것이 였다.

그래서 발견한 사이트가 http://stackoverflow.com/questions/2499324/google-search-api-for-android-systems 여기다

사실 구글에서 android google search api 라고 치면 가장 위에 나오는 사이트다. ㅎ

사이트를 URL을 이용하여 google 서비스에 접근해서 원하는 결과(주변검색결과)를 받아 오는 예제를 얻을 수 있다.

이 예제를 수정하여 완벽하진 않지만 원하는 결과물을 얻을 수 있었다.

 

우선 Google Search API에 접근할 URL을 생성해야 한다.

로컬검색을 위해서는

"http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(검색어, "UTF-8")+"&sll="+위도+경도

위와 같이 URL을 만들어야 한다. 여기서 sll뒤의 위도 경도는 현재위치, 주변검색의 중심이 되는 위치를 말한다.

만약 에뮬레이터가 아닌 디바이스를 사용한다면 디바이스의 GPS를 이용하여 현재 위치를 얻어 값을 넣어주면 된다.

google search API에 대한 자세한 내용은 http://code.google.com/intl/ko/apis/ajaxsearch/ 이곳에서 확인 할 수 있다.

 

다음은 URL을 생성하고 그 URL을 이용하여 검색결과를 받아오는 코드이다.

  StringBuilder responseBuilder = new StringBuilder();
     // URL을 이용하여 검색신청을 하고, 결과를 받아 responseBuilder에 저장한다.
     try {
      // 검색을 위한 URL 생성
      URL url = new 
      URL("http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(search, "UTF-8")
       + "&sll="+currentLocation[0]+","+currentLocation[1]
       + "&hl=kr");
      BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
      String inputLine;
      while ((inputLine = in.readLine()) != null) {
       responseBuilder.append(inputLine);
      }
      in.close();
     } catch (MalformedURLException me) {
      me.printStackTrace();
     } catch (UnsupportedEncodingException ue) {
      ue.printStackTrace();
     } catch (IOException ie) {
      ie.printStackTrace();
     }

URL을 생성하고, stream을 열면 결과를 받아온다. (사실 자세한건 모른다... 하하;;)

 

위의 코드의 결과값, 즉 responseBuilder 에 저장되는 문자열이

{"responseData": {"results":[{"GsearchResultClass":"GlocalSearch","viewportmode":"computed","listingType" ..........

이런 식으로 나오게 된다.(JavaScript 라는데.......)

그리고 이 것을 가지고 원하는 결과값을 얻기 위해서는 JSON(JavaScript Object Notationo)를 이용해 parsing을 해야한다.

JSON이.. 무엇인지는 http://www.json.org/ (영어)  http://www.json.org/json-ko.html (한국어) 이 곳을 들어가면 알 수 있다.

 

음... 안드로이드에서는 JSONObject 를 기본적으로 제공하여 이것을 사용하면 된다...

 

JSONObject를 생성하는 것은 간단하다.

JSONObject json = new JSONObject(responseBuilder.toString());

이렇게 URL을 통해 얻은 결과를 String으로 건내주어 생성하면 된다.

JSONObject를 생성한 다음에는 Key를 이용하여 원하는 결과를 Parsing을 해주면된다.

Parsing을 어떻게하는지는.. http://www.cyworld.com/fourhand/4096293 이곳에서 배웠다.

JSONObject json = new JSONObject(responseBuilder.toString());
      
json = json.getJSONObject("responseData");
JSONArray jarray = json.getJSONArray("results");
    
String result = new String();
      
Log.d(LOG,Integer.toString(jarray.length()));
      
for(int i = 0; i < jarray.length(); i++) {
// 결과별로 결과 object 얻기
JSONObject jtmp = jarray.getJSONObject(i);
       
String streetAddr = jtmp.getString("streetAddress"); // street 주소
String title = jtmp.getString("titleNoFormatting"); // html 이 포함되지 않은 장소명
       
// 번호 얻기
JSONArray jarray_tmp = jtmp.getJSONArray("phoneNumbers");
JSONObject jtmp2 = jarray_tmp.getJSONObject(0); // 처음 번호만 파싱함
String number = jtmp2.getString("number");
       
// 전체 주소 얻기
String addr_tmp = jtmp.getString("addressLines"); // addressLines(ket) 값을 얻어온다.
String[] addr_tmp2 = addr_tmp.split("\""); // ["...."] 이 구조이기 때문에 중간 문자열만 가지고온다
String addr = addr_tmp2[1]; // { "[" , ".....", "]" } 이렇게 되어있는 것에서 중간 문자열을 선택

JSONObject를 생성하고 원하는 결과 - 장소명, 주소, 전화번호-를 얻은 코드이다.

 

그런데... 여기서  한가지 의문이 가는 점이 있다. 결과값이 항상 4개만 나온다는 것이다. 어떤걸 검색하든....

어필 생각하기론 URL의 결과가 페이지 단위로 오는데 한 페이지에 4개의 검색 결과만 저장되고,

작성한 코드는 첫번째 페이지만 가지고 오는 것 같다....

자세한건... 모른다.. ;;;;;;

누가 왜 그런지 알려줬으면 하는 심정이다... (5개 이상의 결과를 얻고싶은 1人)

 

지금까지의 지식으로 만든 결과물이다. 검색의 기준을 숭실대입구역으로 하였기 때문에 숭실대입구 근처의

주변검색 결과가 나온다.

 

 

 

그리고 이 결과물을 바탕으로 GeoCoder로 위도 경도를 얻어,

아래와 같이 Map에 검색 결과를 마킹해주는 것도 할 수 있었다.

 

private void searchOnMap(String search) {
     StringBuilder responseBuilder = new StringBuilder();
     // URL을 이용하여 검색신청을 하고, 결과를 받아 responseBuilder에 저장한다.
     try {
      // 검색을 위한 URL 생성
      URL url = new 
      URL("http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(search, "UTF-8")
       + "&sll="+currentLocation[0]+","+currentLocation[1]
       + "&hl=ko");  // 8.13 수정
      BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8")); // 8.13 수정
      String inputLine;
      while ((inputLine = in.readLine()) != null) {
       responseBuilder.append(inputLine);
      }
      in.close();
     } catch (MalformedURLException me) {
      me.printStackTrace();
     } catch (UnsupportedEncodingException ue) {
      ue.printStackTrace();
     } catch (IOException ie) {
      ie.printStackTrace();
     }

     // 위의 결과를 JSONObject를 이용해 원하는 정보를 parsing 한다
     try {
      JSONObject json = new JSONObject(responseBuilder.toString());
      
      json = json.getJSONObject("responseData");
      JSONArray jarray = json.getJSONArray("results");
      
      String result = new String();
      
      Log.d(LOG,Integer.toString(jarray.length()));
      
      for(int i = 0; i < jarray.length(); i++) {
       // 결과별로 결과 object 얻기
       JSONObject jtmp = jarray.getJSONObject(i);
       
       String streetAddr = jtmp.getString("streetAddress"); // street 주소 파싱
       String title = jtmp.getString("titleNoFormatting"); // html 이 포함되지 않은 주소
       
       // 번호 파싱
       JSONArray jarray_tmp = jtmp.getJSONArray("phoneNumbers");
       JSONObject jtmp2 = jarray_tmp.getJSONObject(0); // 처음 번호만 파싱함
       String number = jtmp2.getString("number");
       
       // full address 파싱
       String addr_tmp = jtmp.getString("addressLines"); // addressLines(ket) 값을 얻어온다.
       String[] addr_tmp2 = addr_tmp.split("\""); // ["...."] 이 구조이기 때문에 중간 문자열만 가지고온다
       String addr = addr_tmp2[1]; // { "[" , ".....", "]" } 이렇게 되어있는 것에서 중간 문자열을 선택
      

       // 주소를 이용하여 위도경도 얻어오기
       // Geocoder를 이용하면 위도경도로 주소를 얻어오거나
       // 주소를 이용해 위도 경도의 정보를 얻어 올 수 있다
       Geocoder geocoder = new Geocoder(this);
       List<Address> addrList = geocoder.getFromLocationName(addr, 1); // 한개의 주소정보만 얻어온다
       Address adr_tmp = addrList.get(0); // list에서 사용할 Address Object를 가지고 온다
       String[] location = new String[2];
       location[0] = Double.toString(adr_tmp.getLatitude()); // 위도 
       location[1] = Double.toString(adr_tmp.getLongitude()); // 경도 
       
       // 화면에 출력할 String 작성
       result += "Title : " + title + "\n";
       result += "Address : " + addr + "\n";
       result += "StreetAddr : " + streetAddr + "\n";
       result += "Phone : " + number +"\n";
       result += "Latitude : " + location[0] + "\n";
       result += "Longitude : " + location[1] + "\n\n";
      }
      
      text.setText(result);
      
      Log.d(LOG,jarray.getString(0));
     } catch (JSONException e) {
      e.printStackTrace();
     } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
    }

 

 

------------------------- 수정 ----------------- 8.13 -------------

 

url 쿼리를 통해 검색을 받아오는 것은 밑에 뎃글을 달아주신 것처럼 1~8만 되도록 되어있다.

rsz 이 파라미터값을 주는 것을 이용하여 그 개수를 조절 할 수 있는데. 기본값이 4여서 위와 같이하면 항상 4개의 결과를 받아오는 것이였다.

 

음.. 그리고 다른 한가지... 더 이상의 결과를 얻기 위해서는 url 쿼리를 다시 날려 그 뒤에 결과값을 받아 올 수 있다.

url 파라미터중에 start 라는 것이 있는데, 이것은 검색결과를 어디서 부터 줄것인지 결정하는 것이다.

(검색 시작 페이지를 위한 파라미터라고 설명하는 곳도 있다.)

 URL("http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(search, "UTF-8")
       + "&sll="+currentLocation[0]+","+currentLocation[1]
       + "&hl=kr");

즉 위의 url 을

 URL("http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(search, "UTF-8")
       + "&sll="+currentLocation[0]+","+currentLocation[1]
       + "&hl=kr"

       + "&start=4" );

위와 같이 start 파라미터값을 주면 4부터 결과값을 받아온다. (start의 기본값은 0이라고한다.)

이를 이용해 or문같은 반복문으로

int rsz = 4;

for(int i = 0; i < 30; i += rsz) {

URL url =   

new URL("http://ajax.googleapis.com/ajax/services/search/local?v=1.0&q="+ URLEncoder.encode(search, "UTF-8")
       + "&sll="+currentLocation[0]+","+currentLocation[1]
       + "&hl=ko"

       + "&rsz=" + String.valueOf(rsz)

       + "&start=" + String.valueOf(i) };

........

}

이렇게 해서 각 결과물을 파싱하면 최대 30개까지의 결과물을 얻을 수 있다.

(start 값이 30개가 넘어가면 out of range 라는 것을 리턴해준다)

아참, 그리고 hl은 언어를 나타내는데.. 우리나라 언어코드고 kr인줄알고 kr을 썼었지만.... 알아보니 kr 이 아니고 ko 가 맞는 것이였다.;;

 

 

 

아무리 생각해도.. 무지하면 몸이 고생하는 것같다... 저런걸 미리 알고있었더라면....

많은 시간 검색에 투자하지 않아도 되었을터인데...

모바일 디바이스와 플랫폼은 매번 새로운 제품이 나올 때마다 더욱 다양한 기능으로 시선을 끌며 업계를 선도하는 모바일 벤더에서는 단지 몇 달 만에 중요한 발표를 하곤 한다. 헤드라인에는 대부분 멀티터치 기능 및 Adobe® Flash® 기술과 같은 UI 기능과 프로세서 속도 및 스토리지 용량과 같은 하드웨어 개선사항에 관한 내용을 채워진다. 그러나 중요한 점은 컨텐츠가 핵심이라는 사실이다. 애플리케이션과 서버, 모바일 디바이스 그리고 사용자 간에는 계속해서 컨텐츠 즉, 데이터의 교환이 이루어진다. 데이터를 처리할 수 있는 기능이 없으면 Apple의 iPhone이나 Google의 Android와 같은 스마트폰은 값은 비싸지만 성능은 이에 미치지 못하는 일반적인 휴대전화가 된다.

자주 사용하는 약어

  • API: Application Programming Interface
  • DOM: Document Object Model
  • HTML: HyperText Markup Language
  • IDE: Integrated development environment
  • SAX: Simple API for XML
  • SDK: Software Developer Kit
  • UI: User Interface
  • XML: Extensible Markup Language

경이적으로 성공한 Facebook, LinkedIn 및 Twitter와 같은 소셜 네트워킹 플랫폼을 생각해보자. 기능 면에서만 보면 이러한 플랫폼은 매우 단조롭다. 구성원과 사이트 방문자가 사이트에서 발행된 컨텐츠에서 가치를 추구한다는 점에서 이러한 플랫폼이 인기를 얻고 있다. 그리고 모바일 디바이스 덕택에 이러한 컨텐츠를 액세스하는 경우가 점차 늘어나고 있다.

이 기사에서는 Android 플랫폼에서 XML과 JSON 데이터 교환 형식을 사용하는 과정을 설명한다. 예제 애플리케이션의 데이터 소스는 Twitter 계정의 상태 업데이트 피드이다. Twitter에서는 이 피드 데이터를 XML과 JSON 형식으로 사용할 수 있다. 아는 바와 같이 이 두 가지 형식은 프로그래밍을 통해 데이터를 처리하는 방식이 매우 다르다.

여기서는 독자가 이 기사에 있는 예제 코드를 실행하기 위해 Eclipse와 Android SDK 버전 1.5 이상을 설치했다고 가정한다. 환경 설정에 관한 자세한 내용은 Android Developers 웹 사이트를 참조한다. 또한, 활성화된 Twitter 계정이 있으면 예제를 따라 하는 데 도움이 되지만 반드시 필요한 것은 아니다. 관련 링크는 참고자료를 확인한다.

먼저, 두 가지 데이터 형식 중 XML을 살펴보기로 하자. 이미 XML과 JSON에 익숙한 경우에는 애플리케이션 기회: Twitter 피드로 건너 뛰어 바로 Android에서 이 두 형식을 사용해볼 수 있다.

XML: 오랜 친구

힘든 작업은 이제 그만!

일반적으로 XML을 채택하기 전 기술이 상태와 비교해보면 XML의 자기 기술적 특성이 가치가 있다는 것을 분명하게 확인할 수 있다. 그 당시에는 데이터를 교환하려면 워드 프로세서나 스프레드시트 애플리케이션으로 번거로운 데이터 기술 문서를 직접 작성하고 유지보수해야 했다. 이러한 문서는 보통 인터페이스 스펙이라고 하며 여기에는 필드 이름과 길이, 구분 기호, 계층 구조 등이 기술되었다. 사용자는 그들이 적합하다고 생각하는 것을 따랐으며 표준에 가장 근접한 것은 CSV(Comma-Separated-Value) 형식이었다. 그러나 CSV 파일 조차도 매우 다양하다. CSV 파일을 스프레드시트 프로그램으로 가져오면서 사용 가능한 모든 옵션을 확인하면 이점을 확인할 수 있다.

사실상 최근에 엔터프라이즈이나 웹, 모바일 분야에서 프로그래밍을 수행한 개발자라면 누구나 XML을 사용했을 것이다. XML은 이제 어디에서나 사용되고 있다.

XML 문서에는 인식 가능한 구조 즉, 속성과 하위 요소를 선택적으로 포함할 수 있는 일련의 요소가 있다. 유효한 모든 XML 문서의 첫 번째 줄에는 <?xml version="1.0" encoding="utf-8"?>이라고 선언되어 있다. 첫 번째 줄 다음부터는 애플리케이션마다 다르다. XML의 장점은 이 형식이 자기 기술적이라는 점에 있다.

XML 스키마

XML 문서가 자기 기술적이라고 하더라도 XML 문서는 특정 규칙과 지침을 따라야 한다. XML 스키마는 이러한 규칙과 지침을 지정한다. XML 스키마는 특정 XML 파일의 구조가 기술된 문서이다. 이러한 구조는 때로는 장황하고 복잡하다. (이론의 여지가 있지만, XML로 인해 매우 설명적인 데이터 구조 개념이 만연하고 부분적으로는 지난 10년간 디스크 스토리지 기술 비용이 대폭 감소하면서 데이터가 급격히 늘어나는 좋지 못한 상황이 발생하였다.)

이와 같은 크고 복잡한 파일을 일반적으로 사용하게 되면서 이러한 파일을 수동으로 처리하는 기술은 프로그래머와 분석가에게 일고의 가치도 없는 기술로 간주되곤 했다. 이러한 문제를 처리하기 위해 XML 편집기와 유효성 확인 도구를 사용하여 이러한 파일과 이 파일과 관련된 작업(레거시 형식으로 문서화하거나 변환하는 작업)을 관리하게 되었다.

또한, XML을 사용하면 CDATA 태그 세트를 통해 일반적인 텍스트 데이터 외에 2진 데이터를 저장할 수 있다. XML 문서 내에 있는 CDATA 태그에는 텍스트에 CDATA 자체가 포함되지만 않으면 다른 마크업 텍스트를 포함한 모든 유형의 데이터가 포함될 수 있다.

API에서는 요청/응답 쿼리를 수행하기 위한 구조로 XML을 사용하여 이러한 기능을 활용하는 것이 일반적이다. 응답 데이터는 XML 구조를 포함하고 있으며 이 XML 구조는 CDATA 태그 내에 포함된다. 예를 들면, API 호출을 통해 이름이 Mott인 고객 레코드를 요청할 수 있다. 데이터가 발견되면 데이터는 XML 구조로 패키지되어 Listing 1에 있는 것과 같은 응답 요소에 저장된다.


Listing 1. 데이터를 XML 구조로 패키지하여 응답 요소 내에 저장하기

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<request>
<query>
<lastname>Mott</lastname>
<maxhits>100</maxhits>
</query>
</request>

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
<returncode>200</returncode>
<query>
<lastname>Mott</lastname>
<hits>1</hits>
</query>
<data>
<![CDATA[
<contact>
<firstname>Troy</firstname>
<lastname>Mott</lastname>
<age>not telling</age>
</contact>
]]>
</data>
</response>

작업영역에서의 XML

오늘날에는 XML을 기본적인 데이터 형식으로 사용한다. 같은 데이터를 다른 형식으로도 사용할 수 있지만 XML 구조의 사용 가능성을 고려하는 것이 안전하다.

ERP(Enterprise Resource Planning) 패키지에서는 데이터 가져오기와 내보내기 작업에 XML을 많이 사용한다. 인터넷 뉴스 사이트에서는 데이터를 RSS(Really Simple Syndication) 피드 즉, 뉴스 판독기에서 처리할 수 있게 설정한 사전 정의된 형식이 있는 XML 문서로 사용할 수 있다. OpenOffice.org 및 Microsoft® Office와 같은 워드 프로세싱 애플리케이션도 XML을 사용한다.

현재의 Microsoft Office 문서는 PKZIP 호환 가능 파일이며 여기에는 다수의 XML 문서가 포함되어 있다. 각 XML 파일의 첫 번째 줄에는 공통으로 선언 문구가 포함된다. Listing 2에서 알 수 있는 바와 같이 속성은 수행하기에 다소 어려울 수 있다.


Listing 2. 각 XML 파일의 첫 번째 줄에 있는 공통 선언

 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <w:document xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
 xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
 xmlns:v="urn:schemas-microsoft-com:vml"
 xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
 xmlns:w10="urn:schemas-microsoft-com:office:word"
 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
 xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
 <w:body><w:p w:rsidR="00B6337C" w:rsidRDefault="00663F0E"><w:r>
 <w:t xml:space="preserve">This is a sample </w:t></w:r><w:r
 w:rsidRPr="006906EA"><w:rPr><w:i/></w:rPr><w:t>Microsoft 
 Word document</w:t></w:r><w:r><w:t xml:space="preserve"> used
 to </w:t></w:r><w:r w:rsidRPr="006906EA"><w:rPr><w:b/>
 <w:u w:val="single"/></w:rPr><w:t>demonstrate</w:t></w:r>
 <w:r><w:t xml:space="preserve"> some XML topics.</w:t></w:r>
 </w:p><w:p w:rsidR="00B14B2A" w:rsidRDefault="00B14B2A"/><w:p 
 w:rsidR="00B14B2A"w:rsidRDefault="00B14B2A"><w:r><w:rPr>
 <w:noProof/></w:rPr><w:drawing><wp:inline distT="0" distB="0" 
 distL="0" distR="0"><wp:extent cx="3276600" cy="3838575"/><wp:effectExtent
 l="19050" t="0" r="0" b="0"/><wp:docPr id="1" name="Picture 0"
 descr="frankableson.jpg"/><wp:cNvGraphicFramePr><a:graphicFrameLocks
 xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
 noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic
 xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData
 uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic
 xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
 <pic:nvPicPr><pic:cNvPrid="0"name="frankableson.jpg"/><pic:cNvPicPr/>
 </pic:nvPicPr><pic:blipFill><a:blip r:embed="rId4"
 cstate="print"/><a:stretch><a:fillRect/></a:stretch>
 </pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/>
 <a:ext cx="3276600" cy="3838575"/></a:xfrm><a:prstGeom
 prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic>
 </a:graphicData></a:graphic></wp:inline></w:drawing>
 </w:r></w:p><w:p w:rsidR="00663F0E" w:rsidRDefault="00663F0E"/>
 <w:p w:rsidR="00CC16CE" w:rsidRDefault="00CC16CE"/><w:sectPr 
 w:rsidR="00CC16CE" w:rsidSect="00B6337C"><w:pgSz w:w="12240" w:h="15840"/>
 <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" 
 w:footer="720" w:gutter="0"/><w:cols w:space="720"/><w:docGrid
 w:linePitch="360"/></w:sectPr></w:body></w:document>

XML은 자기 기술적이지만 그렇다고 해서 태그를 판독하기 쉬운 것은 아니다. 또한, 이처럼 복잡한 예제에서는 다수의 XML 네임스페이스를 사용하고 있으며 이 때문에 특수한 도구가 없으면 XML 문서를 처리하기가 훨씬 더 어려워진다.

XML은 어디에서나 사용할 수 있지만 Android 프로그래머에게는 좋지 못한 선택이 될 수도 있다. 특히, XML의 구조적 특성으로 인해 데이터가 증가하게 되면서 데이터 구조를 희생해야 하는 경우에는 더욱 그렇다. Android와 같이 자원이 제약된 플랫폼은 일반적으로 셀룰러 데이터 네트워크에서 작동하며 대량의 XML 데이터를 저장하거나 구문 분석할 수 없다. 그러나 특정 프로그래밍 작업에서 텍스트와 2진 데이터를 모두 교환해야 하는 경우에는 XML을 사용하는 것이 유용하다.

이제 다른 데이터 교환 형식인 JSON을 살펴보자.


JSON: 인터넷에 새롭게 등장한 기술

JSON을 데이터 형식 옵션으로 제공하는 인터넷 API 제공자가 더욱 많아지고 있다. JSON은 그 자체로 Ajax(Asynchronous JavaScript and XML) 웹 프로그래밍 커뮤니티에서 유명하다. Ajax 기술을 이용하면 웹 페이지에서 전체 페이지를 새로 고치 대신 선택한 부분에 있는 데이터만 다시 고침으로써 웹 페이지를 동적으로 업데이트할 수 있다. 보다 적은 데이터가 전송되고 따라서 훨씬 더 적은 데이터가 구문 분석되어 브라우저 창에 표시되기 때문에 Ajax를 사용하는 애플리케이션은 기존 웹 애플리케이션보다 훨씬 더 우수한 사용자 경험을 제공할 수 있다. 사실상, 제대로 작성된 Ajax 애플리케이션이라면 사용자 경험에 있어서 스마트 애플리케이션이나 팻 클라이언트 애플리케이션에 못지않다.

Ajax 애플리케이션은 웹 서버를 이용하여 데이터를 교환하며 때로는 데이터와 같은 것을 다시 고치도록 요청하지만 형식화는 원칙적으로 필요 없다. 사전 형식화된 HTML을 웹 서버에서 서비스하는 것은 일반적으로 좋지 않다. 그 대신 제대로 작성한 애플리케이션을 통해 데이터 컨텐츠를 브라우저에 전송하고 CSS(Cascading Style Sheet) 파일을 적용하여 색상 및 글꼴 특성과 같은 시각적 효과를 제공해야 한다.

애플리케이션에서 가상의 인물인 Mott의 연락처 레코드를 요청한다고 가정하자. 이 애플리케이션에는 브라우저로 반송할 둘 이상의 데이터 요소가 있다. 그렇다면 이 데이터를 어떻게 패키지해야 할까? Listing 1에 있는 예제에서는 XML로 된 간단한 요청/응답 구조를 사용하였다. 이러한 방식도 매우 적합하지만 이런 경우에는 서버에서 응답을 전송할 때마다 구문 분석하여 DOM과 같은 구조에 데이터를 저장한 후, 웹 페이지 컨텐츠를 업데이트해야 한다.

그 대신 서버에서 Javascript를 일부 되가져와서 직접 처리할 수도 있다. 다음은 이름이 Mott인 남자를 대상으로 하는 쿼리(http://<yourserver/app/searchcontact?Mott)에 응답하는 가상 애플리케이션의 샘플 응답이다. 이 응답은 문자열로 표현된 Javascript 오브젝트 즉, JSON 문자열이며 여기서는 기사의 페이지 넓이에 맞춰 두 줄로 나누어져 있다.

[{"firstname":"Troy","lastname":"Mott","age":"don't ask!"},{"firstname":"Apple seed",
   "lastname":"Mott's","age":"99"}]

XML에서는 문자열이 장황하게 기술되는 반면에 JSON은 읽기가 다소 어렵다는 문제가 있다. JSON 오브젝트는 : 쌍 형식으로 구성된다. JSON 오브젝트의 요소는 쉼표로 분리하고 각 오브젝트는 중괄호({}) 안에 포함된다. 그리고 오브젝트의 배열은 대괄호 안에 포함된다. 이렇게 하는 것이 데이터베이스에 있는 일련의 행을 오브젝트의 배열로 전송하는 일반적인 방법이며 오브젝트의 배열에서 각 배열 요소는 데이터베이스 행에 해당하며 오브젝트의 각 특성은 데이터 열을 나타낸다.

Listing 3에는 이러한 오브젝트를 HTML 페이지 내에서 사용하는 예제가 표시되어 있다. 단순하게 하기 위해 서버와 통신하는 부분은 추가하지 않았으며 그 대신 문자열 변수(serverresponse)를 사용하여 JSON 데이터를 제공한다.


Listing 3. HTML 페이지에서 JSON 오브젝트 사용하기

<html>
<head>
<script language="JavaScript">
var serverresponse = "[{\"firstname\":\"Troy\",\"lastname\":\"Mott\",\"age\":\"don't
ask!\"},{\"firstname\":\"Apple seed\",\"lastname\":\"Mott's\",\"age\":\"99\"}]";
function updatepage()
{
    var contacts = eval!(serverresponse );
    var i;
    var s = "Search Results:<br />";
    for (i=0;i<contacts.length;i++)
    {
        s = s + contacts[i].firstname + " " + contacts[i].lastname + "'s age is ... " 
+ contacts[i].age + "<br />";
    }
    document.getElementById("target").innerHTML = s;
}
</script>
</head>
<body>
<button onclick="updatepage();">Search for Mott</button><br />
<span id="target"> </span>
</body>
</html>

이 예제에서는 eval!() Javascript 함수를 사용하여 문자열을 Javascript 배열로 변환한다. JSON 라이브러리를 사용하면 더욱 신속하고 안전하게 이 단계를 수행할 수 있다. Listing 3에 있는 방식은 우수 사례가 아니다. 이 예제는 Ajax 애플리케이션에서 JSON 오브젝트를 어떻게 사용할 수 있는지 보여주기 위한 것이며 JSON 구조는 클라이언트 코드에서 교환되고 구문 분석되어 처리된다.

요약하면 JSON에는 다음과 특징이 있다.

  • 데이터 교환 형식이다.
  • Javascript 오브젝트를 문자열로 인코드하는 수단이다.
  • 텍스트와 숫자 값으로 제한된다. 2진 값은 명백하게 허용되지 않는다. JSON에는 CDATA에 해당하는 것이 없다.
  • 데이터 크기 면에서 XML보다 더 경제적이지만 가독성은 떨어진다.
  • Twitter와 같은 API 제공자가 옵션으로 제공하는 사례가 증가하고 있다.

Listing 3에서 클라이언트는 클라이언트 측 스크립트를 실행하는 웹 브라우저이다. 이제 본론으로 돌아가서 Android 애플리케이션에서 XML과 JSON을 사용하는 방법을 살펴보도록 하자.


애플리케이션 가능성: Twitter 피드

Twitter는 사람들이 아침 식사로 무엇을 먹는지 그리고 어린이 스포츠 팀이 운동장에서 어떻게 활동하는지부터 인접 국가에서 발생한 정치적 폭동과 관련된 최신 정보나 장기 이식과 관련된 상세한 보도와 같은 심각한 주제에 이르기까지 거의 모든 최신 정보를 제공하면서 국제적인 영향력을 갖게 되었다.

이 기사에 있는 샘플 코드와 함께 사용할 XML과 JSON 문서를 얻기 위한 가장 쉬운 방법은 http://twitter.com/statuses/user_timeline/userid.format URL을 이용하는 것이며 여기서 userid는 Twitter 사용자 ID이고 format은 XML이나 JSON에 해당한다.

또한, 이 페이지를 가리키는 링크를 그림 1과 같은 Twitter 페이지에서 직접 찾을 수 있으며 여기에서 자신의 Twitter 사용자 ID를 확인할 수 있다.


그림 1. 자신의 Twitter 페이지에 있는 피드 페이지를 가리키는 링크
오른쪽 하단 모서리에 'RSS feed of fableson's tweets' 링크가 있는 fableson Twitter 페이지의 스크린 캡처 

전체 피드 파일은 매우 장황하므로 다음 두 개의 Listing에서는 필자의 Twitter 계정에서 가져온 피드의 첫 번째 항목만 표시한다. Listing 4에는 XML 스니펫이 포함되어 있다.


Listing 4. XML 스니펫

<?xml version="1.0" encoding="UTF-8"?>
<statuses type="array">
<status>
  <created_at>Thu Apr 29 05:25:29 +0000 2010</created_at>
  <id>13052369631</id>
  <text>Wrapping up new article on JSON for Android
 programmers...</text>
  <source><a href="http://www.linkedin.com/"rel="nofollow">
   LinkedIn</a></source>
  <truncated>false</truncated>
  <in_reply_to_status_id/>
  <in_reply_to_user_id/>
  <favorited>false</favorited>
  <in_reply_to_screen_name/>
  <user>
    <id>15221439</id>
    <name>fableson</name>
    <screen_name>fableson</screen_name>
    <location>Byram Township, NJ</location>
    <description/>

<profile_image_url>http://a3.twimg.com/profile_images/260492935
/bookcover_normal.jpg</profile_image_url>
    <url>http://msiservices.com</url>
    <protected>false</protected>
    <followers_count>52</followers_count>
    <profile_background_color>9ae4e8
    <profile_text_color>000000</profile_text_color>
    <profile_link_color>0000ff</profile_link_color>
    <profile_sidebar_fill_color>e0ff92
</profile_sidebar_fill_color>
    <profile_sidebar_border_color>87bc44
</profile_sidebar_border_color>
    <friends_count>10</friends_count>
    <created_at>Tue Jun 24 17:04:11 +0000 2008</created_at>
    <favourites_count>0</favourites_count>
    <utc_offset>-18000</utc_offset>
    <time_zone>Eastern Time (US & Canada)</time_zone>

   <profile_background_image_url>http://s.twimg.com/a/1272044617/
images/themes/theme1/bg.png</profile_background_image_url>
   
<profile_background_tile>false</profile_background_tile>
    <notifications>false</notifications>
    <geo_enabled>false</geo_enabled>

    <verified>false</verified>
    <following>false</following>
    <statuses_count>91</statuses_count>
    <lang>en</lang>
    <contributors_enabled>false</contributors_enabled>
  </user>
  <geo/>
  <coordinates/>
  <place/>
  <contributors/>
</status>
</statuses>

Listing 5에는 동일한 데이터가 JSON 형식으로 표시되어 있다.


Listing 5. JSON 형식의 피드 데이터

[
{"in_reply_to_status_id":null,
"favorited":false,
"created_at":"Thu Apr 29 05:25:29 +0000 2010",
"in_reply_to_screen_name":null,
"geo":null,
"source":"<a href=\"http://www.linkedin.com/\" rel=\"nofollow\
          ">LinkedIn</a>",
"contributors":null,
"place":null,
"truncated":false,
"coordinates":null,
"user":
{
    "friends_count":10,
    "description":"",
    "lang":"en",
    "statuses_count":91,
    "time_zone":"Eastern Time (US & Canada)",
    "profile_link_color":"0000ff",
    "favourites_count":0,
    "created_at":"Tue Jun 24 17:04:11 +0000 2008",
    "contributors_enabled":false,
    "profile_sidebar_fill_color":"e0ff92",
    "following":null,
    "geo_enabled":false,
    "profile_background_image_url":"http://s.twimg.com/a/1272044617/images/themes
/theme1/bg.png",
    "profile_image_url":"http://a3.twimg.com/profile_images/260492935
/bookcover_normal.jpg",
    "notifications":null,
    "profile_sidebar_border_color":"87bc44",
    "url":"http://msiservices.com",
    "verified":false,
    "profile_background_tile":false,
    "screen_name":"fableson",
    "protected":false,
    "location":"Byram Township, NJ",
    "profile_background_color":"9ae4e8",
    "name":"fableson",
    "followers_count":52,
    "id":15221439,
    "utc_offset":-18000,
    "profile_text_color":"000000"
},
"in_reply_to_user_id":null,
"id":13052369631,
"text":"Wrapping up new article on JSON for Android programmers..."}
]

두 개의 Listing에 상태 업데이트 외에 얼마나 많은 추가 데이터가 삽입되었는지 확인한다. 게시물이 작성되고 나면 날짜/시간과 해당 게시물의 텍스트 자체만 신경 쓰면 된다. 다음은 이 데이터를 구문 분석하는 Android 애플리케이션의 관련 부분을 살펴보자. 전체 프로젝트는 다운로드링크를 통해 얻을 수 있다.


XMLvsJSON 애플리케이션

실시간 업데이트

샘플 애플리케이션은 웹에서 실시간으로 데이터를 가져오지 않지만 실제 애플리케이션에서는 실시간으로 데이터를 가져오게 된다. 데이터 피드는 raw 자원 폴더에서 가져오므로 샘플 애플리케이션에서는 구문 분석 특성에 집중할 수 있다. Android에서의 네트워크 연결과 관련된 정보는 참고자료를 참조한다.

이 Android 애플리케이션은 단순하다. 이 애플리케이션은 XML과 JSON 데이터 피드의 전체 사본을 포함하고 있으며 사용자에게 이 두 사본 중 어느 하나를 구문 분석할 수 있는 옵션을 제공한다. 그림 2에는 Eclipse에 있는 프로젝트 파일의 구조가 표시되어 있다. (그림 2의 텍스트 전용 버전 보기)


그림 2. Eclipse 프로젝트의 파일 구조
Eclipse에 있는 파일의 구조를 캡처한 화면 

그림 3에는 구문 분석 옵션을 선택하기 전의 애플리케이션 UI가 표시되어 있다.


그림 3. 구문 분석 옵션을 선택하기 전의 애플리케이션 UI
구문 분석 옵션을 선택하기 전의 애플리케이션 사용자 인터페이스를 캡처한 화면 

애플리케이션 UI에는 먼저, Parse XML과 Parse JSON file 단추가 표시되고 그다음에 default text가 표시된다. Listing 6에는 UI에 맞는 레이아웃이 포함되어 있으며 해당 프로젝트의 res/layout 폴더에 있는 main.xml 파일에서 이 레이아웃을 확인할 수 있다.


Listing 6. UI 레이아웃

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="center_horizontal">

<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
 android:id="@+id/btnXML" android:text="Parse XML"></Button>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
 android:id="@+id/btnJSON" android:text="Parse JSON file"></Button>
</LinearLayout>

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 
android:id="@+id/ScrollView01" android:layout_width="fill_parent"
android:layout_height="wrap_content">

<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="default text" 
    android:layout_gravity="center_horizontal"
    android:id="@+id/txtData" 
    />


</ScrollView>

</LinearLayout>

Parse XML과 Parse JSON file 단추는 TextView 제어가 포함된 ScrollView 위에 정의되어 있다. 이렇게 하는 이유는 사용자가 결과 데이터를 스크롤할 수 있게 하려는 것이다.

여러 개의 LinearLayout 구조를 사용했다는 점에 주목한다. 첫 번째는 수직 맞춤이고 여기에는 수평 구조가 있는 LinearLayoutScrollView가 모두 포함되어 있다. 내부에 있는 LinearLayout에는 두 개의 Button 위젯이 포함되어 있다. 이 레이아웃은 Listing 7에 있는onCreate() 메소드에서 확대되어 연결된다.


Listing 7. onCreate() 메소드

    Button btnXML;
    Button btnJSON;
    TextView tvData;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        tvData = (TextView) findViewById(R.id.txtData);
        btnXML = (Button) findViewById(R.id.btnXML);
        btnXML.setOnClickListener(new Button.OnClickListener()
        {
            public void onClick(View v)
            { 
                examineXMLFile();
            }
        });


        btnJSON = (Button) findViewById(R.id.btnJSON);
        btnJSON.setOnClickListener(new Button.OnClickListener()
        {
            public void onClick(View v)
            {
                examineJSONFile();
            }
        });

    }

examineXMLFile() 메소드는 XML 구문 분석을 제어한다.


XML 구문 분석

SAX 대 DOM

또한, Android는 DOM 구문 분석기를 지원한다. 이 구문 분석기를 사용하면 메모리 용량이 많이 필요하지만 SAX 구문 분석기를 사용할 때보다는 덜 복잡해진다. XMLvsJSON과 같은 애플리케이션은 대용량 데이터 피드의 매우 작은 서브세트에만 관여하며 SAX 방식은 이러한 작업에 적합하다고 할 수 있다.

XML 데이터를 구문 분석하는 작업은 SAX 방식의 구문 분석기를 사용하여 수행되는 경우가 많다. 여기서는 이러한 유형의 구문 분석기를 사용하여 소스 XML 데이터를 가리키는 InputSource를 설정하며 해당 문서가 "처리"될 때 특정 이벤트를 수신하는 핸들러를 제공한다. Listing 8에는 examineXMLFile() 메소드가 있으며 이 메소드는 다음과 같은 작업을 수행한다.

  • 원시 자원의 XML 파일을 사용하여 InputSource를 설정
  • Listing 9에 있는 twitterFeedHandler 핸들러와 관련하여 SAXParser 작성
  • 구문 분석기를 호출하여 TextView 위젯에 해당 결과를 표시. 이 위젯은 레이아웃 파일에서는 R.id.txtData로 식별되고 코드에서는tvData로 참조한다.
  • TextView에 모든 오류 표시


Listing 8. examineXMLFIle() 메소드

void examineXMLFile()
    {
        try {
            InputSource is = new InputSource(getResources()
.openRawResource(R.raw.xmltwitter));
            // create the factory
            SAXParserFactory factory = SAXParserFactory.newInstance();
            // create a parser
            SAXParser parser = factory.newSAXParser();
            // create the reader (scanner)
            XMLReader xmlreader = parser.getXMLReader();
            // instantiate our handler
            twitterFeedHandler tfh = new twitterFeedHandler();

            // assign our handler
            xmlreader.setContentHandler(tfh);
            // perform the synchronous parse
            xmlreader.parse(is);
            // should be done... let's display our results
            tvData.setText(tfh.getResults());
        }
        catch (Exception e) {
            tvData.setText(e.getMessage());
        }
    }

examineXMLFile() 메소드는 여러 가지 작업을 수행하지만 애플리케이션의 Perspective에서 수행되는 실제 구문 분석 작업은 twitterFeedHandler.java 파일에서 구현한 핸들러에서 처리된다. 이 클래스에서 Listing 9에 있는 DefaultHandler 인터페이스가 구현된다.


Listing 9. twitterFeedHandler 클래스

public class twitterFeedHandler extends DefaultHandler {

    StringBuilder sb = null;
    String ret = "";
    boolean bStore = false;
    int howMany = 0;

    twitterFeedHandler() {
    }

    String getResults()
    {
        return "XML parsed data.\nThere are [" + howMany + "] status updates\n\n" + ret;
    }
    @Override

    public void startDocument() throws SAXException {
        // initialize "list"
    }

    @Override
    public void endDocument() throws SAXException {

    }

    @Override
    public void startElement(String namespaceURI, String localName, String qName, 
Attributes atts) throws SAXException {

        try {
            if (localName.equals("status")) {
                this.sb = new StringBuilder("");
                bStore = true;
            }
            if (localName.equals("user")) {
                bStore = false;
            }
            if (localName.equals("text")) {
                this.sb = new StringBuilder("");
            }
            if (localName.equals("created_at")) {
                this.sb = new StringBuilder("");
            }
        } catch (Exception ee) {

            Log.d("error in startElement", ee.getStackTrace().toString());
        }
    }

    @Override

    public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {

        if (bStore) {
            if (localName.equals("created_at")) {

                ret += "Date: " + sb.toString() + "\n"; 
                sb = new StringBuilder("");
                return;

            }

            if (localName.equals("user")) {
                bStore = true;
            }

            if (localName.equals("text")) {

                ret += "Post: " + sb.toString() + "\n\n";
                sb = new StringBuilder("");
                return;

            }


        }
        if (localName.equals("status")) {
            howMany++;
            bStore = false;
        }
    }

    @Override

    public void characters(char ch[], int start, int length) {

        if (bStore) {
            String theString = new String(ch, start, length);

            this.sb.append(theString);
        }
    }

}

Listing 9에는 몇 가지 주목해야 할 항목이 포함되어 있다. 첫 번째로 주목해야 할 사항은 SAX 구문 분석기가 이벤트 기반 구문 분석기라는 점이다. 따라서 구문 분석될 때와 같은 실제 문서를 작성해야 한다. 이벤트는 데이터가 발견될 때마다 그리고 문서의 시작과 끝, 태그의 시작과 끝에서 개시된다. 따라서 중요한 데이터는 유지하고 나머지 데이터는 버리도록 데이터 구조를 정의해야 한다.

특정 데이터 소스를 InputSource에서 여러 번 읽어서 처리할 수 있으므로 StringBuilder를 사용하여 데이터를 추가한다. characters()메소드를 호출할 때마다 모든 데이터가 제공되는 것은 아니다.

이 애플리케이션은 데이터를 수집하여 형식화된 문자열로 간단히 변환한다. 그 대신 다른 예제에서는 이러한 항목을 콜렉션 클래스나 데이터베이스에 삽입할 수 있으며 특히, 구문 분석을 한 후에 대량의 데이터를 처리해야 하는 경우에 그렇게 한다.

getResults() 메소드는 이 클래스를 사용자 정의한 것이다. 이 메소드는 이러한 데이터를 어셈블된 형태로 수집하여 애플리케이션에 제공하기 위해 사용된다. 이 메소드는 DefaultHandler 인터페이스의 일부가 아니다.

그림 4는 구문 분석된 XML 데이터를 나타낸다. (그림 4의 텍스트 전용 버전 보기)


그림 4. 구문 분석된 XML 데이터
구문 분석된 XML 데이터가 표시된 휴대전화 화면을 캡처한 화면 

최종 구조를 구성, 관리하고 탐색한다는 면에서 보면 SAX 구문 분석기를 사용하여 XML을 구문 분석하는 것이 사소한 작업은 아니지만 이 구문 분석기의 중요한 장점은 구문 분석을 하는 과정과 그 이후 과정에서 필요한 램 용량을 대폭 줄일 수 있다는 점과 속도에 있다.

이제 Android 방식으로 JSON 데이터를 구문 분석하는 과정을 살펴보자.


JSON 구문 분석

애플리케이션에서 JSON 데이터를 구문 분석하는 과정은 사용자가 JSON 단추를 선택할 때 시작한다. JSON 단추를 클릭하면 Listing 10에 있는 examineJSONFile() 메소드가 호출된다. 모든 구문 분석과 문서 관리는 Android에서 제공하는 라이브러리 내에서 수행되고 JSON과 관련된 모든 코드가 이 메소드에 포함되어 있기 때문에 핸들러 클래스가 추가로 필요하지는 않다.


Listing 10. examineJSONfile() 메소드 호출

void examineJSONFile()
    {
        try
        {
            String x = "";
            InputStream is = this.getResources().openRawResource(R.raw.jsontwitter);
            byte [] buffer = new byte[is.available()];
            while (is.read(buffer) != -1);
            String jsontext = new String(buffer);
            JSONArray entries = new JSONArray(jsontext);

            x = "JSON parsed.\nThere are [" + entries.length() + "]\n\n";

            int i;
            for (i=0;i<entries.length();i++)
            {
                JSONObject post = entries.getJSONObject(i);
                x += "------------\n";
                x += "Date:" + post.getString("created_at") + "\n";
                x += "Post:" + post.getString("text") + "\n\n";
            }
            tvData.setText(x);
        }
        catch (Exception je)
        {
            tvData.setText("Error w/file: " + je.getMessage());
        }
    }

앞서 살펴본 XML 루틴과 마찬가지로 이 코드는 원시 자원에 있는 파일에서 데이터를 읽는다. 메모리로 데이터를 완전히 읽고java.lang.String으로 변환한 후, 구문 분석하여 JSONArray에 삽입할 수 있다. 이 예제에서와 같이 특정 문자열을 직접 구문 분석하여 배열에 삽입하거나 문자열을 구문 분석하여 JSONObject로 변환할 수 있다. Twitter 데이터는 오브젝트로 구성된 배열이기 때문에 전체 문자열을 구문 분석하여 배열에 삽입한 후, 원래의 위치에 따라 개별적으로 오브젝트를 액세스하는 것이 일반적이다.

이 메소드의 플로우는 간단하며 일단 데이터가 구문 분석되면 해당 코드는 XML 구문 분석기 핸들러에서 처리하는 방식과 비슷하게 문자열 표현을 구성한다. 여기서 중요한 점은 데이터가 관리되고 있다는 점이며 따라서 데이터가 포함될 메모리 구조를 개발자가 추가로 구성할 필요가 없다. 마찬가지로 애플리케이션은 JSONArray에 항목이 몇 개가 있는지 미리 인식한다. (이 예제에서는 20개의 항목이 있다.)

프로그램 면에서 JSON 구문 분석이 훨씬 더 간단하지만 대가가 없는 것은 아니다. JSON 구문 분석을 사용하면 데이터를 처리하기 전에 전체 데이터 스트림을 읽는 과정에서 메모리 소비량이 더 늘어나게 되며 모든 데이터를 저장하는 데도 메모리가 더 필요하게 된다. 이와는 대조적으로 SAX XML 방식에서는 관련된 데이터만을 사용한다. 이러한 문제를 제외하면 특정 JSON 오브젝트를 구문 분석하는 데 필요한 메모리가 충분한 경우, 대부분의 애플리케이션에서는 JSON 구문 분석 방식이 적합하며 특히, DOM과 함께 사용하고 싶지 않은 경우에 유용하다.

출처 : http://cafe.naver.com/aphone.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=8044&

TextView내 글자가 TextView 영역을 넘치는 경우, 흐르는 효과(marquee)을 줄수가 있는 다음과 같이 하면 된다.



1) 마퀴효과

TextView tv;

tv.setSingleLine(true);

tv.setEllipsize(TruncateAt.MARQUEE);  // 마퀴 효과주기

tv.setSelected(true); // 원래 포커스가 가야 마퀴가 동작하는데, 대신 선택시키면 동작함


2) ... 효과

TextView tv;

tv.setSingleLine(true);

tv.setEllipsize(TextUtils.TruncateAt.END);

+ Recent posts