[출처 : http://replygun.tistory.com/136]

ListView의 내용을 드래그 앤 드롭으로 순서를 변경하고 싶을 때 사용한다.

 

안드로이드 폰의 "Music" 앱의 playlist를 보면 아이템 드래그 앤 드롭이 가능하다는 것을 확인 할 수 있다. 다만 안드로이드 기본 API로 제공되지 않기 때문에 직접 구현해야만 한다. 드래그 앤 드롭이 적용된 ListView예제는 안드로이드 기본 앱인 Music에서도 확인 가능하며 필자가 만든 "마이투두" 앱에서도 확인이 가능하다.

 

다음은 안드로이드의 Music 앱의 소스를 참고하여 만든 DndListView이다.

아래의 코드를 사용하기 위해서는 안드로이드 버전 1.5 이상부터 가능하다.

 

  1. /*
    
     * Copyright (C) 2008 The Android Open Source Project
    
     *
    
     * Licensed under the Apache License, Version 2.0 (the "License");
    
     * you may not use this file except in compliance with the License.
    
     * You may obtain a copy of the License at
    
     *
    
     *      http://www.apache.org/licenses/LICENSE-2.0
    
     *
    
     * Unless required by applicable law or agreed to in writing, software
    
     * distributed under the License is distributed on an "AS IS" BASIS,
    
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    
     * See the License for the specific language governing permissions and
    
     * limitations under the License.
    
     */
    
    
    
    package org.pyframe.tools.view;
    
    
    
    import android.content.Context;
    
    import android.graphics.Bitmap;
    
    import android.graphics.PixelFormat;
    
    import android.graphics.Rect;
    
    import android.util.AttributeSet;
    
    import android.view.GestureDetector;
    
    import android.view.Gravity;
    
    import android.view.MotionEvent;
    
    import android.view.View;
    
    import android.view.ViewConfiguration;
    
    import android.view.ViewGroup;
    
    import android.view.WindowManager;
    
    import android.widget.AdapterView;
    
    import android.widget.ImageView;
    
    import android.widget.ListView;
    
    
    
    import com.mytodo.andriod.R;
    
    
    
    public class DndListView extends ListView {
    
        
    
    	private Context mContext;
    
        private ImageView mDragView;
    
        private WindowManager mWindowManager;
    
        private WindowManager.LayoutParams mWindowParams;
    
        private int mDragPos;      // which item is being dragged
    
        private int mFirstDragPos; // where was the dragged item originally
    
        private int mDragPoint;    // at what offset inside the item did the user grab it
    
        private int mCoordOffset;  // the difference between screen coordinates and coordinates in this view
    
        private DragListener mDragListener;
    
        private DropListener mDropListener;
    
    //    private RemoveListener mRemoveListener;
    
        private int mUpperBound;
    
        private int mLowerBound;
    
        private int mHeight;
    
        private GestureDetector mGestureDetector;
    
    //    private static final int FLING = 0;
    
    //    private static final int SLIDE = 1;
    
    //    private int mRemoveMode = -1;
    
        private Rect mTempRect = new Rect();
    
        private Bitmap mDragBitmap;
    
        private final int mTouchSlop;
    
        private int mItemHeightNormal;
    
        private int mItemHeightExpanded;
    
    
    
        public DndListView(Context context, AttributeSet attrs) {
    
            super(context, attrs);
    
    //        SharedPreferences pref = context.getSharedPreferences("Music", 3);
    
    //        mRemoveMode = pref.getInt("deletemode", -1);
    
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    
            mContext = context;
    
    //        Resources res = getResources();
    
    //        mItemHeightNormal = res.getDimensionPixelSize(R.dimen.normal_height);
    
    //        mItemHeightExpanded = res.getDimensionPixelSize(R.dimen.expanded_height);
    
        }
    
        
    
        @Override
    
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            if (mDragListener != null || mDropListener != null) {
    
                switch (ev.getAction()) {
    
                    case MotionEvent.ACTION_DOWN:
    
                        int x = (int) ev.getX();
    
                        int y = (int) ev.getY();
    
                        int itemnum = pointToPosition(x, y);
    
                        if (itemnum == AdapterView.INVALID_POSITION) {
    
                            break;
    
                        }
    
                        ViewGroup item = (ViewGroup) getChildAt(itemnum - getFirstVisiblePosition());
    
                        mDragPoint = y - item.getTop();
    
                        mCoordOffset = ((int)ev.getRawY()) - y;
    
                        View dragger = item.findViewById(R.id.dragicon);
    
                        
    
    //                    item.setBackgroundColor(Color.RED);
    
                        
    
                        Rect r = mTempRect;
    
                        dragger.getDrawingRect(r);
    
                        // The dragger icon itself is quite small, so pretend the touch area is bigger
    
                        if (x < r.right * 2) {
    
                            item.setDrawingCacheEnabled(true);
    
                            // Create a copy of the drawing cache so that it does not get recycled
    
                            // by the framework when the list tries to clean up memory
    
                            Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache());
    
                            startDragging(bitmap, y);
    
                            mDragPos = itemnum;
    
                            mFirstDragPos = mDragPos;
    
                            mHeight = getHeight();
    
                            int touchSlop = mTouchSlop;
    
                            mUpperBound = Math.min(y - touchSlop, mHeight / 3);
    
                            mLowerBound = Math.max(y + touchSlop, mHeight * 2 /3);
    
                            return false;
    
                        }
    
                        mDragView = null;
    
                        break;
    
                }
    
            }
    
            return super.onInterceptTouchEvent(ev);
    
        }
    
        
    
        /*
    
         * pointToPosition() doesn't consider invisible views, but we
    
         * need to, so implement a slightly different version.
    
         */
    
        private int myPointToPosition(int x, int y) {
    
            Rect frame = mTempRect;
    
            final int count = getChildCount();
    
            for (int i = count - 1; i >= 0; i--) {
    
                final View child = getChildAt(i);
    
                child.getHitRect(frame);
    
                if (frame.contains(x, y)) {
    
                    return getFirstVisiblePosition() + i;
    
                }
    
            }
    
            return INVALID_POSITION;
    
        }
    
        
    
        private int getItemForPosition(int y) {
    
            int adjustedy = y - mDragPoint - 32;
    
            int pos = myPointToPosition(0, adjustedy);
    
            if (pos >= 0) {
    
                if (pos <= mFirstDragPos) {
    
                    pos += 1;
    
                }
    
            } else if (adjustedy < 0) {
    
                pos = 0;
    
            }
    
            return pos;
    
        }
    
        
    
        private void adjustScrollBounds(int y) {
    
            if (y >= mHeight / 3) {
    
                mUpperBound = mHeight / 3;
    
            }
    
            if (y <= mHeight * 2 / 3) {
    
                mLowerBound = mHeight * 2 / 3;
    
            }
    
        }
    
    
    
        /*
    
         * Restore size and visibility for all listitems
    
         */
    
        private void unExpandViews(boolean deletion) {
    
            for (int i = 0;; i++) {
    
                View v = getChildAt(i);
    
                if (v == null) {
    
                    if (deletion) {
    
                        // HACK force update of mItemCount
    
                        int position = getFirstVisiblePosition();
    
                        int y = getChildAt(0).getTop();
    
                        setAdapter(getAdapter());
    
                        setSelectionFromTop(position, y);
    
                        // end hack
    
                    }
    
                    layoutChildren(); // force children to be recreated where needed
    
                    v = getChildAt(i);
    
                    if (v == null) {
    
                        break;
    
                    }
    
                }
    
                ViewGroup.LayoutParams params = v.getLayoutParams();
    
                params.height = mItemHeightNormal;
    
                v.setLayoutParams(params);
    
                v.setVisibility(View.VISIBLE);
    
            }
    
        }
    
        
    
        /* Adjust visibility and size to make it appear as though
    
         * an item is being dragged around and other items are making
    
         * room for it:
    
         * If dropping the item would result in it still being in the
    
         * same place, then make the dragged listitem's size normal,
    
         * but make the item invisible.
    
         * Otherwise, if the dragged listitem is still on screen, make
    
         * it as small as possible and expand the item below the insert
    
         * point.
    
         * If the dragged item is not on screen, only expand the item
    
         * below the current insertpoint.
    
         */
    
        private void doExpansion() {
    
            int childnum = mDragPos - getFirstVisiblePosition();
    
            if (mDragPos > mFirstDragPos) {
    
                childnum++;
    
            }
    
    
    
            View first = getChildAt(mFirstDragPos - getFirstVisiblePosition());
    
    
    
            for (int i = 0;; i++) {
    
                View vv = getChildAt(i);
    
                if (vv == null) {
    
                    break;
    
                }
    
                int height = mItemHeightNormal;
    
                int visibility = View.VISIBLE;
    
                if (vv.equals(first)) {
    
                    // processing the item that is being dragged
    
                    if (mDragPos == mFirstDragPos) {
    
                        // hovering over the original location
    
                        visibility = View.INVISIBLE;
    
                    } else {
    
                        // not hovering over it
    
                        height = 1;
    
                    }
    
                } else if (i == childnum) {
    
                    if (mDragPos < getCount() - 1) {
    
                        height = mItemHeightExpanded;
    
                    }
    
                }
    
                ViewGroup.LayoutParams params = vv.getLayoutParams();
    
                params.height = height;
    
                vv.setLayoutParams(params);
    
                vv.setVisibility(visibility);
    
            }
    
        }
    
        
    
        @Override
    
        public boolean onTouchEvent(MotionEvent ev) {
    
            if (mGestureDetector != null) {
    
                mGestureDetector.onTouchEvent(ev);
    
            }
    
            if ((mDragListener != null || mDropListener != null) && mDragView != null) {
    
                int action = ev.getAction(); 
    
                switch (action) {
    
                    case MotionEvent.ACTION_UP:
    
                    case MotionEvent.ACTION_CANCEL:
    
                        Rect r = mTempRect;
    
                        mDragView.getDrawingRect(r);
    
                        stopDragging();
    
    //                    if (mRemoveMode == SLIDE && ev.getX() > r.right * 3 / 4) {
    
    //                        if (mRemoveListener != null) {
    
    //                            mRemoveListener.remove(mFirstDragPos);
    
    //                        }
    
    //                        unExpandViews(true);
    
    //                    } else {
    
                        if (mDropListener != null && mDragPos >= 0 && mDragPos < getCount()) {
    
                            mDropListener.drop(mFirstDragPos, mDragPos);
    
                        }
    
                        unExpandViews(false);
    
    //                    }
    
                        break;
    
                        
    
                    case MotionEvent.ACTION_DOWN:
    
                    case MotionEvent.ACTION_MOVE:
    
                        int x = (int) ev.getX();
    
                        int y = (int) ev.getY();
    
                        dragView(x, y);
    
                        int itemnum = getItemForPosition(y);
    
                        if (itemnum >= 0) {
    
                            if (action == MotionEvent.ACTION_DOWN || itemnum != mDragPos) {
    
                                if (mDragListener != null) {
    
                                    mDragListener.drag(mDragPos, itemnum);
    
                                }
    
                                mDragPos = itemnum;
    
                                doExpansion();
    
                            }
    
                            int speed = 0;
    
                            adjustScrollBounds(y);
    
                            if (y > mLowerBound) {
    
                                // scroll the list up a bit
    
                                speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4;
    
                            } else if (y < mUpperBound) {
    
                                // scroll the list down a bit
    
                                speed = y < mUpperBound / 2 ? -16 : -4;
    
                            }
    
                            if (speed != 0) {
    
                                int ref = pointToPosition(0, mHeight / 2);
    
                                if (ref == AdapterView.INVALID_POSITION) {
    
                                    //we hit a divider or an invisible view, check somewhere else
    
                                    ref = pointToPosition(0, mHeight / 2 + getDividerHeight() + 64);
    
                                }
    
                                View v = getChildAt(ref - getFirstVisiblePosition());
    
                                if (v!= null) {
    
                                    int pos = v.getTop();
    
                                    setSelectionFromTop(ref, pos - speed);
    
                                }
    
                            }
    
                        }
    
                        break;
    
                }
    
                return true;
    
            }
    
            return super.onTouchEvent(ev);
    
        }
    
        
    
        private void startDragging(Bitmap bm, int y) {
    
            stopDragging();
    
    
    
            mWindowParams = new WindowManager.LayoutParams();
    
            mWindowParams.gravity = Gravity.TOP;
    
            mWindowParams.x = 0;
    
            mWindowParams.y = y - mDragPoint + mCoordOffset;
    
    //
    
            mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
    
            mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
    
            mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
    
                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
    
                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
    
            mWindowParams.format = PixelFormat.TRANSLUCENT;
    
            mWindowParams.windowAnimations = 0;
    
            
    
            ImageView v = new ImageView(mContext);
    
            int backGroundColor = mContext.getResources().getColor(R.color.dragndrop_background);
    
    //        int backGroundColor = Color.parseColor("#e0103010");
    
            v.setBackgroundColor(backGroundColor);
    
            
    
            v.setImageBitmap(bm);
    
            mDragBitmap = bm;
    
    
    
            mWindowManager = (WindowManager)mContext.getSystemService("window");
    
            mWindowManager.addView(v, mWindowParams);
    
            mDragView = v;
    
        }
    
        
    
        private void dragView(int x, int y) {
    
    //        if (mRemoveMode == SLIDE) {
    
    //            float alpha = 1.0f;
    
    //            int width = mDragView.getWidth();
    
    //            if (x > width / 2) {
    
    //                alpha = ((float)(width - x)) / (width / 2);
    
    //            }
    
    //            mWindowParams.alpha = alpha;
    
    //        }
    
            mWindowParams.y = y - mDragPoint + mCoordOffset;
    
            mWindowManager.updateViewLayout(mDragView, mWindowParams);
    
        }
    
        
    
        private void stopDragging() {
    
            if (mDragView != null) {
    
                WindowManager wm = (WindowManager)mContext.getSystemService("window");
    
                wm.removeView(mDragView);
    
                mDragView.setImageDrawable(null);
    
                mDragView = null;
    
            }
    
            if (mDragBitmap != null) {
    
                mDragBitmap.recycle();
    
                mDragBitmap = null;
    
            }
    
        }
    
        
    
        public void setDragListener(DragListener l) {
    
            mDragListener = l;
    
        }
    
        
    
        public void setDropListener(DropListener l) {
    
            mDropListener = l;
    
        }
    
        
    
    //    public void setRemoveListener(RemoveListener l) {
    
    //        mRemoveListener = l;
    
    //    }
    
    
    
        public interface DragListener {
    
            void drag(int from, int to);
    
        }
    
        public interface DropListener {
    
            void drop(int from, int to);
    
        }
    
        public interface RemoveListener {
    
            void remove(int which);
    
        }
    
    }

 

