아직 완벽하진 않지만 대강 필요한 기능을 구현한 후 내 앱에 Admob Banner를 배치하고자 했다. 처음에는 R symbol을 못찾아 build 자체가 안되어 그 해결책을 찾아보니 xmlns:ads 항목을 다음과 같이 xml layout에 기록하면 된다고 하여 해봤다. 그리고 일단은 build가 되었다. 그런데...

<com.google.android.gms.ads.AdView
    xmlns:ads=http://schemas.android.com/apk/lib/com.google.ads <!-- 이로서 R symbol을 못 찾는 error는 해결하긴 했지만 adSize was missing error가 발생하였다. -->
    android:id="@+id/adViewAdMob03"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_centerHorizontal="true"
    android:layout_alignParentBottom="true"
    ads:adSize="BANNER"
    ads:adUnitId="@string/banner_ad_unit_id_imageview">
</com.google.android.gms.ads.AdView>

  위 사진과 같이 'Required XML attribute "adSize" was missing' error가 발생하면서 banner가 보여야 할 부분이 저렇게 까맣게 처리되어 출력되는 게 아닌가? 다시 구글링을 해보니 중간의 '~/lib/~'를 '~/libs/~'로 고쳐보라고 하는 글이 많아 그대로 해 봤지만 이 역시 동작하지 않았다. 이제 어쩌나 싶었는데, stackOverflow에 한 답변이 눈에 띄었다.

최신 Admob SDK는 다른 namespace를 사용합니다. 
xmlns:ads="http://schemas.android.com/apk/res-auto

[출처] StackOverflow - AdMob in android “AdView missing required XML attribute 'adSize' ”
          (http://stackoverflow.com/questions/7185335/admob-in-android-adview-missing-required-xml-attribute-adsize)

  아무래도 SDK가 update 된 모양이다. 이대로 해보니 이젠 된다. 개발을 하면 할수록 느끼는 거지만 변화를 쫓는데 힘들어하는 사람이라면 (다른 직군도 그렇겠지만 유독 더) 어려운 일이 개발이지 않을까 싶다. 그나저나 새로나온 Gradle 서적 서평도 기록해야 하는데 정초부터 집안에 일이 많아 아직 1/3밖에 읽지 못했다. 지금까지 읽은 내용만 봐도 정리가 잘 되어 좋은 책이다 싶긴 한데 이벤트로 당첨되어 받은 책이라 빨리 끝까지 읽고 서평을 써야겠다.

 

[출처] Stack Overflow - http://stackoverflow.com/questions/4212861/what-is-a-correct-mime-type-for-docx-pptx-etc

  자세히 Intent에 대하여 정리하고 싶지만, 지금은 시간이 없으므로 일단 파일 확장자에 따라 맞는 타 앱을 Intent로 부를 때 필요한 MIME type을 기록해 두려 한다. 기본적으로 Intent intent = new Intent(Intent.ACTION_VIEW); 와 같이 선언한 뒤, intent.setDataAndType("file URI", "아래 MIME Type String"); 로 지정하여 연결한다.

Extension MIME Type
.doc     application/msword
.dot      application/msword

.docx    application/vnd.openxmlformats-officedocument.wordprocessingml.document
.dotx     application/vnd.openxmlformats-officedocument.wordprocessingml.template
.docm   application/vnd.ms-word.document.macroEnabled.12
.dotm    application/vnd.ms-word.template.macroEnabled.12

.xls      application/vnd.ms-excel
.xlt       application/vnd.ms-excel
.xla      application/vnd.ms-excel

.xlsx     application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xltx      application/vnd.openxmlformats-officedocument.spreadsheetml.template
.xlsm    application/vnd.ms-excel.sheet.macroEnabled.12
.xltm     application/vnd.ms-excel.template.macroEnabled.12
.xlam    application/vnd.ms-excel.addin.macroEnabled.12
.xlsb     application/vnd.ms-excel.sheet.binary.macroEnabled.12

.ppt      application/vnd.ms-powerpoint
.pot      application/vnd.ms-powerpoint
.pps     application/vnd.ms-powerpoint
.ppa     application/vnd.ms-powerpoint

.pptx      application/vnd.openxmlformats-officedocument.presentationml.presentation
.potx      application/vnd.openxmlformats-officedocument.presentationml.template
.ppsx     application/vnd.openxmlformats-officedocument.presentationml.slideshow
.ppam    application/vnd.ms-powerpoint.addin.macroEnabled.12
.pptm     application/vnd.ms-powerpoint.presentation.macroEnabled.12
.potm     application/vnd.ms-powerpoint.presentation.macroEnabled.12
.ppsm    application/vnd.ms-powerpoint.slideshow.macroEnabled.12

  Custom Gallery를 동작하도록 만들기는 했지만 scroll을 상하로 바꿔서 격자 구조로 사진의 thumbnail을 보는 게 편할 것 같다. 찾아보니 GridView라는 layout이 따로 있었다. Android API reference에 따르면 애초에 Grid View 자체가 2차원으로 item을 보여주는(이 말은 즉, Main이 되는 View위에 다른 View를 여러 개 보여주는) scroll이 가능한 Viewgroup이라고 한다. 이전에 만들었던 앱에서 Gallery를 GridView로 바꾸어 구연해 보자. 

1. 목적 : GridView에 대해 알아보고 이를 이용한 Image Gallery(List)를 만들어보자

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Grid View Example (http://developer.android.com/intl/ko/guide/topics/ui/layout/gridview.html)
 2) AndroidHive - Android GridView Layout Tutorial (http://www.androidhive.info/2012/02/android-gridview-layout-tutorial/)

4. 과정
 1) Main이 되는 Layout(여기서는 activity_main.xml)에 <GridView ...></GridView>를 다음과 같이 선언한다. 자동 완성 scroll 밑으로 <GridLayout>도 볼 수 있으나 minSdkVersion이 높으므로 낮은 사양에서도 사용할 수 있도록 <GridView>를 사용하기로 한다.

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/gridview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
    android:columnWidth="90dp" <!-- columnWidth(나눠진 칸 안에 있는 View의 가로 길이)에 따라 column(열)의 갯수가 달라진다 -->
    android:numColumns="auto_fit" <!-- 임의로 숫자 지정 가능 -->
    android:verticalSpacing="10dp"
    android:horizontalSpacing="10dp"
    android:stretchMode="columnWidth"
    android:gravity="center">

</GridView>

 

 2) GridView 내부에 ImageView 들을 뿌릴 Adapter를 다음과 같이 정의한다. 이전의 Adapter 부분과 비슷하다.

    // 이상 생략
   
