이전에 Camera Intent 예제를 통해서 찍은 사진을 onActivityResult()를 이용하여 바로 확인하는 code의 기본 구조를 살펴 보았다. 이번에는 앞에서 구현한 기본 기능에다가 file.lists()를 더하여 특정 Directory 내 사진을 확인할 수 있는 Custom gallery 기능을 구현해보려 한다. 자료를 찾아보니 사진의 경로를 받아 Gallery 에 연결하는 방법이 있고 GridView에 연결하는 방법 등 여러가지가 있었는데, 오늘은 Gallery 기능을 활용해 보고자 한다.

1. 목적 : Gallery의 기본 구조를 살펴보고 Custom Gallery View를 구현해 보자.

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

3. 참고자료
 1) Envato tuts+ - Android Gallery View Tutorial (http://code.tutsplus.com/tutorials/android-sdk-displaying-images-with-an-enhanced-gallery--mobile-11130)
 2) Android People - Android Gallery, ImageView Example (http://www.androidpeople.com/android-gallery-imageview-example)

4. 과정
  이번 예시는 일전의 Camera Intent Example에 살을 붙여 사용했다.


 1) AndroidManifest.xml에 사진을 읽고 쓸 수 있도록 permission을 다음과 같이 추가한다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- 외부 저장소 read permission -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 외부 저장소 write permission -->

 

 2) activity_main.xml과 같이 App.에서 Gallery를 보여줄 곳에 다음과 같이 Gallery 변수를 추가한다.

 


<?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:layout_gravity="center"
    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">

    <!-- Camera Intent를 실행할 Button 선언 -->
    <Button
        android:id="@+id/takepicbtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:padding="5dp"
        android:text="Take Picture" />

    <!-- 사진의 경로를 받아다 String으로 보여줄 TextView 선언 -->
    <TextView
        android:id="@+id/imgpath"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/takepicbtn"
        android:padding="10dp"
        android:text="Hello World!"/>

    <!-- 지정한 경로 내의 사진들을 보여줄 Gallery 선언 -->
    <Gallery
        android:id="@+id/customgallery"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/imgpath"></Gallery>

    <!-- Gallery 내 선택한 사진을 크게 보여줄 ImageView 선언 -->
    <ImageView
        android:id="@+id/resultview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_below="@id/customgallery" />
</LinearLayout>

 

 3) Gallery의 Frame이 될 values/attrs.xml을 생성하여 다음과 같이 정의한다. 이 부분은 추후에 GalleryAdapter에서 적용된다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="GalleryTheme">
        <attr name="android:galleryItemBackground"/>
    </declare-styleable>
</resources>

 

 4) main이 되는 activity에서 gallery를 연결할 Adapter를 다음과 같이 customizing하여 CustomGalleryAdapter.java와 같은 파일로 정의한다. CustomGalleryHolder를 통해 ImageView를 연결할 수도 있지만 기본 example처럼 context를 받아와 ImageView를 연결하여 사용할 예정이다.

public class CustomGalleryAdapter extends BaseAdapter {
    int CustomGalleryItemBg; // 앞서 정의해 둔 attrs.xml의 resource를 background로 받아올 변수 선언
    String mBasePath; // CustomGalleryAdapter를 선언할 때 지정 경로를 받아오기 위한 변수
    Context mContext; // CustomGalleryAdapter를 선언할 때 해당 activity의 context를 받아오기 위한 context 변수
    String[] mImgs; // 위 mBasePath내의 file list를 String 배열로 저장받을 변수
    Bitmap bm; // 지정 경로의 사진을 Bitmap으로 받아오기 위한 변수

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

    public CustomGalleryAdapter(Context context, String basepath){ // CustomGalleryAdapter의 생성자
        this.mContext = context;
        this.mBasePath = basepath;

        File file = new File(mBasePath); // 지정 경로의 directory를 File 변수로 받아
        if(!file.exists()){
            if(!file.mkdirs()){
                Log.d(TAG, "failed to create directory");
            }
        }
        mImgs = file.list(); // file.list() method를 통해 directory 내 file 명들을 String[] 에 저장

        /* 앞서 정의한 attrs.xml에서 gallery array의 배경 style attribute를 받아옴 */
        TypedArray array = mContext.obtainStyledAttributes(R.styleable.GalleryTheme);
        CustomGalleryItemBg = array.getResourceId(R.styleable.GalleryTheme_android_galleryItemBackground, 0);
        array.recycle();
    }

