AdapterView로 Custom List 만들기 – 1. AdapterView 기본 구조

 

 

AdapterView로 Custom List 만들기 – 1. AdapterView 기본 구조

 

역시 안드로이드 UI 의 꽃은 Custom view.

요즘 각광받는 CardFlip 애니메이션 적용해 보다가 아예 커스텀 리스트로 만들어 보는게 어떨까 싶어서 몇 가지 작업을 해봤습니다.

 

일단 Strechable list 로 워밍업을 좀 하고 (스크롤 될 때 아이템에 약간의 효과가 가미된 커스텀 리스트, 동영상 참조) 추후 다양한 애니메이션 효과를 가진 커스텀 리스트를 게시할 예정입니다.

 


 

Strechable List :

 – 리스트를 드래그하면 아이템이 살짝 작아졌다가 멈출 때 원복되는 리스트

 – 안드로이드 기본 리스트처럼 사용할 수 있도록 AdapterView 를 상속

 – 총 4편에 걸쳐 소스를 분석하고, 각 편마다 해당하는 예제소스를 제공합니다.

 – 소스 이해를 위해선 안드로이드 프로그래밍에 대한 기본 지식이 필요합니다.

 

소스 다운로드 : (최종 소스가 필요하시면 4번째 강좌의 소스를 쓰세요.)


AdapterView로 Custom List 만들기 [4] – 애니메이션 효과

AdapterView로 Custom List 만들기 [3] – Velocity, Runnable

AdapterView로 Custom List 만들기 [2] – Touch event, Scrolling

AdapterView로 Custom List 만들기 [1] – AdapterView 기본 구조 <<<<< 현재 위치 


 

1. 테스트 앱 준비

 

테스트에 사용할 레이아웃 만들어 주고…

StrechableList 는 우리가 만들 Custom view

 

 

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

 

    <TextView

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="@string/hello_world" />

    

    <com.example.cardflipper.StretchableList

        android:id="@+id/layout_card"

        android:layout_width="match_parent"

        android:layout_height="match_parent" />

 

</LinearLayout>

 

 

테스트용 Activity 코드도 만들었습니다.

DummyData 클래스를 사용하는 ArrayAdapter를 만들어서 StrechableList에 끼웁니다.

리스트 아이템 별로 구분이 잘 되라고 배경색에 조금씩 변화를 줬습니다.

 

 

 public class MainActivity extends Activity {

 

    private Context mContext;

    

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        

        mContext = getApplicationContext();

 

        StretchableList listview = (StretchableList) findViewById(R.id.layout_card);

        

        ArrayList<DummyData> itemList = new ArrayList<DummyData>();

        for(int i=0; i<20; i++) {

            DummyData temp = new DummyData("Card title " + Integer.toString(i),

                    "Card description. Test card number #" + Integer.toString(i),

                    Color.rgb(128 + i*5, 64 + i*5, 64 + i*5) );

            itemList.add(temp);

        }

        

        CardAdapter adapter = new CardAdapter(mContext, R.layout.card, itemList);

        

        listview.setAdapter(adapter);

        

    }

 

    /* 테스트 용 데이터 클래스 */

    public class DummyData {

        public String mName = null;

        public String mDesc = null;

        public int mBGColor = 0;

        

        public DummyData(String name, String desc, int bgcolor) {

            this.mName = name;

            this.mDesc = desc;

            this.mBGColor = bgcolor;

        }

    }

    

    /* 테스트 용 어댑터 클래스 */

    private class CardAdapter extends ArrayAdapter<DummyData> {

        private Context mContext;

        private ArrayList<DummyData> mArrayList;

        public CardAdapter(Context c, int resid, ArrayList<DummyData> itemList) {

            super(c, resid, itemList);

            mContext = c;

            mArrayList = itemList;

        }

        

        @Override

        public int getCount() {

            return mArrayList.size();

        }

        @Override

        public DummyData getItem(int position) { 

            return mArrayList.get(position); 

        }

        @Override

        public long getItemId(int position) {

            return position;

        }

        

        @Override

        public View getView(int position, View convertView, ViewGroup parent) {

            View v = null;

            DummyData data = mArrayList.get(position);

            if (v == null) 

            {

                LayoutInflater li = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

                v = li.inflate(R.layout.card, null);

                

                LinearLayout layout_row = (LinearLayout) v.findViewById(R.id.layout_row);

                TextView title = (TextView) v.findViewById(R.id.card_title);

                TextView description = (TextView) v.findViewById(R.id.card_desc);

                

                title.setText(data.mName);

                description.setText(data.mName);

                layout_row.setBackgroundColor(data.mBGColor); // 아이템 구별되도록 배경색 변화

                

            }

            return v;

        }

    }

}

 

 

 

2. Strechable List (Custom view) 기본 뼈대

 

 

 

public class StretchableList extends AdapterView {

 

    private Adapter mAdapter = null;

 

 