// create a new ImageView for each item referenced by the Adapter
   // 참고자료 내 Android Developers에 게시된 Example 수정

    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView imageView;
        if (convertView == null) {
            // if it's not recycled, initialize some attributes
            imageView = new ImageView(mContext);
        } else {
            imageView = (ImageView) convertView;
        }
        bm = BitmapFactory.decodeFile(mBasePath + File.separator + mImgList[position]);
        Bitmap mThumbnail = ThumbnailUtils.extractThumbnail(bm, 300, 300);
        imageView.setPadding(8, 8, 8, 8);
        imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
        imageView.setLayoutParams(new GridView.LayoutParams(GridView.LayoutParams.MATCH_PARENT, GridView.LayoutParams.MATCH_PARENT));
        imageView.setImageBitmap(mThumbnail);
        return imageView;
    }
    // 이하 생략

 

 3) MainActivity.java 에서 앞서 정의한 GridView layout과 Adapter를 연결하고 Item Click 시 adapter에서 넘어온 해당 ImageView의 주소 값을 Toast message로 보여주도록 다음과 같이 설정한다.


public class
MainActivity extends AppCompatActivity {

    public String basePath = null;
    public GridView mGridView;
    public CustomImageAdapter mCustomImageAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), "MyCameraApp");

        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                Log.d("MyCameraApp", "failed to create directory");
            }
        }

        basePath = mediaStorageDir.getPath();

        mGridView = (GridView)findViewById(R.id.gridview); // .xml의 GridView와 연결
        mCustomImageAdapter = new CustomImageAdapter(this, basePath); // 앞에서 정의한 Custom Image Adapter와 연결
        mGridView.setAdapter(mCustomImageAdapter); // GridView가 Custom Image Adapter에서 받은 값을 뿌릴 수 있도록 연결
        mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(getApplicationContext(), mCustomImageAdapter.getItemPath(position), Toast.LENGTH_LONG).show();
            }
        });
    }
}

 

(위 Code의 실행 결과)

  

      

 

  요즘에 만들고 있는 Custom Camera & Gallery App에서 Gallery의 갱신(update)가 잘 되지 않는 문제를 해결하기 위해서 이전까지는 Activity의 생애주기(Life-cycle)를 살펴봤다. 이를 통해서 Camera Intent 실행 후 넘어왔을 때 Gallery의 배열이 update 되도록 하는 문제는 해결되었으나, 파일 삭제를 위해 따로 Intent 만들어 불러오기는 작동은 되나 쓸데없이 군더더기가 많은 듯 하였다. 그래서 gallery update 에 대하여 자료를 찾아봤더니 애초에 adapter class에 있는 notifyDataSetChanged() method를 활용하면 되는 것이었다(이걸 모르고 괜히 헛짓거리를 했네 쿨럭;;). 그런데 이전에 ListView도 그렇고 Adapter를 여기저기 활용하는데, 대체 Adapter란게 무엇인가에 대하여 제대로 살펴봐야 겠다.

1. 목적 : Adapter에 대하여 알아보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Adapter Reference (http://developer.android.com/reference/android/widget/Adapter.html)

4. Adapter에 대한 이해
 1) Adapter란 무엇인가?
   Android APIs' Reference에 따르면 'Adapter' 자체는 하나의 Object(객체)로서, 보여지는 View와 그 View에 올릴 Data를 연결하는 일종의 Bridge라고 한다. 그래서 그런지 ListView Adapter에 대한 예시에서도, GalleryAdapter에 대한 예시에서도 해당 Adapter의 생성자에서 Data를 연결하고 나서 getView() method에서 해당 position에 올라갈 TextView나 ImageView를 setting 해 View로서 뿌려주는 모양이다.

[사진 1] 기본 Adapter를 상속받는 subclass들 목록이다.

 2) Adapter 에 속해 있는 method들

 
[사진 2] Adapter의 기본 public method list

  Adapter class에 속해 있는 기본 method 의 목록은 위 [사진 2]와 같다. getCount()를 통해서 Adapter가 보여주는 item의 갯수값을 int로 받을 수 있고, getItem(int position)을 통해서 position에 위치해 있는 item의 data를 받을 수도 있다. getItemId(int postion)의 경우에는 getItem(int position)과 비슷하게 position에 위치한 값을 반환하기는 하나 그 값이 Item마다 부여된 일종의 ID라고 한다. 그리고 화면에 뿌려주기 위해서 getView(int position, View convertView, ViewGroup parent)를 활용하고, 이 getView()를 통해 생성된 View의 갯수를 getViewTypeCount()로 받을 수 있다고 한다. 이 외에도 hasStableIds()를 통해서 각 Row마다 부여된 ID를 변화에 상관없이 고정할지(stable) 여부를 결정할 수 있으며, 해당 Adapter가 비었는지를 isEmpty()로 판단할 수 있다.

  한편, notifyDataSetChanged()와 같은 기능을 활용하기 위해서는 Adapter에 묶여있는 Item의 변경 여부를 감지해야 하는데 이를 감지하는 것이 말그대로 'DataSetObserver'이다. 따라서, item의 변경이 감지(notify)되도록 하기 위해서는 registerDataSetObserver(DataSetObserver observer)를 통해서 observer를 Adapter에 연결해야 한다.