    @Override
    public int getCount() { // Gallery array의 객체 갯수를 앞서 세어 둔 file.list()를 받은 String[]의 원소 갯수와 동일하다는 가정 하에 반환
        return mImgs.length;
    }

    @Override
    public Object getItem(int position) { // Gallery array의 해당 position을 반환
        return position;
    }

    @Override
    public long getItemId(int position) { // Gallery array의 해당 position을 long 값으로 반환
        return position;
    }


    // Override this method according to your need
    // 지정 경로 내 사진들을 보여주는 method.
   // Bitmap을 사용할 경우, memory 사용량이 커서 Thumbnail을 사용하거나 크기를 줄일 필요가 있음

   // setImageDrawable()이나 setImageURI() 등의 method로 대체 가능
    @Override
    public View getView(int index, View view, ViewGroup viewGroup)
    {
        // TODO Auto-generated method stub
      ImageView i = new ImageView(mContext); // Gallery array에 들어갈 ImageView 생성

        // 이 부분에서부터 'options.inJustDecodeBounds=false'까지는
        // Bitmap 사용시 나타나는 memory 부족 현상을 예방하기 위한 code. 경우에 따라서는 생략해도 가능하다.
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        int width = options.outWidth;
        int height = options.outHeight;
        int inSampleSize = 1;
        int reqWidth = 256;
        int reqHeight = 192;
        if((width > reqWidth) || (height > reqHeight)){
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;


        bm = BitmapFactory.decodeFile(mBasePath+ File.separator +mImgs[index], options);
        Bitmap bm2 = ThumbnailUtils.extractThumbnail(bm, 300, 300); // 크기가 큰 원본에서 image를 300*300 thumnail을 추출.
                                                                                               // 이를 통해 view 가 까맣게 보이는 null 값을 피할 수 있었다.
        i.setLayoutParams(new Gallery.LayoutParams(300, 300));
        i.setImageBitmap(bm2);
        i.setVisibility(ImageView.VISIBLE);

        i.setBackgroundResource(CustomGalleryItemBg); // Gallery 원소의 BackGround를 생성자에서 지정한 값으로 지정
        i.setScaleType(ImageView.ScaleType.FIT_CENTER);

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

 

 5) MainActivity.java와 같이 App. 에서 Gallery를 보일 부분에 다음과 같이 앞서 정의해 둔 Gallery와 Adapter를 선언하고 연결한다.


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

       // App.을 실행하자 마자 지정한 경로의 생성 및 접근에 용이하도록 아래와 같이 생성
        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");
//                return null;
            }
        }

        basePath = mediaStorageDir.getPath();

        imgPath = (TextView)findViewById(R.id.imgpath);
        resultView = (ImageView)findViewById(R.id.resultview);
        takePicBtn = (Button)findViewById(R.id.takepicbtn);
        // Button click시, Camera Intent를 불러 옴 
        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);
            }
        });

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

        customGallery = (Gallery)findViewById(R.id.customgallery); // activity_main.xml에서 선언한 Gallery를 연결
        customGalAdapter = new CustomGalleryAdapter(getApplicationContext(), basePath); // 위 Gallery에 대한 Adapter를 선언
        customGallery.setAdapter(customGalAdapter); // Gallery에 위 Adapter를 연결
        // Gallery의 Item을 Click할 경우 ImageView에 보여주도록 함
        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]);
            }
        });
        // Gallery의 Item을 LongClick할 경우 해당 File을 삭제하도록 함.
      // 이 부분에서 동작은 하나, 삭제 후 결과가 View에 반영이 안되어 추가 보완 필요

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

 

(위 Code의 실행 결과)

  

  

 

 

 

  ListView를 만들고 나니까 이번엔 ListView 안에 또 다른 ListView를 만들 필요가 생겼다. ListView 안에 객체로 다른 ListView를 선언해야 하나 하고 다시 여기저기 자료를 찾아봤는데(Google 만세!) 굳이 ListView를 두 번 선언하지 않고도 ExpandableListView라는 class를 이용하여 ParentList, ChildList 식으로 구성하면 되었다. 다만 Android Studio의 자동 완성 기능을 활용해 보니, ExpandableListViewAdapter의 Customizing이 필요했다.

