ListView列表拖拽排序

ListView列表拖拽排序可以参考Android源代码下的Music播放列表,他是可以拖拽的,源码在[packages/apps/Music下的TouchInterceptor.java下]。

首先是搭建框架,此处的ListView列表类似于QQ消息列表,当然数据只是模拟,为了简单起见,没有把ListView的条目的所有的属性全部写上。首先是消息的实体类Msg.java:

package me.chenfuduo.mymsgdrag;

public class Msg {

    private int ivId;

    private String text;

    public Msg() {

    }

    public Msg(int ivId, String text) {
        this.ivId = ivId;
        this.text = text;
    }

    public int getIvId() {
        return ivId;
    }

    public String getText() {
        return text;
    }

}

然后是数据列表的每个Item的布局item.xml:

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

    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="10dp"
        android:layout_centerInParent="true"
        android:id="@+id/imageView"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textView"
        android:layout_toRightOf="@id/imageView"
        android:layout_centerInParent="true"
        android:layout_marginLeft="10dp"
        />

</RelativeLayout>

现在可以新建一个MsgAdapter适配器类,让其继承自ArrayAdapter,并实现其构造方法和重写getView()方法。

package me.chenfuduo.mymsgdrag;

import java.util.List;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class MsgAdapter extends ArrayAdapter<Msg> {

    public MsgAdapter(Context context, List<Msg> msgList) {
        super(context, 0, msgList);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = View.inflate(getContext(), R.layout.item, null);
            viewHolder = new ViewHolder();
            viewHolder.imageView = (ImageView) view
                    .findViewById(R.id.imageView);
            viewHolder.textView = (TextView) view.findViewById(R.id.textView);
            view.setTag(viewHolder);
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }

        viewHolder.imageView.setImageResource(getItem(position).getIvId());
        viewHolder.textView.setText(getItem(position).getText());
        return view;
    }

    static class ViewHolder {
        ImageView imageView;
        TextView textView;
    }

}

主界面MainActivity设置适配器:

package me.chenfuduo.mymsgdrag;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    private MyDragListView list;

    private List<Msg> msgList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = (MyDragListView) findViewById(R.id.list);
        msgList = new ArrayList<Msg>();
        initData();
        list.setAdapter(new MsgAdapter(this, msgList));
    }

    private void initData() {
        for (int i = 0; i < 30; i++) {
            msgList.add(new Msg(R.drawable.ic_launcher, "new item" + i));
        }

    }

}

这里提到的MyDragListView就是下面我们要着重介绍的自定义的ListView,先不管。

OK,现在运行,数据全部展示在ListView上了,下面开始新建一个类MyDragListView,并让其继承自ListView,提供三个构造方法。

public MyDragListView(Context context) {
        this(context, null);
    }

    public MyDragListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyDragListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

接下来,需要重写onInterceptTouchEvent()拦截事件的方法,为了能在子控件响应触摸事件的情况下此ListView也能监听到触摸事件,需要重写此方法,做一些初始化的工作,在这里捕获ACTION_DOWN事件,在ACTION_DOWN事件中,做一些拖动的准备工作。

  • 获取点击数据项,初始化一些变量(pointToPosition)
  • 判断是否是拖动还是仅仅是点击
  • 如果是拖动,建立拖动影像()

以上都是后面拖动的基础。

那首先定义我们需要的一些变量:

    // 原始条目位置
    private int dragSrcPosition;
    // 目标条目位置
    private int dragDestPosition;
    //在当前数据项中的位置
    private int dragPoint;
    //拖动的时候,开始向上滚动的边界
    private int upScrollBounce;
    //拖动的时候,开始向下滚动的边界
    private int downScrollBounce;
    //窗口控制类
    private WindowManager windowManager;
    //用于控制拖拽项显示的参数
    private WindowManager.LayoutParams windowParams;
    //当前视图和屏幕的距离(这里只使用了y轴上的)
    private int dragOffset;

    // 判断滑动的一个距离,scroll的时候会用到
    private int scaledTouchSlop;
    // 被拖拽项的影像,其实就是一个ImageView,在我们这里是"用户头像"
    private ImageView dragImageView;

我们在构造器中获取滑动的距离:

public MyDragListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

注意获取这个系统所能识别出的被认为是滑动的最小距离的方式。

getScaledTouchSlop()是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件。

接下来,在onInterceptTouchEvent()捕获的ACTION_DOWN事件中,做处理。

还是按照上面的来,第一部需要得到选中的数据项的位置,这里使用pointToPosition(x,y)即可。如果想要测试这个api,也很简单,下面是实例代码:

 mListView.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int item=mListView.pointToPosition((int) event.getX(), (int) event.getY());
                System.out.println("---> 现在点击了ListView中第"+(item+1)+"个Item");
                return true;
            }
        });  

ok,在我们这里是这样:

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

            // 触点所在的条目的位置

            int x = (int) ev.getX();
            int y = (int) ev.getY();
            dragSrcPosition = dragDestPosition = pointToPosition(x, y);

            //如果是无效位置(超出边界,分割线等位置),返回
                 if(dragDestPosition==AdapterView.INVALID_POSITION){
                        return super.onInterceptTouchEvent(ev);
                 }

}

现在我们要获取ListView的单个Item,因为获取了这个单个的Item,才能获取Item里面的”头像”(姑且这么叫),ok,代码如下:

            //这里如果不减去,会报空指针异常
            ViewGroup itemView = (ViewGroup) getChildAt(dragSrcPosition
                    - getFirstVisiblePosition());

在这里我当时遇到一个NPE的问题,就是当ListView滚动到下面的时候,我选择下面的Item,报错了,归根到底,还是没有理解好getChildAt(i)这个方法。这里,我参考了下面的资料去理解的。

ListView中getChildAt(index)的使用注意事项

通过getChildAt方法取得AdapterView中第n个Item

stackover:ListView getChildAt returning null for visible children

说到底,getChildAt(i)是获取可见视图的。

接下来,就可以获取”用户头像”了:

            // 图标
            View dragger = itemView.findViewById(R.id.imageView);

下面需要判断手指的触点是不是在logo(”用户头像”)范围内:

        if (dragger != null && x < dragger.getRight() + 10) {

                upScrollBounce = Math.min(y - scaledTouchSlop, getHeight() / 3);
                downScrollBounce = Math.max(y + scaledTouchSlop,
                        getHeight() * 2 / 3);
}

这个很好理解。

接着便可以获取选中条目的图片了。

                itemView.setDrawingCacheEnabled(true);
                Bitmap bitmap = itemView.getDrawingCache();
                startDrag(bitmap, y);
                itemView.setDrawingCacheEnabled(false);

这里,又学到一招,将View转化为Bitmap,相关的api:

  • setDrawingCacheEnabled(boolean)注意最后需要设置其为false
  • getDrawingCache()

那么现在就可以拖动了。

startDrag(bitmap, y);

最后,我们返回false,让事件可以传递到子控件。

整体的代码入下:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {

            // 触点所在的条目的位置

            int x = (int) ev.getX();
            int y = (int) ev.getY();
            dragSrcPosition = dragDestPosition = pointToPosition(x, y);

            // 如果是无效位置(超出边界,分割线等位置),返回
            if (dragDestPosition == AdapterView.INVALID_POSITION) {
                return super.onInterceptTouchEvent(ev);
            }

            //这里如果不减去
            ViewGroup itemView = (ViewGroup) getChildAt(dragSrcPosition
                    - getFirstVisiblePosition());

            // 手指在条目中的相对y坐标

            dragPoint = y - itemView.getTop();

            /*Log.e("Test", "dragPoint:" + dragPoint + "\n" + "y:"+ y
                    + "\n" + "itemView.getTop():" + itemView.getTop());*/

            dragOffset = (int) (ev.getRawY() - y);

            /*Log.e("Test", "dragOffset:" + dragPoint + "\n" + "y:"+ y
                    + "\n" + "ev.getRawY():" + ev.getRawY());*/

            // 图标
            View dragger = itemView.findViewById(R.id.imageView);
            // 判断触点是否在logo的区域

            if (dragger != null && x < dragger.getRight() + 10) {

                upScrollBounce = Math.min(y - scaledTouchSlop, getHeight() / 3);
                downScrollBounce = Math.max(y + scaledTouchSlop,
                        getHeight() * 2 / 3);

                // 获取选中条目的图片

                itemView.setDrawingCacheEnabled(true);
                Bitmap bitmap = itemView.getDrawingCache();
                itemView.setDrawingCacheEnabled(false);
                startDrag(bitmap, y);

            }

            // 可以传递到子控件
            return false;

        }

        return super.onInterceptTouchEvent(ev);
    }