5. 적용 예시

 1) CustomGalleryAdapter.java 내 registerDataSetObserver() 등의 추가


public class
CustomGalleryAdapter extends BaseAdapter {
    int CustomGalleryItemBg;
    String mBasePath;
    Context mContext;
    String[] mImgs;
    Bitmap bm;
    DataSetObservable mDataSetObservable = new DataSetObservable(); // DataSetObservable(DataSetObserver)의 생성

    public String TAG = "Gallery Adapter Example :: ";

    public CustomGalleryAdapter(Context context, String basepath){
        this.mContext = context;
        this.mBasePath = basepath;

        File file = new File(mBasePath);
        if(!file.exists()){
            if(!file.mkdirs()){
                Log.d(TAG, "failed to create directory");
            }
        }
        mImgs = file.list(); 

        TypedArray array = mContext.obtainStyledAttributes(R.styleable.GalleryTheme);
        CustomGalleryItemBg = array.getResourceId(R.styleable.GalleryTheme_android_galleryItemBackground, 0);
        array.recycle();
    }

    @Override
    public int getCount() {
        File dir = new File(mBasePath);
        mImgs = dir.list();
        return mImgs.length;
    }

    @Override
    public Object getItem(int position) {
        return position;
    }

    // Adapter 내 Item에서 직접 주소를 받아오도록 method 추가.
    // 이전에는 MainActivity와 주소 및 position이 달라 비정상적인 앱의 종료가 발생한 것으로 보인다

    public String getItemPath(int position){ 
        String path = mBasePath + File.separator + mImgs[position];
        return path;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    // Override this method according to your need
    @Override
    public View getView(int index, View view, ViewGroup viewGroup)

    {
        // TODO Auto-generated method stub
        ImageView i = new ImageView(mContext);

        File dir = new File(mBasePath);
        mImgs = dir.list();
        bm = BitmapFactory.decodeFile(mBasePath+ File.separator +mImgs[index]);

        Bitmap bm2 = ThumbnailUtils.extractThumbnail(bm, 300, 300);
        i.setLayoutParams(new Gallery.LayoutParams(300, 300));
        i.setImageBitmap(bm2);
        i.setVisibility(ImageView.VISIBLE);

        i.setBackgroundResource(CustomGalleryItemBg);
        i.setScaleType(ImageView.ScaleType.FIT_CENTER);

        if (bm != null && !bm.isRecycled()) {
            bm.recycle();
        }
        return i;
    }

    @Override
    public void registerDataSetObserver(DataSetObserver observer){ // DataSetObserver의 등록(연결)
        mDataSetObservable.registerObserver(observer);
    }

    @Override
    public void unregisterDataSetObserver(DataSetObserver observer){ // DataSetObserver의 해제
        mDataSetObservable.unregisterObserver(observer);
    }

    @Override
    public void notifyDataSetChanged(){ // 위에서 연결된 DataSetObserver를 통한 변경 확인
        mDataSetObservable.notifyChanged();
    }
}

 

 2) MainActivity.java에 적용

// 파일 삭제 후 notifyDataSetChanged() 적용
        builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                        File file = new File(delFilePath);
                        file.delete();
                        File temp = new File(basePath);
                        imgs = temp.list();
                        customGalAdapter.notifyDataSetChanged();
                    }
            });

 

(위 Code의 실행 결과)

     

      

 

  앞선 글에서 Activity 생애주기(Life Cycle)를 고려하여 gallery와 gallery adapter를 onCreate()가 아니라 onStart()에 생성 및 연결을 했을 경우, camera intent 호출 후 새로 찍은 사진이 gallery에 정상 반영됨을 확인했다. 이를 보면서 사진을 삭제한 후에도 onStart()를 불러온다면 문제가 해결되겠다는 생각이 들었는데, 이를 구현하기 위해서 사진을 삭제하는 과정 자체를 새 intent로 불러왔다가 종료하여 MainActivity가 onRestart()가 되도록 해야했다. 그래서 이번에는 App. 내부에 새로운 activity를 추가해 intent로 활용하는 방법을 알아봤다. 

1. 목적 : Intent를 만들어 activity의 작동 구조를 이해하여 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Intent Reference (http://developer.android.com/reference/android/content/Intent.html)
 2) Android Developers - 인텐트 및 인텐트 필터 (http://developer.android.com/intl/ko/guide/components/intents-filters.html)

4. 과정
 일반적인 Intent 생성 및 작동 예제에 내가 필요한 기능을 추가했다.

 1) 새로 추가할 intent의 layout이 될 .xml 파일을 다음과 같이 '/layout' directory 밑에 생성한다. 필요에 따라 layout_main.xml을 활용하듯이 Button, ImageView, TextView 등을 추가하여 새로운 activity를 구성하면 된다.

 

2) Intent가 될 새 Activity를 생성한다. 'extends Activity'한 class java file을 다음과 같이 '/java' 밑에 만들고 MainActivity에서 onCreate() 내부에 내용을 쓰듯이 원하는 동작을 작성하면 된다. 나 같은 경우에는 단순히 AlertDialog를 사용하기 위해 intent를 불러오므로 따로 setContentView(R.layout.'intent layout file 명')하지 않아도 동작은 했었으나 일단 다음과 같이 코딩하였다.

public class FileRearrange extends AppCompatActivity{

    String delFilePath = null; // MainActivity에서 삭제할 file의 경로를 받을 String 변수 선언