1. 목적 : ExpandableListView의 기본 구조 및 사용법을 알아보자.

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

3. 참고자료
 1) Android Hive - Android Expandable List View Tutorial (http://www.androidhive.info/2013/07/android-expandable-list-view-tutorial/)
 2) 아라비안나이트 블로그 - ExpandableListView 제작 예시(http://arabiannight.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9CAndroid-ExpandableListView-%EB%A7%8C%EB%93%A4%EA%B8%B0)

4. 과정
 1) activity_main.xml(App에 보여질 전체적인 Layout)에 ExpandableListView를 정의하고,  ExpandableListView의 ParentList와 ChildList에 해당하는 list의 객체들을 각각 parent_listview.xml과 child_listview.xml로 다음과 같이 정의한다. 이 때, .xml 파일명의 사용자의 편의에 따라 다르게 쓸 수 있고 .java 에서 연결할 때 해당 .xml 파일명을 적용하면 된다.

<!-- activity_main.xml -->
<?
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">
    <ExpandableListView
        android:id="@+id/expandablelist"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:groupIndicator="@null">
    </ExpandableListView>
</RelativeLayout>
<!-- parent_listview.xml -->
<?
xml version="1.0" encoding="utf-8"?>

<LinearLayout 
   xmlns:
android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"  
   android
:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="10dp">
   <TextView
       android:id="@+id/parenttext"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="5dp"
       android:text="Parent TextView"
       android:textColor="#AA000000"/>
</LinearLayout

 

[xml Code] ExpandableListView에서 ParentListView에 해당하는 'parent_listview.xml'을 통해 parent는 각 원소별로 1개의 TextView로 이루어지게 설계했음을 알 수 있으며, ParentList의 원소를 선택했을 경우 나타날 ChildListView는 'child_listview.xml'에서 보이는 바와 같이 1개의 ImageView와 1개의 TextView로 구성되도록 설계했다.
<!-- child_listview.xml -->
<?
xml version="1.0" encoding="utf-8"?>

<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="10dp">
   <ImageView
       android:id="@+id/child_item_icon"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="10dp"
       android:src="@mipmap/ic_launcher"/>
   <TextView
       android:id="@+id/childtext"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_toRightOf="@id/child_item_icon"
       android:layout_gravity="center_vertical"
       android:gravity="center_vertical"
       android:padding="5dp"
       android:text="Child TextView"
       android:textColor="#88000000"/>
</RelativeLayout>

 

 2) 1)의 ExpandableListView가 적용될 CustomAdapter를 다음과 같이 정의한다. 상속받은 method를 @Override 해야함을 잊으면 안된다. 그리고 아래 보기에서 나는 생성자를 통해서 받는 childList를 HashMap 변수를 통해서 연결했지만, 다른 참고자료에서 처럼 List<>등으로 바꾸어 입맛에 맞게 편집하면 된다.