在上面有获取坐标的getRawY()等等,如果不清楚,可以看下这个文章。

android MotionEvent中getX()和getRawX()的区别

接下来是拖拽的方法startDrag(bitmap, y);:

private void startDrag(Bitmap bitmap, int y) {

        // 释放影像,在准备影像的时候,防止影像没释放,每次都执行一下
        stopDrag();

        windowManager = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);

        // 窗体参数配置

        windowParams = new WindowManager.LayoutParams();

        windowParams.gravity = Gravity.TOP;

        windowParams.x = 0;

        // 图片在屏幕上的绝对坐标

        windowParams.y = y - dragPoint + dragOffset;

        /*Log.e("Test", "windowParams.y:" + windowParams.y + "\n" + "y:"+ y
                + "\n" + "dragPoint:" + dragPoint +
                "\n" + "dragOffset" + dragOffset);*/

        // 添加显示窗体

        windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        // 下面这些参数能够帮助准确定位到选中项点击位置,照抄即可
        windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        windowParams.format = PixelFormat.TRANSLUCENT;
        windowParams.windowAnimations = 0;

        // 把影像ImagView添加到当前视图中
        ImageView imageView = new ImageView(getContext());
        imageView.setImageBitmap(bitmap);
        windowManager = (WindowManager) getContext().getSystemService("window");
        windowManager.addView(imageView, windowParams);
        // 把影像ImageView引用到变量drawImageView,用于后续操作(拖动,释放等等)

        dragImageView = imageView;
    }

如果做过自定义Toast,对上面的代码不会陌生。

不做解释,接着重写boolean onTouchEvent(MotionEvent ev):

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 如果dragImageView为空,说明拦截事件中已经判定仅仅是点击,不是拖动,返回
        // 如果点击的是无效位置,返回,需要重新判断
        if (dragImageView != null && dragDestPosition != INVALID_POSITION) {
            int action = ev.getAction();
            switch (action) {
            case MotionEvent.ACTION_UP:
                int upY = (int) ev.getY();
                // 释放拖动影像
                stopDrag();
                // 放下后,判断位置,实现相应的位置删除和插入
                onDrop(upY);
                break;
            case MotionEvent.ACTION_MOVE:
                int moveY = (int) ev.getY();
                // 拖动影像
                onDrag(moveY);
                break;
            default:
                break;
            }
            return true;
        }
        // 这个返回值能够实现selected的选中效果,如果返回true则无选中效果
        return super.onTouchEvent(ev);
    }

首先得判断是点击还是拖动,直接通过dragImageView即可判断,如果dragImageView为空,说明拦截事件中已经判定仅仅是点击,不是拖动,返回。接着分析拖动的方法onDrag(moveY);:

拖动的时候,当前拖动的条目的透明度让其有所变化,然后是位置在不断更新,其次需要判断位置是否合法,最后是滚动。

private void onDrag(int y) {
        if (dragImageView != null) {
            windowParams.alpha = 0.8f;
            windowParams.y = y - dragPoint + dragOffset;
            windowManager.updateViewLayout(dragImageView, windowParams);
        }
        // 为了避免滑动到分割线的时候,返回-1的问题
        int tempPosition = pointToPosition(0, y);
        if (tempPosition != INVALID_POSITION) {
            dragDestPosition = tempPosition;
        }

        // 滚动
        int scrollHeight = 0;
        if (y < upScrollBounce) {
            scrollHeight = 8;// 定义向上滚动8个像素,如果可以向上滚动的话
        } else if (y > downScrollBounce) {
            scrollHeight = -8;// 定义向下滚动8个像素,,如果可以向上滚动的话
        }

        if (scrollHeight != 0) {
            // 真正滚动的方法setSelectionFromTop()
            setSelectionFromTop(dragDestPosition,
                    getChildAt(dragDestPosition - getFirstVisiblePosition())
                            .getTop() + scrollHeight);
        }
    }