    @Override
    protected void onCreate(Bundle savedIntanceState){
        super.onCreate(savedIntanceState);
        setContentView(R.layout.activity_file_rearrange);

        final Intent receiveIntent = getIntent(); // MainActivity에서 intent를 받음
        delFilePath = receiveIntent.getStringExtra("delFilePath"); // MainActivity의 intent에서 추가한 data 참조
        /* 이하 AlertDialog 활용 */
        AlertDialog.Builder builder = new AlertDialog.Builder(FileRearrange.this);
        builder.setTitle("파일을 삭제하시겠습니까?");
        builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss(); // 해당 AlertDialog의 dismiss() 및
                finish(); // receiveIntent의 finish();
            }
        });
        builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                File file = new File(delFilePath);
                file.delete(); // receive 받은 data를 활용해 file 삭제 후
                finish(); // receiveIntent의 finish();
            }
        });
        Dialog dialog = builder.create();
        dialog.show();
    }
}

 

 3) AndroidMainfest.xml 에 (2)에서 만든 Activity를 <application>...</application> 내부에 다음과 같이 <activity>...</activity>하여 선언한다. 이를 통해 App. 내부 Activity같의 communication이 가능해 진다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.zip.customcameragalleryexample" >

    <uses-feature android:name="android.hardware.camera2" android:required="false" />

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


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:hardwareAccelerated="false">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- 이하 activity 추가 부분 -->
        <activity android:name=".FileRearrange"> <!-- 위의 activity class명과 동일 -->
            <intent-filter>
                <action android:name="com.example.zip.customcameragalleryexample.filerearrange"/> <!-- intent 생성시 참조 -->
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <!-- 이상 activity 추가 부분 -->
    </application>

</manifest>

 

 4) MainActivity.java 에서 파일을 삭제하고자 할 때 FileRearrange로 넘어가도록 다음과 같이 intent를 생성 및 추가한다. Intent 의 생성 → 필요할 경우 data 추가 → 해당 Intent의 start 순서로 넘어감을 이해하면 된다.

// CustomGallery 예시의 LongClickListener 부분을 다음과 같이 수정하였다.
      customGallery
.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {

                final int tmpPosition = position;
                final String delFilePath = basePath + File.separator + imgs[tmpPosition];
                Intent intent = new Intent(MainActivity.this, FileRearrange.class);
                intent.putExtra("delFilePath", delFilePath);
                startActivity(intent);
                return false;
            }
        });

 

(위 Code의 실행 결과)
  원하는 동작이 오류 없이 실행되긴 했으나, 새로 intent를 불러오니 MainActivity 가 보이지 않았다. 그래서 OnItemLongClick 내부에서 file 삭제 후 다시 gallery를 불러오도록 해당 method를 재선언을 하자니 이 방법은 재귀적이라 app 자체가 비정상적으로 종료해서 쓸 수가 없었다. 아무래도 Adapter에서 배열의 변화를 감지하여 update가 되도록 notifyDataSetChanged() 와 같은 adapter의 method들을 다시 살펴볼 필요가 있어 보인다.

     

  

 

  사실 일전의 Custom Gallery Example에서 Gallery Item Long Click시 해당 사진 파일을 삭제하는 코드를 입력했었다. 실행 결과, 파일은 삭제되기는 했지만 파일 삭제 후의 Gallery Item들의 배열이 정리되지 않았고, 원래 해당 파일이 있던 frame 내부가 null 값으로 까맣게 보이며 click 시 App.이 비정상적으로 종료되었다. 그리고 Camera Intent로 촬영 후 새로 찍은 사진이 Gallery에 넘어오지 않았다. 아무래도 file 배열이 변경된 후 Gallery Adapter를 다시 불러와야 될 것 같은데, 원래 adapter가 생성되어 있던 onCreate()를 통째로 다시 불러들여도 괜찮을지 Activity 생애주기(Life Cycle)에 대해 알아봐야 겠다.