    public StretchableList(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

 

    @Override

    public void setSelection(int position) {

    }

 

    @Override

    public View getSelectedView() {

        return null;

    }

    

    @Override

    public Adapter getAdapter() {

        return mAdapter;

    }

    

 

    @Override

    public void setAdapter(Adapter adapter) {

        mAdapter = adapter;

        removeAllViewsInLayout();    // 모든 자식 뷰 제거

        requestLayout();            // 레이아웃 위치 계산, onLayout() 호출

    }

 

}

 

 

 

 

안드로이드 기본 리스트와 유사하게 사용할 수 있도록 AdapterView 를 상속 받았습니다.

그리고 상속 후 기본으로 구현해야 할 method 들을 구현.

selection은 focus와 관련된 내용인데 여기서는 사용하지 않으므로

 

setSelection(), getSelectedView() 는 계속 비워둘 예정입니다.

 

Adapter를 설정하는 기본 method는 setAdapter() 입니다.

setAdapter() 에서 입력된 adapter를 mAdapter에 저장해두고,

새로운 adapter가 설정되므로 기존 child view 들을 지우고,

화면을 새로 구성할 수 있도록 removeAllViewsInLayout(), requestLayout() 를 호출합니다.

 

requestLayout() 호출이 되면 화면을 새로 그리기 위한 모든 작업이 시작됩니다.

 

 

3. getView, Add view, Measuring, Layout

 

리스트는 각각의 아이템이 가지고 있는 view 를 얻어오기 위해

(안드로이드 프로그래머에게 익숙한) Adapter 의 getView()를 호출합니다.

Adapter에서 얻어온 view는 리스트에 child view로 붙이고,

리스트의 Layout과 child view의 layout의 사이즈가 서로 조정될 수 있도록 measuring 과정을 거치고,

리스트 상에서 정확한 위치에 보여지도록 Layout 위치 조정을 해야합니다.

 

1. Adapter의 getView() 호출로 child를 가져옴

2. 가져온 view를 리스트에 child view로 붙임

3. Measuring – 리스트와 child view 간의 사이즈 조정

4. Layout 조정 – 현재 리스트의 상태에 따라 Child view 들의 위치 조정

 

 

     /**

     * 화면에 표시될 item을 추가/삭제, item의 위치를 설정

     * 사용자 touch, adapter data 변화에 따라 layout이 변경되는 시작점. 

     */

 

    @Override

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        super.onLayout(changed, l, t, r, b);

 

        if (mAdapter == null)

            return;

 

        if (getChildCount() == 0) {        // No child registered.

            int position = 0;

            while ( position < mAdapter.getCount() – 1) {

                final View child = mAdapter.getView(position, null, this);    // [1]

                addViewAndMeasure(child, -1);    // [2], [3]

                position++;

            }

        }

 

        layoutItems();        // 자식 뷰의 화면 내 위치를 설정 (child.layout() 호출)        // [4]

        invalidate();

    }

 

 

 

    private static final int ADD_VIEW_DIRECTION_TOP = 1;

    private static final int ADD_VIEW_DIRECTION_BOTTOM = 2;

 

 

    /**

     * child view 설정, measuring (부모와 자식 view 간의 size 협상)

     *

     * @param child The view to add

     */

    private void addViewAndMeasure(View child, int direction) {

        LayoutParams params = child.getLayoutParams();

        if (params == null) {

            params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

        }

        int index = 0;

        if(direction == ADD_VIEW_DIRECTION_TOP)

            index = 0;

        else

            index = -1;

        child.setDrawingCacheEnabled(true);        // drawing cache 사용. drawChild() 참고.

        addViewInLayout(child, -1, params, true);       // [2]

 

        int itemWidth = getWidth();

        child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);    // [3]

    }

 

 

 

    /**

     * 자식 뷰가 표시될 화면 내 위치 설정

     * 자식 뷰 크기를 확정하기 위해 measuring 과정이 선행 되어야 함.

     */

    private void layoutItems() {        // [4]

        int top = 0;

     

        for (int index = 0; index < getChildCount(); index++) {

            View child = getChildAt(index);

     

            int width = child.getMeasuredWidth();    // Measuring 과정이 끝난 후 확정된 크기

            int height = child.getMeasuredHeight();

            int left = (getWidth() – width) / 2;

     

            child.layout(left, top, left + width, top + height);

            top += height;

        }

    }

 

 

    /**

     * 자식 뷰를 그리는 루틴.

     * 자식 뷰에 특별한 효과를 넣고 싶다면 이 메소드를 상속해서 수정

     */

    @Override

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

        // Drawing cache에서 자식 뷰의 bitmap을 얻어옴.

        // child.setDrawingCacheEnabled(true);        //–> 이 설정이 선행되어야 함. 

        final Bitmap bitmap = child.getDrawingCache();

        if (bitmap == null) {

            // drawing cache에서 bitmap을 얻지 못하면 super.drawChild() 호출

            return super.drawChild(canvas, child, drawingTime);

        }

        

        super.drawChild(canvas, child, drawingTime);

        

        return false;

    }

 

 

 

 

터치 이벤트나 어댑터의 변화로 onLayout() 이 호출되면 앞서 얘기한 4 단계를 수행합니다.

 

[1] onLayout() 내부에서 getView()를 통해 child view를 가져오고 (여기서는 Adapter의 모든 아이템을 한번에 가져오도록 구현)

[2][3] 각각의 view를 addViewAndMeasure()를 통해 리스트에 붙이고 Measuring 수행,

[4] layoutItems() 를 통해 child 로 붙은 view 들을 순서대로 위치시킵니다.

 

마지막 drawChild() 는 당장은 필요하지 않지만 추후 작업이 필요해서 상속받은 method 입니다.

[1]~[4] 의 작업이 끝나고 리스트가 각각의 child view를 그릴 때 호출되는 callback 인데

child view 의 bitmap을 얻어와서 다양한 효과를 줄 수 있습니다.

 

이상의 코드만 실행시켜도 리스트에 아이템들이 표시되는 것을 볼 수 있습니다.

다만 터치에 대한 반응이 전혀 없는 상태입니다.

다음은 터치 이벤트 리스너를 구현해서 리스트 답게 만들 차례입니다.

 

 

 

Post Author: TORTUGA

TORTUGA
궁금하신 점은 새로 개편한 홈페이지의 QnA 게시판을 이용해주세요!!!!!!! http://www.hardcopyworld.com/gnuboard5/bbs/board.php?bo_table=qna

댓글 남기기

이메일은 공개되지 않습니다.