public class
CustomExpandableListViewAdapter extends BaseExpandableListAdapter {

    private Context mContext;
    private ArrayList<String> mParentList;
    private ArrayList<ChildListData> mChildList;
    private ChildListViewHolder mChildListViewHolder;
    private HashMap<String, ArrayList<ChildListData>> mChildHashMap;

    // CustomExpandableListViewAdapter 생성자
    public CustomExpandableListViewAdapter(Context context, ArrayList<String> parentList, HashMap<String, ArrayList<ChildListData>> childHashMap){
        this.mContext = context;
        this.mParentList = parentList;
        this.mChildHashMap = childHashMap;
    }

    /* ParentListView에 대한 method */
   
@Override
    public String getGroup(int groupPosition) { // ParentList의 position을 받아 해당 TextView에 반영될 String을 반환
        return mParentList.get(groupPosition);
    }
    
    @Override
    public int getGroupCount() { // ParentList의 원소 개수를 반환
        return mParentList.size();
    }
    
    @Override
    public long getGroupId(int groupPosition) { // ParentList의 position을 받아 long값으로 반환 
        return groupPosition;
    }
    
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { // ParentList의 View
        if(convertView == null){
            LayoutInflater groupInfla = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            // ParentList의 layout 연결. root로 argument 중 parent를 받으며 root로 고정하지는 않음
            convertView = groupInfla.inflate(R.layout.parent_listview, parent, false);
        }

        // ParentList의 Layout 연결 후, 해당 layout 내 TextView를 연결
        TextView parentText = (TextView)convertView.findViewById(R.id.parenttext);
        parentText.setText(getGroup(groupPosition));
        return convertView;
    }

    /* 여기서부터 ChildListView에 대한 method */
    @Override
    public ChildListData getChild(int groupPosition, int childPosition) { // groupPostion과 childPosition을 통해 childList의 원소를 얻어옴
        return this.mChildHashMap.get(this.mParentList.get(groupPosition)).get(childPosition);

    }
    
    @Override
    public int getChildrenCount(int groupPosition) { // ChildList의 크기를 int 형으로 반환
        return this.mChildHashMap.get(this.mParentList.get(groupPosition)).size();

    }
    
    @Override
    public long getChildId(int groupPosition, int childPosition) { // ChildList의 ID로 long 형 값을 반환
        return childPosition;
    }
    
    @Override 
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        // ChildList의 View. 위 ParentList의 View를 얻을 때와 비슷하게 Layout 연결 후, layout 내 TextView, ImageView를 연결
        ChildListData childData = (ChildListData)getChild(groupPosition, childPosition);
        if(convertView == null){
            LayoutInflater childInfla = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = childInfla.inflate(R.layout.child_listview, null);

            mChildListViewHolder = new ChildListViewHolder();
            mChildListViewHolder.mChildListViewIcon = (ImageView)convertView.findViewById(R.id.child_item_icon);
            mChildListViewHolder.mChildListViewText = (TextView)convertView.findViewById(R.id.childtext);
            convertView.setTag(mChildListViewHolder);
        } else{
            mChildListViewHolder = (ChildListViewHolder)convertView.getTag();
        }

        mChildListViewHolder.mChildListViewText.setText(getChild(groupPosition, childPosition).mChildText);
        mChildListViewHolder.mChildListViewIcon.setImageDrawable(getChild(groupPosition, childPosition).mChildItem);
        return convertView;

    }

    @Override
    public boolean hasStableIds() { return true; } // stable ID인지 boolean 값으로 반환

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } // 선택여부를 boolean 값으로 반환

 

 3) MainActivity.java(ExpandableListView에 객체를 입력하고 동작에 대한 code 작성)을 다음과 같이 coding한다.

public class MainActivity extends Activity {
    public ExpandableListView expandableListView; // ExpandableListView 변수 선언
    public CustomExpandableListViewAdapter mCustomExpListViewAdapter; // 위 ExpandableListView를 받을 CustomAdapter(2번 class에 해당)를 선언
    public ArrayList<String> parentList; // ExpandableListView의 Parent 항목이 될 List 변수 선언
    public ArrayList<ChildListData> fruit; // ExpandableListView의 Child 항목이 될 List를 각각 선언
    public ArrayList<ChildListData> vegetables;
    public ArrayList<ChildListData> etc;
    public HashMap<String, ArrayList<ChildListData>> childList; // 위 ParentList와 ChildList를 연결할 HashMap 변수 선언

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // activity_main.xml을 MainActivity에 연결

        // ExpandableListView의 ParentList에 해당할 항목을 입력
        parentList = new ArrayList<String>();
        parentList.add("과일");
        parentList.add("채소");
        parentList.add("기타");

        // 앞서 ParentList에 연결할 ChildList 항목을 선언 및 입력
        ChildListData apple = new ChildListData(getResources().getDrawable(R.mipmap.apple), "사과");
        ChildListData pair = new ChildListData(getResources().getDrawable(R.mipmap.pair), "배");
        ChildListData persimmon = new ChildListData(getResources().getDrawable(R.mipmap.persimmon), "감");
        fruit = new ArrayList<ChildListData>();
        fruit.add(apple);
        fruit.add(pair);
        fruit.add(persimmon);