1. 목적 : Android Activity LifeCycle(Activity 생애주기)를 이해하고 기본 구조를 알아보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - Activity Reference (http://developer.android.com/reference/android/app/Activity.html)

4. Activity Life Cycle(Acitivity 생애 주기)의 이론

  Android Studio로 App. project를 생성해 보면 /java/'해당 packaging 명'/MainActivity.java 이 기본으로 만들어져 있고 그 안에 파일명과 같은 class가 'extends Activity' 혹은 'extends AppCompatAcitivity'을 끌고 기본 method인 onCreate(Bundle savedInstanceState)를 @Override한 채로 만들어져 있는 광경을 볼 수 있다. 이 모습을 보다보면 아무래도 C 언어에서 void main(void) 가 생성되며 시작 지점이 자동으로 지정되듯이 onCreate()가 필수적 임을 눈치로 대강 알 수 있을텐데, 실제로 'Android Developers reference'를 찾아보면 다음과 같은 순서도를 통해서 App.의 Activity에 대한 동작 구조를 파악할 수 있다.

  보시다시피 App.이 실행되면서 Activity가 시작되면 처음에 onCreate()를 통해서 Activity가 생성되고 onStart()와 onResume()을 거처 해당 Activity가 실행된다. 그러다가 해당 Activity를 sleep 시킬 때 onPause()와 onStop()을 거치게 되고, onStop() method 시점에서 사용자가 해당 Activity를 다시 실행시키고자 하면 onRestart()를, App.이 memory를 많이 잡아먹어 시스템에 의해 process kill이 됐을 때 사용자가 원할 경우 onCreate()로 넘어간다. 이런 method들을 돌고 돌던 Activity는 끝날 때가 되면 onDestory()를 통해 system에서 사라진다.

  그래서 App.의 activity의 생애주기에 따라 작동해야 할 특정한 기능이 있다면 아래와 같이 Activity class 내에 위 method()를 따로 정의해서 사용한다. 하나의 예로서, 이전에 항상 꺼지지 않는 App.을 만들기 위해서 onStop() 을 @Override하여 onStop() 내에서 onRestart()를 무조건 실행시키게 만들었었다.

public class Activity extends ApplicationContext {
    protected void onCreate(Bundle savedInstanceState);

    protected void onStart();

    protected void onRestart();

    protected void onResume();

    protected void onPause();

    protected void onStop();

    protected void onDestroy();
}

  그럼 이번 Gallery가 새로 찍은 사진이 update되지 않는 문제는 Gallery Adapter의 생성 및 연결을 onStart() method에 선언하여 @Override 해보면 되지 않을까? 아무래도 Camera Intent를 불러오면서 기존 MainActivity는 onStop()에 있어 intent가 종료되고 나면 onStart() 부터 다시 시작할 것 같았기 때문이다.

5. 실험

  이전의 Custom Gallery Example의 Gallery Adapter 선언 부분을 단순히 onStart()로 아래와 같이 옮겨봤다.

public class MainActivity extends AppCompatActivity {

    public static final int MEDIA_TYPE_IMAGE = 1;
    public static final int MEDIA_TYPE_VIDEO = 2;
    public int inSampleSize = 1;

    private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
    private Uri fileUri;
    private static String basePath;

    public float imageViewRotation = 90;
    public String TAG = "Camera Example :: ";

    private Button takePicBtn;
    private ImageView resultView;
    private TextView imgPath;
    private Gallery customGallery;
    private CustomGalleryAdapter customGalAdapter;

    private String[] imgs;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES), "MyCameraApp");
        // This location works best if you want the created images to be shared
        // between applications and persist after your app has been uninstalled.

        // Create the storage directory if it does not exist
        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                Log.d("MyCameraApp", "failed to create directory");

            }
        }

        basePath = mediaStorageDir.getPath();

        imgPath = (TextView)findViewById(R.id.imgpath);
        resultView = (ImageView)findViewById(R.id.resultview);
        takePicBtn = (Button)findViewById(R.id.takepicbtn);
        takePicBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // create Intent to take a picture and return control to the calling application
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

                fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // create a file to save the image
                intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // set the image file name

                // start the image capture Intent
                startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
            }
        });

       
        customGallery = (Gallery)findViewById(R.id.customgallery);


    }

    /* 아래와 같이 onStart()를 Override하여 Gallery Adapter 와 연관된 부분을 그대로 복사해 옮겨놓았다. */
    @Override
    protected void onStart(){
        super.onStart();

        File file = new File(basePath);
        imgs = file.list();
        for(int i=0; i<imgs.length; i++){
            imgPath.setText(imgs[i]);
        }

        customGalAdapter = new CustomGalleryAdapter(getApplicationContext(), basePath);
        customGallery.setAdapter(customGalAdapter);
        customGallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Bitmap bm = BitmapFactory.decodeFile(basePath+ File.separator +imgs[position]);
                Bitmap bm2 = ThumbnailUtils.extractThumbnail(bm, bm.getWidth() / inSampleSize, bm.getHeight() / inSampleSize);
                resultView.setImageBitmap(bm2);
                imgPath.setText(basePath+File.separator+imgs[position]);
            }
        });
        customGallery.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                File temp = new File(basePath+File.separator+imgs[position]);
                temp.delete();
                return false;
            }
        });
    }

  위 변경사항을 반영하여 compile/Run을 시켜봤더니 다행히 camera intent 실행 후 새로 찍은 사진이 gallery 에 반영되었다. 이제는 파일 삭제 후 Gallery update가 되도록 다른 method를 살펴봐야겠다.

(위 code의 실행결과)

     

 

  어제는 PopupWindow를 통해 main이 되는 View 에 말풍선처럼 popup을 띄우는 방식을 살펴보았다. 그런데 기존 App.들에서는 안내 메시지 창 자체가 따로 위에 뜨는 느낌이었던 터라 뭐 다른게 있나 싶어 찾아봤더니, AlertDialog를 사용한 것이었다. 이번엔 AlertDialog.builder를 통해 메시지 창을 만들어 동작시켜 보자.

1. 목적 : AlertDialog 의 사용법을 익히고 App.에 적용해 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - AlertDialog Reference (http://developer.android.com/reference/android/app/AlertDialog.html)
 2) 남시언의 문화지식탐험 - AlertDialog 생성 예제 (http://namsieon.com/313)

4. 과정
 1) 어제와 같이 AlertDialog를 띄울 ImageButton와 return 여부를 확인할 TextView를 activity_main.xml 에 생성한다. AlertDialog에서 돌아왔을 때 return 여부를 보여주기 위해서 TextView의 text는 비워둔다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <ImageButton
        android:id="@+id/imgbtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="15dp"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/returnval"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/imgbtn"/>
</RelativeLayout>

 

 2) App.의 전체적인 activity를 구성하는 MainActivity.java에 위의 ImageButton과 연결할 변수를 생성 및 연결하고, 해당 Button을 Click하면 AlertDialog 가 나타나도록 다음과 같이 설계한다.

public class MainActivity extends AppCompatActivity {

    public ImageButton imgBtn;
    public TextView returnVal;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imgBtn = (ImageButton)findViewById(R.id.imgbtn); // ImageButton의 activity 내 생성 및 연결
        returnVal = (TextView)findViewById(R.id.returnval);
        imgBtn.setOnClickListener(new View.OnClickListener() { // ImageButton을 Click시 AlertDialog가 생성되도록 아래과 같이 설계
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); // AlertDialog를 띄울 activity를 argument로 지정해야 한다.
                builder.setTitle("앱을 종료하시겠습니까?"); // AlertDialog.builder를 통해 Title text를 입력
                builder.setPositiveButton("네", new DialogInterface.OnClickListener() { // AlertDialog.Builder에 Positive Button을 생성
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish(); // App.의 종료. Activity 생애 주기 참고
                    }
                });
                builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() { // AlertDialog.Builder에 Negative Button을 생성
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        returnVal.setText("앱을 종료하지 않고 돌아왔음");
                        dialog.dismiss(); // "아니오" button이 받은 DialogInterface를 dismiss 하여 MainView로 돌아감
                    }
                });
                AlertDialog dialog = builder.create(); // 위의 builder를 생성할 AlertDialog 객체 생성
                dialog.show(); // dialog를 화면에 뿌려 줌
            }
        });
    }
}