这里的ViewManager.updateViewLayout(View arg0, LayoutParams arg1)会使得view所引用的实例使用params重新绘制自己。

接下来介绍下ListView的setSelectionFromTop(...)setSelection(...)方法。

看一下setSelectionFromTop()的具体实现,代码如下:

/**
 * Sets the selected item and positions the selection y pixels from the top edge
 * of the ListView. (If in touch mode, the item will not be selected but it will
 * still be positioned appropriately.)
 *
 * @param position Index (starting at 0) of the data item to be selected.
 * @param y The distance from the top edge of the ListView (plus padding) that the
 *        item will be positioned.
 */
public void setSelectionFromTop(int position, int y) {
    if (mAdapter == null) {
        return;
    }  

    if (!isInTouchMode()) {
        position = lookForSelectablePosition(position, true);
        if (position >= 0) {
            setNextSelectedPositionInt(position);
        }
    } else {
        mResurrectToPosition = position;
    }  

    if (position >= 0) {
        mLayoutMode = LAYOUT_SPECIFIC;
        mSpecificTop = mListPadding.top + y;  

        if (mNeedSync) {
            mSyncPosition = position;
            mSyncRowId = mAdapter.getItemId(position);
        }  

        requestLayout();
    }
}  

从上面的代码可以得知,setSelectionFromTop()的作用是设置ListView选中的位置,同时在Y轴设置一个偏移量(padding值)。

ListView还有一个方法叫setSelection(),传入一个index整型数值,就可以让ListView定位到指定Item的位置。

这两个方法有什么区别呢?看一下setSelection()的具体实现,代码如下:

/**
 * Sets the currently selected item. If in touch mode, the item will not be selected
 * but it will still be positioned appropriately. If the specified selection position
 * is less than 0, then the item at position 0 will be selected.
 *
 * @param position Index (starting at 0) of the data item to be selected.
 */
@Override
public void setSelection(int position) {
    setSelectionFromTop(position, 0);
}  

原来,setSelection()内部就是调用了setSelectionFromTop(),只不过是Y轴的偏移量是0而已。

Ok,当手指抬起来的时候,需要停止拖动:

    private void stopDrag() {
        if (dragImageView != null) {
            windowManager.removeView(dragImageView);
            dragImageView = null;
        }

    }

最后得将Item放到正确的位置:

private void onDrop(int y) {
        // 获取放下位置在数据集合中position
        // 定义临时位置变量为了避免滑动到分割线的时候,返回-1的问题,如果为-1,则不修改dragPosition的值,急需执行,达到跳过无效位置的效果
        int tempPosition = pointToPosition(0, y);
        if (tempPosition != INVALID_POSITION) {
            dragDestPosition = tempPosition;
        }

        // 超出边界处理
        if (y < getChildAt(1).getTop()) {
            // 超出上边界,设为最小值位置0
            dragDestPosition = 0;
        } else if (y > getChildAt(getChildCount() - 1).getTop()) {
            // 超出下边界,设为最大值位置,注意哦,如果大于可视界面中最大的View的底部则是越下界,所以判断中用getChildCount()方法
            // 但是最后一项在数据集合中的position是getAdapter().getCount()-1,这点要区分清除
            dragDestPosition = getAdapter().getCount() - 1;
        }

        // 数据更新
        if (dragDestPosition >= 0 && dragDestPosition < getAdapter().getCount()) {
            MsgAdapter adapter = (MsgAdapter) getAdapter();
            Msg dragItem = adapter.getItem(dragSrcPosition);

            // 删除原位置数据项
            adapter.remove(dragItem);
            // 在新位置插入拖动项
            adapter.insert(dragItem, dragDestPosition);
        }
    }

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2025-01-13 02:45:32

ListView列表拖拽排序的相关文章

列表拖拽排序功能

前几天在做项目的时候,遇到一个表格里边的数据通过上下拖拽来改变其排序方式,后来通过一阵查找,发现jquery-ui提供了sortable这个方法,甚是欢喜,在此便把我写的小demo奉上: <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width

