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의 실행 결과)

     

      

 

  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의 실행 결과)

      

 

 

 

 

 

 

 

+ Recent posts