(위의 code 실행 결과)

   

  

 

  앞서 기본 ListView code를 보다가 Adapter를 잘 이용하면 사용자의 입맛에 맞는 ListView layout을 적용하여 Custom ListView를 생성할 수 있지 않을까 싶었다. 실제로 검색해보니 많은 분들이 CustomListView를 구성하여 활용하였고, 그 방법 또한 구체적으로 설명을 하고 있었다. 이제 나도 Customizing을 해 봐야지.



[사진 1] Android Studio에서 해당 App의 External Libraries 중 Android API로 주어진 자료들을 뒤져보면 앞서 Adapter에 적용된 'simple_list_view_1.xml'을 사진과 같이 찾을 수 있었다. 이를 통해서 Custom ListView를 만들려면 ListView에 대한 .xml 작성이 필요할 것이라는 추측이 들었다.


1. 목적 : 기본 ListView를 customizing하여 사용해 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Using lists in Android (ListView) - Tutorial (http://www.vogella.com/tutorials/AndroidListView/article.html)
 2) berabue 블로그 - ListView의 사용 및 Customizing (http://berabue.blogspot.kr/2014/05/android-listview.html)
 3) 미르의 IT 정복기 (http://itmir.tistory.com/477)

4. 과정
 1) 원하는 모양의 ListView Layout을 만들기 위해 listview_custom.xml을 다음과 같이 만든다. 필요에 따라 TextView를 추가하고 그림(ImageView)를 빼는 등 사용자의 입맛에 맞게 제작하면 된다. 나는 ImageView 1개와 TextView 2개를 다음과 같이 구성하였다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:padding="10dp">

    <ImageView
        android:id="@+id/listview_pic01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:src="@mipmap/ic_launcher"/>
    <TableLayout
        android:id="@+id/text_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/listview_pic01"
        android:layout_gravity="center_horizontal"
        android:gravity="center"
        android:padding="5dp">
        <TableRow>
            <TextView
                android:id="@+id/listview_text01"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_toRightOf="@id/listview_pic01"
                android:padding="2dp"
                android:text="Text01" />
            </TableRow>
        <TableRow>
            <TextView
                android:id="@+id/listview_text02"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:padding="2dp"
                android:text="Text02"/>
        </TableRow>
    </TableLayout>

</RelativeLayout>

 

 

 

 

 

[사진 2] Custom ListView의 Layout으로 TextView  2개와 ImageView 1개로 위 사진과 같이 구성하려 한다.

 2) Layout을 Customizing했으면 해당 Layout을 받을 ListViewAdapter도 Customizing한다. BaseAdapter를 받아 필수 요소는 아래와 같이 Override하는데, 특히 해당 위치를 보여주는 getView() 같은 경우에는 미리 Custom ListView의 요소를 Holder하여 값을 받도록 지정한다.


public class CustomListViewAdapter extends BaseAdapter {
    // CustomListView의 원소를 각각 배열(Array)로 보관할 ArrayList.
   // 현재 Code에서는 선언만 했을 뿐 사용하지 않았다. 제작자의 기호에 따라 고치면 되겠다.

    private ArrayList<ImageView> m_ListPic;
    private ArrayList<String> m_List01;
    private ArrayList<String> m_List02;

    // CustomListView의 ImageView와 TextView들을 묶어서 Array로 보관할 ArrayList와 자료를 받을 Context, adapter 선언
    private Context mContext = null;
    public ArrayList<CustomListData> mListData = new ArrayList<CustomListData>();
    private CustomListViewAdapter mAdapter = this;

    public CustomListViewAdapter(Context mContext) { // CustomListViewAdapter 생성자
        super();
        this.mContext = mContext;
    }

    @Override
    public int getCount() { // List에 속한 원소의 갯수 count method
        return mListData.size();
    }

    @Override
    public Object getItem(int position) { // CustomListView의 position 위치에 있는 Item을 반환
        return mListData.get(position);
    }

    @Override
    public long getItemId(int position) { // CustomListView의 Item의 position을 반환
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) { // CustomListView를 목록으로 보여줌
        CustomListViewHolder holder; // 이전에 정의한 CustomListView의 원소에 맞춘 Holder를 선언
        if (convertView == null) { // 아직 CustomListView의 구성이 반영되지 않은 경우, 다음과 같이 listview_custom.xml을 반영
            holder = new CustomListViewHolder();

            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.listview_custom, null);

            holder.mListPicHolder = (ImageView) convertView.findViewById(R.id.listview_pic01);
            holder.mListText01Holder = (TextView) convertView.findViewById(R.id.listview_text01);
            holder.mListText02Holder = (TextView) convertView.findViewById(R.id.listview_text02);
            convertView.setTag(holder);
        }else{ // convertView에 구성이 이미 반영되어 있으면(null이 아니면) holder에 연결
            holder = (CustomListViewHolder) convertView.getTag();
        }

        CustomListData mData = mListData.get(position); // 위와 같이 layout 연결 후, position에 ListData를 받아와 
        if (mData.mListPic != null) { // ImageView에 입력될 data가 있을 경우 반영하고
            holder.mListPicHolder.setVisibility(View.VISIBLE);
            holder.mListPicHolder.setImageDrawable(mData.mListPic);
        }else{
            holder.mListPicHolder.setVisibility(View.GONE);
        }

        // holder에 text도 각각 반영. 필요에 따라 위와 같이 mText01/mText02 == null 여부를 따져 반영할 수 있다.
        holder.mListText01Holder.setText(mData.mText01);
        holder.mListText02Holder.setText(mData.mTest02);