위 코드중 정상적인 컴파일을 위해서 살펴보아야 할 부분이 두군데 있다.

 

1. 위 코드에 다음과 같은 부분이 있다.

  1. View dragger = item.findViewById(R.id.dragicon);

 

R.id.dragicon 이 바로 드래그를 할 대상이 되는 View가 된다.

 

2. 또 다음과 같은 코드가 있다.

  1. int backGroundColor = mContext.getResources().getColor(R.color.dragndrop_background);

 

이 부분은 드래그 앤 드롭시 백그라운드 색상을 지정해 주는 부분이다.

 

strings.xml파일에 다음과 같은 항목을 추가해 주어야 한다.

  1. <color name="dragndrop_background">#e0103010</color>

 

 

이제 드래그 앤 드롭을 구현하기 위해서 ListActivity는 어떻게 구현해야 하는지 알아보자.

 

아래와 같이 DragListener와 DropListener를 구현하도록 ListActivity를 만든다.

  1. public class MainActivity extends ListActivity implements DragListener, DropListener {

 

onCreate메써드에서 드래그 앤 드롭을 사용한다는 정보를 입력한다.

  1. listView = (DndListView) findViewById(android.R.id.list);
  2. listView.setDragListener(this);
    listView.setDropListener(this);

 

그리고 다음과 같은 메써드를 구현한다.

  1. public void drag(int from, int to) {
  2.   // 드래그 이벤트가 발생시 구현해야 할 것들을 기술한다.
    }
  3.  
  4. public void drop(int fr, int to) {
  5.   // 드롭 이벤트 발생시 구현해야 할 것들을 기술한다.
  6. }

 

Activity의 레이아웃 파일은 다음과 같이 작성해야 한다.

  1.     <org.pyframe.tools.view.DndListView
            android:id="@android:id/list"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:drawSelectorOnTop="false"
            android:fastScrollEnabled="true"
            android:cacheColorHint="#00000000"
            android:layout_weight="1.0"
             />
 

cacheColorHint값을 주어야 드래그 앤 드롭시 선택된 아이템의 백그라운드 색상이 표시된다.

Posted by dlucky

안드로이드에서 자주 사용하는 기법인 ListView에 동적으로 아이템을 추가시 새로추가된 아이템으로 스크롤이 이동이 되지 않는 현상에 대해서 간단하게 알아보겠습니다.


일단 간단한 상황을 예를 들어서 풀어보도록 하겠습니다.

상황 : 현재의 Activity는 ListView와 EditText로 구성이 되어있고 EditText에 내용을 입력하고 Enter키를 입력시 ListView에 동적으로 추가된다.

위의 상황을 코드로 표현을 해보겠습니다.

view source
print?
01 public class TranscriptDemo extends ListActivity implements OnKeyListener {
02     private ArrayAdapter<STRING> adapter;
03     private ArrayList<STRING> list = new ArrayList<STRING>();
04     private EditText editText;
05        
06     public void onCreate(Bundle bundle){
07         super.onCreate(bundle);
08         setContentView(R.layout.transcript);
09           
10         editText = (EditText)findViewById(R.id.EditText01);
11         editText.setOnKeyListener(this);
12         adapter = new ArrayAdapter<STRING>(this, android.R.layout.simple_list_item_1, list);
13         setListAdapter(adapter);
14     }
15   
16     @Override
17     public boolean onKey(View v, int keyCode, KeyEvent event) {
18         if (event.getAction() == KeyEvent.ACTION_DOWN) {
19             switch (keyCode) {
20                 case KeyEvent.KEYCODE_ENTER:
21                     setText();
22                     return true;
23             }
24         }
25         return false;
26     }
27       
28     private void setText() { 
29         adapter.add(editText.getText().toString());
30         editText.setText(null);
31     }
32 }

코드를 정상적으로 작성을 하였으면 밑에 화면과 같이 정상적으로 동작이 됩니다.



하지만 여기서 문제가 있는게 위와같이 게속 아이템을 등록을 한다면 스크롤바가 생기게 될텐데 그 포커스가 밑에 그림과 같이 처음의 포커스에 맞춰져 잇다는점 입니다.



이 문제를 해결하기 위해서 저는 예전에 새로운 아이템을 추가할 시에 마지막 아이템을 선택 하기 조건을 줬습니다.

view source
print?
1 getListView().setSelection(list.size() - 1);

하지만 안드로이드 내에서 ListView에 제공하는 속성인 android:transcriptMode를 이용하여 더욱더 간단하게 처리를 할수 있습니다.
android:transcriptMode에 대해서 알아보자.

android:transcriptMode ?


Sets the transcript mode for the list. In transcript mode, the list scrolls to the bottom to make new items visible when they are added.

Must be one of the following constant values.


 Constant  Value  Description
 disabled  0  Disables transcript mode. This is the default value.
 normal  1  The list will automatically scroll to the bottom when a data set change notification is received and only if the last item is already visible on screen.
 alwaysScroll  2  The list will automatically scroll to the bottom, no matter what items are currently visible.

위에 자료는 안드로이드 API에 있는 내용이다. 간단하게 설명을하자면
transcriptMode 속성은 3가지의 값을 정의해줄수 있고 각각의 설명은 아래와 같다.  

1. disabled : 스크롤이 되지 않음
2. nomal : 현재의 포커스에서 추가된만큼의 포커스가 이동
3. alwaysScroll : 마지막에 추가된곳으로 포커스 이동

제가 만들프로그램은 마지막에 추가된곳으로 포커스가 이동을 해야하기 때문에 alwaysScroll을 줘야한다.

view source
print?
1 getListView().setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);

위와같이 Java단에서 설정을 해줘도 되고 xml에서 설정을 해줘도 된다.
설정이 올바르게 끝났으면 정상적으로 밑에 화면과 같이 작동을 할것이다.
Posted by dlucky

티스토리 툴바