        ChildListData onion = new ChildListData(getResources().getDrawable(R.mipmap.onion), "양파");
        ChildListData cabbage = new ChildListData(getResources().getDrawable(R.mipmap.cabbage), "양배추");
        ChildListData potato = new ChildListData(getResources().getDrawable(R.mipmap.potato), "감자");
        ChildListData sweetPotato = new ChildListData(getResources().getDrawable(R.mipmap.sweetpotato), "고구마");
        ChildListData pumpkin = new ChildListData(getResources().getDrawable(R.mipmap.pumpkin), "호박");
        vegetables = new ArrayList<ChildListData>();
        vegetables.add(onion);
        vegetables.add(cabbage);
        vegetables.add(potato);
        vegetables.add(sweetPotato);
        vegetables.add(pumpkin);

        ChildListData seaweed = new ChildListData(getResources().getDrawable(R.mipmap.seaweed), "미역");
        ChildListData bread = new ChildListData(getResources().getDrawable(R.mipmap.bread), "빵");
        ChildListData mackerel = new ChildListData(getResources().getDrawable(R.mipmap.mackerel), "고등어");
        etc = new ArrayList<ChildListData>();
        etc.add(seaweed);
        etc.add(bread);
        etc.add(mackerel);

        // 위에서 선언한 ParentList와 ChildList를 HashMap을 통해 
        childList = new HashMap<String, ArrayList<ChildListData>>();
        childList.put(parentList.get(0), fruit);
        childList.put(parentList.get(1), vegetables);
        childList.put(parentList.get(2), etc);

        // 앞서 정의해 놓은 ExpandableListView와 그 CustomAdapter를 선언 및 연결한 후
      // ExpandableListView에 대한 OnClickListener 등을 선언

        expandableListView = (ExpandableListView)findViewById(R.id.expandablelist);
        mCustomExpListViewAdapter = new CustomExpandableListViewAdapter(this, parentList, childList);
        expandableListView.setAdapter(mCustomExpListViewAdapter);
        expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
            @Override
            public void onGroupExpand(int groupPosition) {
            }
        });
        expandableListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
            @Override
            public void onGroupCollapse(int groupPosition) {
            }
        });
        expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
            @Override
            public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
                return false;
            }
        });
    }
}

 

(위 Code의 실행 결과)

   

    

 

  이전에 CustomListView를 만들다 보니 ListView에 기본적으로 속할 사진을 미리 project 내부에 등록할 필요가 있었다. 방법을 까먹기 전에 기록해 두려 한다.

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

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

3. 과정
 1) App.의 project 내에서 [res] - [drawable] directory를 찾아 우클릭을 누르면 나오는 menu들 중에 [New] - [Image Asset]을 찾아 들어간다.

 

 2) [Image Asset] 창에서 'Asset Type'으로 'launcher Icons'를 선택한 뒤, Image file의 경로로 원하는 image를 찾아 지정한다. 그리고 Resource name으로 App. 개발 중 불러들일 때 사용할 변수명을 입력하고 [Next] 버튼을 눌러 적용 사항을 확인하고 [Finish] 한다.

  

 3) 다음과 같이 image를 반영할 때, 앞에서 지정한 'Resource Name'을 사용한다. 앞서 CustomListView에서 예시를 빌려 왔다.

 

  앞서 기본 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의 실행 결과)

     

 

  App을 만들다 보니 목록을 만들어 사용자가 목록에서 선택한 값을 받아 처리할 일이 생겼다. 일단 가볍게 ListView의 기본 구조를 살펴보고 그 동작 원리를 파악하여 응용을 하려 한다.

1. 목적 : ListView의 사용법을 습득해 보자.

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) activity_main.xml(layout을 설정)에 ListView layout을 다음과 같이 선언해 둔다.


<?
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">

      <ListView
          android:id="@+id/listview"
          android:layout_width="match_parent"
          android:layout_height="wrap_content" />