        return convertView;
    }

    public void addItem(Drawable icon, String mTitle, String mDate){ // 필수 method는 아니나 ListView에 item을 반영하기 위해 필요
        CustomListData addInfo = null;
        addInfo = new CustomListData();
        addInfo.mListPic = icon;
        addInfo.mText01 = mTitle;
        addInfo.mTest02 = mDate;

        mListData.add(addInfo); // 앞에서 선언한 CustomListData를 받아 ArrayList에 통째로 add
    }

    public void remove(int position){ // 해당 position의 값을 제거하는 method
        mListData.remove(position);
        dataChange();
    }
    public void dataChange(){ // 위 remove(int position) method에서 CustomAdapter에 변경사항이 반영되도록 하는 method
        mAdapter.notifyDataSetChanged();
    }
}

public class CustomListData {
    /* CustomListView가 담을 객체에 대한 Class 생성 */
    // ImageView에 상응
    public Drawable mListPic;
    // TextView01에 상응
    public String mText01;
    // TextView02에 상응
    public String mTest02;
}
public class CustomListViewHolder {
    / * CustomListView의 구성요소에 대한 Holder 생성 */
    // Drawable을 받을 ImageView
    public ImageView mListPicHolder;
    // String mText01을 받을 TextView
    public TextView mListText01Holder;
    // String mText02를 받을 TextView
    public TextView mListText02Holder;
}

 3) 마지막으로 CustomListView를 App에 반영한다. 여기서는 MainActivity.java에 다음과 같이 사용하였다.


public class
MainActivity extends AppCompatActivity {

    public ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView)findViewById(R.id.listview); // 이전에 activity_main.xml에 선언되어 있던 ListView와 연결
        final CustomListViewAdapter mAdapter = new CustomListViewAdapter(this);
        listView.setAdapter(mAdapter);

        // CustomListViewAdapter의 addItem() method를 통해 Item 추가
        mAdapter.addItem(getResources().getDrawable(R.mipmap.apple), "과일", "사과");
        mAdapter.addItem(getResources().getDrawable(R.mipmap.pair), "과일", "배");
        mAdapter.addItem(getResources().getDrawable(R.mipmap.potato), "채소", "감자");
        mAdapter.addItem(getResources().getDrawable(R.mipmap.pumpkin), "채소", "호박");
        mAdapter.addItem(getResources().getDrawable(R.mipmap.onion), "채소", "양파");

        // CustomListView의 Item을 누를 경우 Toast를  출력하도록 작성
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
            @Override
            public void onItemClick(AdapterView<?> parent, final View view, int position, long id){
                CustomListData mData = mAdapter.mListData.get(position);
                Toast.makeText(getApplicationContext(), mData.mText01+" "+mData.mTest02+" is selected!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}


(위 Code의 실행 결과)

     

 

  앞에서 스마트폰에 내장되어 있던 기본 camera 기능을 intent로 불러와 사진을 촬영하는 기능을 살펴보았다. 동작이 제대로 되는 것을 확인했으니 촬영한 사진 경로를 가져다가 ImageView를 받아오는 것은 간단하리라 생각했었다. 그러나 생각은 그냥 생각일 뿐, 역시나 쉽게 되지 않았다. 단순히 Imageview 변수를 선언한 뒤 imageview01.setImageBitmap("Bitmap file"); 혹은 imageview01.setImageURI("URI값");와 같은 method를 사용하면 되리라 생각하고 해당 code를 추가하고 실행해 봤더니 그런 file은 찾을 수 없다는 Log가 나를 반기며, imageview는 텅 빈 채로 출력되는 것이 아닌가?

  아무래도 App에서 직접 camera hardware에 접근하여 사진을 촬영해 저장할 때처럼 FileOutputStream으로 data를 write하고 난 후 바로 해당 파일을 Bitmap 변수로 읽어 ImageView에 반영해야 겠다는 생각이 들었다(이런 방식으로 만들었을 때는 ImageView에 사진이 반영이 안되는 문제는 없었기 때문이다). 그러기 위해 기본 camera intent가 종료된 후 동작할 onActivityResult();를 @Override 하여 다음과 같이 작성하였다.

1. 목적 : Camera App을 직접 만들어 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료
 1) Android Developers - API Guide(http://developer.android.com/intl/ko/guide/topics/media/camera.html)
 2) smile8916님 블로그(http://smile8916.tistory.com/92)

4. 과정
 (시작하기에 앞서, 이전 게시글에 따른 Camera intent 관련 설정을 해주어야 한다.)
 1) activity_main.xml(layout을 설정)에 촬영 결과를 보여줄 ImageView를 다음과 같이 선언해 놓는다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView
        android:id="@+id/imgpath"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"/>

    <ImageView
        android:id="@+id/resultview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:layout_below="@id/imgpath" />
</LinearLayout

 

 2) MainActivity.java(앞서 camera intent를 불러들였던 Activity에 해당)에 intent가 정상적으로 동작한 후에 해당 경로로 FileOutputStream을 이용하여 data를 write 할 onActivityResult()를 다음과 같이 Override 한다.


    @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK) // intent 결과 정상적으로 마쳐질 경우
        {
            /* 앞서 수행한 intent request 값이 image capture와 관련된 것일 경우 다음을 수행한다. */
            if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE
            {
                ImageView resultView = (ImageView)findViewById(R.id.resultview); // .xml 에서 선언한 ImageView 변수를 Activity에도 선언한 후
                File tempImg = new File(fileUri.getPath()); // 앞서 지정한 경로에 파일의 경로에
                if(!tempImg.exists()){ // file이 존재하지 않으면
                    Log.d(TAG, "The image doesn't exist : " + tempImg.toString());
                    try {
                        FileOutputStream fos = new FileOutputStream(new File(fileUri.getPath())); // FileOutputStream 변수를 선언한 후
                        try {
                            fos.write(data.getExtras().getByte("data")); // Intent의 data 값을 write 하여 저장
                            fos.close(); // 그러고 나서 FileOutputStream을 종료한다.
                        } catch(IOException e){
                            Log.d(TAG, "IOException occur : " + e.getMessage());
                        }
                    } catch (FileNotFoundException e){
                        Log.d(TAG , "The image doesn't exist : "+ tempImg.toString());
                    }
                }
                Bitmap bm = BitmapFactory.decodeFile(fileUri.getPath()); // 해당 file을 Bitmap으로 decode 하여 저장한 후
                resultView.setImageBitmap(bm); // setImageBitmap() method로 앞서 선언한 ImageView에 촬영한 사진을 입력
                resultView.setRotation(imageViewRotation); // ImageView의 회전
            }
        }
    }

 