自定义控件——可拖拽排序的ListView

前言 最经研究了一下拖拽排序的ListView,跟酷狗里的播放列表排序一样,但因为要添加自己特有的功能,所以研究了好长时间.一开始接触的是GitHub的开源项目--DragSortListView,实现的效果和流畅度都很棒.想根据他的代码自己写一个,但代码太多了,实现的好复杂,看别人的代码你懂的了,就去尝试寻找其他办法.最后还是找到了更简单的实现方法,虽然跟开源项目比要差一点,但对我来说可以了,最重要的是完全可以自定义. 实现的效果如下: 主要问题 如何根据触摸的位置确定是哪个条目? ListV

【Android】可拖拽排序的ListView

[Android]可拖拽排序的ListView 实现Item的拖拽排序效果 下载地址:http://www.devstore.cn/code/info/746.html 运行截图:

RecyclerView实现条目Item拖拽排序与滑动删除

RecyclerView实现条目Item拖拽排序与滑动删除 版权声明:转载请注明本文转自严振杰的博客: http://blog.csdn.net/yanzhenjie1003 效果演示 直播视频讲解:[http://pan.baidu.com/s/1miEOtwG1 推荐大家结合我直播的视频看效果更好. 本博客源码传送门. 需求和技术分析 RecyclerView Item拖拽排序::长按RecyclerView的Item或者触摸Item的某个按钮. RecyclerView Item滑动删除:

Android基础控件——RecyclerView实现拖拽排序侧滑删除效果

RecyclerView实现拖拽排序侧滑删除效果 事先说明: RecyclerView是ListView的升级版,使用起来比ListView更规范,而且功能和动画可以自己添加,极容易扩展,同样也继承了ListView复用convertView和ViewHolder的优点.   思路分析: 1.导包.在布局中使用RecyclerView 2.需要一个JavaBean用来存储展示信息 3.需要一个填充RecyclerView的布局文件 4.在代码中找到RecyclerView,并为其绑定Adapte

一款优雅的小程序拖拽排序组件实现

前言 最近po主写小程序过程中遇到一个拖拽排序需求. 上网一顿搜索未果, 遂自行实现. 这次就不上效果图了, 直接扫码感受吧. 灵感 首先由于并没有啥现成的小程序案例给我参考. 所以有点无从下手, 那就找个h5的拖拽实现参考参考. 于是在jquery插件网看了几个拖拽排序实现后基本确定了思路. 大概就是用 transform 做变换. 是的, 灵感这种东西就是借鉴过来的-- 确定需求 要能拖拽, 毕竟是拖拽排序嘛, 拖拽肯定是第一位. 要能排序, 先有拖拽后有天 -- 跑偏了, 拖拽完了肯定是要

使用knockout-sortable实现对自定义菜单的拖拽排序

在开始之前,照例,我们先看效果和功能实现. 关于自定义菜单的实现,这里就不多说了,需要了解的请访问:http://www.cnblogs.com/codelove/p/4838766.html 这里需要说明的是排序的实现. 我们先来看看关键的页面代码: <div class="row"> <div class="col-lg-12 full-width" id="leftMenus"> <div class=&quo

jQuery拖拽插件制作拖拽排序特效

基于jQuery拖拽插件制作拖拽排序特效是一款非常实用的鼠标拖拽布局插件.效果图如下: 在线预览   源码下载 实现的代码. html代码: <h1>水平拖拽</h1> <div class="demo"> <div class="item item1"><span>1</span></div> <div class="item item2"><

jquery -- 拖拽排序分析

今天应一个朋友的委托,研究一下拖拽排序,我记得我上次写拖拽排序,因为方法太死板,效果我一直不是很满意,一直想再从写一个,一直没机会(懒),这次因为公司部门变动所以有了一些时间(无聊)来写,本来准备使用Vue写,奈何功夫不到家在自定义指令的时候,问题卡住了,研究了一段时间之后,还是决定放弃,研究一下Vue再来写过,所以本次还是用了Jquery来写. 直接上代码 这是CSS部分 1 *{/*Css*/ 2 margin: 0px; 3 padding: 0px; 4 list-style: none