</RelativeLayout>

 

 2) MainActivity.java(ListView의 Activity와 관련된 code 작성)에 App 생성시 ListView의 동작에 관한 code를 다음과 같이 작성한다. 이 때, 다른 variable과는 달리 ListView는 ListView 자체의 layout을 위 activity_main.xml과 같은 app의 layout에 적용시키는 역할을 하는 "Adapter"가 필요하다.


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에 대하여 ListView 변수 정의
        String[] listVal = new String[] { "감자", // listView에 반영될 항목을 String[] (배열, Array) 변수로 정의
                "사과",
                "배",
                "양파",
                "호박"
        };

        final ArrayList<String> list = new ArrayList<String>(); // String 변수들을 받는 ArrayList 변수를 선언하여 위 String[] 변수를 반영
        for (int i = 0; i < listVal.length; ++i) {
            list.add(listVal[i]);
        }

        // ArrayAdapter를 통해 Android API platform의 'res' library에 simple_list_item_1.xml에 상응하는 형태로 위의 list를 listView에 반영
        final ArrayAdapter<String> mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list);
        listView.setAdapter(mAdapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){ // listView에 반영된 item을 Click할 경우 다음 동작을 수행
            @Override
            public void onItemClick(AdapterView<?> parent, final View view, int position, long id){
                final String item = (String) parent.getItemAtPosition(position); // 선택한 값을 String 문자열로 받아들여 Toast 출력
                Toast.makeText(getApplicationContext(), item + " 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로 확인하기[추가사항] → 다음 글 확인

  어제 .jar 파일의 repack 작업을 하면서 jar 명령어에 대해 정리할 필요가 있었다. 아무래도 각자 다른 개발환경 아래의 컴퓨터를 서로 다른 사람들이 사용하고 있으니 같은 명령어라도 다른 결과물이 나온다는 생각이 들었기 때문이다. 이제 무조건 [Ctrl] + [C] & [Ctrl] + [V] 하기 보다는 과정 하나하나를 이해하려고 노력해야지. 우선은 jar 부터 시작하자.

  먼저 .jar file 자체의 정의를 보니 Java platform에서 app과 같은 응용 프로그램이나 library로 사용할 수 있도록 여러 파일을 묶어 놓은 패키지(packaging) file format으로, 일종의 .zip과 같은 파일 묶음과 같은 것이었다. 그렇다 보니 지난 번에 참조했던 조언과 같이 unzip 명령이 jar file을 풀 수 있었던 모양이다.

"JAR(Java Archive, 자바 아카이브)는 소프트웨어에서 수많은 자바 클래스 파일과 연관 메타데이터, 리소스(텍스트, 그림 등)을 하나의 파일로 모아서 자바 플랫폼응용 소프트웨어라이브러리를 배포하기 위한 패키지 파일 포맷이다.[1]

  JAR 파일은 실제로 ZIP 파일 포맷으로 이루어진 압축 파일로서, 파일 확장자는 .jar이다. 컴퓨터 사용자들은 JDK에 포함된 jar 명령어를 이용하여 JAR 파일을 만들거나 압축을 풀 수 있다. 또, zip 도구를 사용할 수도 있으나 압축 시에는 매니페스트 파일이 처음이어야 하는 경우가 있어서 zip 파일 헤더의 엔트리 순서가 중요하다. JAR 안에서 파일 이름들은 유니코드 텍스트로 되어 있다."

[참고] Wikipedia - https://ko.wikipedia.org/wiki/JAR_(%ED%8C%8C%EC%9D%BC_%ED%8F%AC%EB%A7%B7)

 

  이렇다 보니 .jar file을 사용하고 또 생성하기 위해서는 Java platform 환경이 갖추어져 있어야 하는데, 그것이 JDK(Java Development Kit)이다. 요즘은 인터넷을 활용하는데도 Java platform이 필요하다보니 웬만해서는 다 설치되어 있겠지만, 혹시나 깔끔하게 다시 설치해야 한다면 Oracle 홈페이지 내 Java SE Development Kit를 download하여 설치하면 되겠다(2015년 11월 27일 현재 http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 에 방문하면 Java SE Development Kit 8u65/8u66 을 Linux나 Windows 32-bit(x86)/64-bit(x64)와 같은 사용자의 사용 환경에 맞춰 설치할 수 있다).

 

  이렇게 설치하고 나면, 해당 JDK를 사용자가 어느 위치에서나 편하게 활용할 수 있게끔 환경변수를 설정해 주어야 하는데 Windows 7 기준으로 1) [시작] - [컴퓨터] 오른쪽 클릭해 메뉴 도출 - [속성] 순으로 접근하던가 2) [제어판] - [시스템] 순으로 들어가면 나타나는 창에서 왼쪽 상단의 메뉴 중 맨 끝에 있는 [고급 시스템 설정]을 클릭해 [시스템 속성 - 고급 탭] 을 띄워 창 하단의 [환경 변수] 버튼을 클릭, 그리고 path(혹은 Path, PATH)변수에 java.exe에 접근할 수 있도록 (컴퓨터 내 jdk 설치 directory)\bin을 추가한다(혹은 추가되었는지 확인한다). 혹시 이 과정이 귀찮다면 java.exe가 설치되어 있는 폴더 내에서 jar 명령어를 동작시키면 되긴 했는데, 환경변수를 설정하는 쪽이 추후에 더 편리했다.

   

 

  이제 console(ex. cmd)을 띄워, jar 명령어를 다음과 같이 활용한다. 기본적으로 jar {ctxu}[vfm0M] (jar file name).jar (대상이 되는 directory name) (.jar file이 생성되는 지점)으로 구성되는데 주로 중괄호 {}에 해당하는 명령어가 c이면 jar 압축, x이면 jar 압축 해제, 대괄호 []에 해당하는 명령어로 v와 조합할 경우 해당 c 또는 x 명령 수행 과정을 console에 나타내며, f의 경우 해당 manifest를 포함함을 의미한다.

jar {ctxu}[vfm0M] [jar-file] [manifest-file] [-C dir or files] ...

 

  1) {ctxu} 필수 option
    - c : archive 파일(.jar file)을 생성
    - t : [jar-file]에 해당하는 archive file의 content list를 설정
    - x : 뒤에 [jar-file]에 해당하는 archive 파일(.jar file)의 압축 해제
    - u : [jar-file]에 해당하는 archive 파일 내에 들어있는 파일을 수정

  2) [vfm0M] 앞의 {ctxu}의 동작을 도와주는 option
    - v : console 창으로 진행 사항에 관한 메시지를 모두 출력. 안 쓸 경우, 중요 error message만 console 창에 뿌려주었다.
    - f : archive 될 파일 이름을 지정
    - m : manifest 파일로부터 정보를 포함
    - 0 : 저장(묶기)만 하고, zip으로 압축지 않음
    - M : 추가되는 entry에 대해 manifest 파일을 생성하지 않음

  3) [jar-file] archive 대상이 되는 .jar file의 이름

  4) [manifest-file] 정보를 포함할 manifest file의 이름

  5) [-C dir or files] archive 할 directory 명 혹은 file 명. 명기한 순서대로 처리.

 

  (사용 예시) 

 

  Android app으로 word 파일(.docx/.doc)를 볼 필요가 생겨서 관련된 library를 찾다가 먼저 Apache POI를 발견했다. 아직 App 개발에 익숙하지 않아 개발 환경을 꾸미는 것만으로도 구글링하면서 진땀을 뺐는데, 겨우겨우 관련 .jar file을 import하고 compile을 해보니 이게 웬 걸. duplicate class 어쩌구 하는 error가 떡하니 뜨는게 아닌가?

 

 

  구글링으로 해당 error의 이유를 보니 애초에 배포된 xmlbeans-2.6.0.jar 내에 중복된 이름의 class가 있어 그렇다고 한다. 이 문제를 해결하려면 직접 xmlbeans-2.6.0.jar를 푼 다음(unzip) 다시 jar로 repack을 해야한다는데, 일단 개발환경 내에 jar 명령어가 동작할 수 있도록 JDK를 설치해야 했다(JDK는 android studio의 구동을 위해 필히 설치해야하는 항목이었으므로 보통은 C:\Program Files\Java 내에 jdk1.x.x_xx 따위의 directory로 정리되어 설치되어 있을 것이다). 그리고 다른 경로에서도 java.exe를 활용할 수 있도록 window의 Path 환경변수에 (내 컴퓨터 내 Java jdk 경로)\bin을 추가해 놓아야 했다.

   

  

 

  그리고 POI-bin 내에 xmlbeans-2.6.0.jar 파일을 찾아 압축을 푼다. 나는 여기서 7-Zip을 활용했다. 압축을 풀다 보면 문제가 됐었던 중복된 class 파일들에 대해 대체여부를 묻는 창이 뜨는데 '모두 예(A)'를 선택하여 대체했다.

 

  다음으로 콘솔을 실행하여 압축을 푼 해당 directory에 들어가서 'jar cf (repack할 jar file명).jar -C *'을 수행했다. 다른 분들의 조언에 따르면 'jar cf (repack할 jar file명).jar -C * .'을 해야한다는데, 내 컴퓨터 환경에서는 그렇게 하면 환경 자체의 문제점이 있는 모양인지 손상된 jar file이 생성되어 제대로 동작하지 않았다. 혹시나 나와 비슷한 문제를 겪고 있는 분들이 있다면 jar 명령어에 관한 reference를 찾아 살짝 바꿔서 실행해 보면 원하는 결과가 나올지도 모르겠다.

 

 

  이제 새로 생성된 xmlbeans-2.6.0-repack.jar를 app library에 추가하여 compile/run을 시켜보았다. 그 결과 실행은 되긴 했으나 app이 XWPFDocument(); method를 거치는 순간 중단되는 문제가 발생하였다. 그러나 이 문제는 xmlbeans-2.6.0.jar 파일의 유무와 상관없이 그전에도 일어났던 터라 해당 파일과는 상관이 없으리라 생각된다. 이제 이 다음 문제를 해결해야지.

 

 