(위 code를 실행한 결과) 

 

 

 

 

 

 

 

 

 

 [Camera Intent 촬영 장면]

 [Camera Intent 종료 후 App 화면]

 [촬영된 사진 경로 확인]

 

 

 

 

 

 

 

  가끔 폰을 사용하다보면 Camera 기능을 여기저기에 응용해서 활용하고 있는 본인의 모습을 더러 볼 수 있을 것이다. 어떤 사람은 셀카에 포토샵처럼 보정하는 기능을 더해 사용하기도 하고, 또 어떤 사람은 놓치지 말아야 할 중요한 내용을 부랴부랴 받아 적기를 뒤로 하고 바로 사진으로 포착한다. 이렇듯 가끔 도촬이나 몰카같은 문제가 발생하지 않는다면 camera 기능은 참 유용하다는 생각이 든다. 이제 이 기능을 직접 구현하기 위해 공부해 보자. 오늘은 일단 Android 내의 기본 Camera App을 Intent로 불러다가 쓰는 방법을 살펴보려고 한다.

1. 목적 : Camera App을 직접 만들어 보자.

2. 개발 환경
 - PC : Windows 7, Android Studio 1.4.1(64-bit)
 - Phone : LG G pro Lollipop(API 21)

3. 참고자료 : Android Developers - API Guide(http://developer.android.com/intl/ko/guide/topics/media/camera.html)

4. 과정

 1) AndroidManifest.xml 내 Camera 기능 활용 permission 추가
   : 먼저 카메라 기능과 촬영한 사진을 저장하기 위해 카메라와 외부 저장소에 대한 permission을 설정해 놔야 한다.


 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.zip.camaraexample" >

    <!-- intent로 불러올 기본 camera feature 설정 -->
    <uses-feature android:name="android.hardware.camera2" />
    <!-- 사진을 외부 저장소(SD Card)에 저장할 수 있도록 외부 저장소 write permission 설정 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application ...

 2) MainActivity.java 내 onCreate()에 App을 실행하자 마자 camera intent가 실행되도록 기본 intent code 추가
    : 우선 Android Developer Guide에 기재된 기본 code를 추가해 본다. 그 결과 해당 경로를 따라가면 camera로 촬영한 사진 file이 생성되어 있었다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // create Intent to take a picture and return control to the calling application
    // 기본 내장 되어 있는 IMAGE CAPTURE 기능을 해당 app.에서 intent로 선언한다. 
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

    // 아래 정의한 capture한 사진의 저장 method를 실행 한 후
    fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // create a file to save the image
    // 먼저 선언한 intent에 해당 file 명의 값을 추가로 저장한다.
    
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // set the image file name

    // start the image capture Intent
    // 해당 intent를 시작한다. 
    startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);

}

/** Create a file Uri for saving an image or video */
/** 저장할 image file의 이름(URI)을 값을 반환. onCreate()에서 fileUri 값에 반영되는 값을 반환하도록 설계되어 있음 */
private static Uri getOutputMediaFileUri(int type){
    // 아래 capture한 사진이 저장될 file 공간을 생성하는 method를 통해 반환되는 File의 URI를 반환
    return Uri.fromFile(getOutputMediaFile(type));
}

/** Create a File for saving an image or video */
private static File getOutputMediaFile(int type){
    // To be safe, you should check that the SDCard is mounted
    // using Environment.getExternalStorageState() before doing this.

    // 외부 저장소에 이 App을 통해 촬영된 사진만 저장할 directory 경로와 File을 연결
    File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), "MyCameraApp");
    // This location works best if you want the created images to be shared
    // between applications and persist after your app has been uninstalled.

    // Create the storage directory if it does not exist
    if (! mediaStorageDir.exists()){ // 해당 directory가 아직 생성되지 않았을 경우 mkdirs(). 즉 directory를 생성한다.
        if (! mediaStorageDir.mkdirs()){ // 만약 mkdirs()가 제대로 동작하지 않을 경우, 오류 Log를 출력한 뒤, 해당 method 종료
            Log.d("MyCameraApp", "failed to create directory");
            return null;
        }
    }

    // Create a media file name
    // File 명으로 file의 생성 시간을 활용하도록 DateFormat 기능을 활용
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    File mediaFile;

    if (type == MEDIA_TYPE_IMAGE){
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
                "IMG_"+ timeStamp + ".jpg");
    } else if(type == MEDIA_TYPE_VIDEO) {
        mediaFile = new File(mediaStorageDir.getPath() + File.separator +
                "VID_"+ timeStamp + ".mp4");
    } else {
        return null;
    }
    return mediaFile; // 생성된 File valuable을 반환

   (위 code의 실행 결과) 아직 App의 layout.xml을 건드리지 않은 기본 상태이기 때문에 기본 camera intent 가 finish()된 후 Hello World!가 출력된다. 그리고 App에서 지정한 경로로 접근해 보면 camera intent로 촬영한 결과 file이 생성되어 있다.

 

 

 

 

 

 

 

 

 

 

 

   3) 기본 Camera intent로 촬영한 결과물을 App의 ImageView로 확인하기[추가사항] → 다음 글 확인

+ Recent posts