1. 목표 필요한 File을 생성하고 해당 File의 경로(Path)를 확인한다.

2. 개발 환경
  - Android Studio 1.4.1(64bit)
  - LG G pro (LolliPop)

3. App Coding 과정
 1) AndroidManifest.xml 의 수정
   : 해당 App.이 File을 read/write할 권한을 주기 위해 <uses-permission ...>을 다음과 같이 준다. 이 과정을 수행하지 않을 경우, compile 및 실행은 문제 없이 되나 실제 File을 생성되지 않는다.

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

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

    <application ...

 

 2) .xml file에 파일 생성 여부를 보여 주기 위한 Layout 설정
   : 생성된 File의 경로를 보여주기 위해 TextView를 사용하기로 했다. 이 부분은 추후에 List 등을 이용하여 icon화 시키는 등 응용하는 방법을 배워야 겠다.

<?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/pathview01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path01"/>
    <TextView
        android:id="@+id/pathview02"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path02"/>
    <TextView
        android:id="@+id/pathview03"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="Path03"/>

</LinearLayout>

 

 3) .java file을 통한 Activity 설정
   a. external storage에 file directory 생성
    : File 생성자를 통해 다음과 같이 file을 저장할 directory를 생성한다.

File storeDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "MyDocApp");
if (!storeDir.exists()) {
    if (!storeDir.mkdirs()) {
        Log.d("MyDocApp", "failed to create directory");
        return;
    }
}

 

  b. file 생성
   : 실제 생성할 File을 다음과 같이 생성한다. file name은 해당 명령이 수행되는 시간을 따와서 쓸 수 있도록 SimpleDateFormat().format(new Date());를 사용한다.

timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
temp = new File(storeDir.getPath() + File.separator + "test_"+ timeStamp + ".docx");
if(temp == null){
    Log.d(TAG, "Error at creating .docx file, check storage permissions :");
    return;
}

 

  c. file 생성 경로의 확인
    : 생성된 파일의 경로를 String으로 찍어 봄으로서 파일의 생성 여부를 확인한다. 이 때, 'file 생성자 변수명'.getPath(); 혹은 .getAbsolutePath();, .getCanonicalPath(); 등 file 내 경로 확인 함수가 여러 개가 있었는데 string 값에는 차이가 없었으나, 실행 장치나 compile 방식에 따라 차이가 있을 수도 있다(참고 : http://egloos.zum.com/entireboy/v/4196432).

pathView01.setText(temp.getAbsolutePath());
pathView02.setText(temp.getPath());
try {
    pathView03.setText(temp.getCanonicalPath());
} catch (IOException e) {
    Log.d(TAG, "Error at creating .docx file, check storage permissions :");
    return;
}

 

4. App 생성 결과
  아래 사진과 같이 file path를 확인했는데 android 내에서는 getPath()나 getAbsolutePath(), getCanonicalPath();들의 차이가 확인되지 않았다. 다른 사람들을 블로그를 보면 다소 차이가 있었는데, 아무래도 나는 file을 생성할 때 경로를 모두 찍어 입력해서 그렇지 않은가 싶다.

 

+ Recent posts