前言
最经研究了一下拖拽排序的ListView,跟酷狗里的播放列表排序一样,但因为要添加自己特有的功能,所以研究了好长时间。一开始接触的是GitHub的开源项目——DragSortListView,实现的效果和流畅度都很棒。想根据他的代码自己写一个,但代码太多了,实现的好复杂,看别人的代码你懂的了,就去尝试寻找其他办法。最后还是找到了更简单的实现方法,虽然跟开源项目比要差一点,但对我来说可以了,最重要的是完全可以自定义。
实现的效果如下:
主要问题
如何根据触摸的位置确定是哪个条目?
ListView有一个方法,可以根据ListView控件内的坐标位置确定条目索引:int position = pointToPosition(int x, int y)
如何把此条目View的提取出来(我称之为快照)?
ListView可通过getChildAt(int index)
来获取子控件。但因为ListView内的条目View都要复用,所以此index不等于pointToPosition(x, y)
获取的位置,要减去第一个可见条目的位置。即:View itemView = getChildAt(position - getFirstVisiblePosition());
获取到View后,要把它变成一张照片(快照),View中有自带的方法,可以把View的当前显示的界面保存为Bitmap图片:
// 进行绘图缓存 itemView.setDrawingCacheEnabled(true); // 提取缓存中的图片 Bitmap bitmap = Bitmap.createBitmap(itemView.getDrawingCache());
如何悬浮在窗口上,并跟着手移动?
有了View的图片,可通过ImageView显示出来,但如何悬浮在窗口上?这里需要使用WindowManager来显示,并设置其参数WindowManager.LayoutParams。跟平常用代码在ViewGroup中添加View一样。ImageView mDragPhotoView= new ImageView(getContext()); mDragPhotoView.setImageBitmap(mDragPhotoBitmap); // 获取当前窗口管理器 WindowManager mWindowManager= (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams mWindowLayoutParams= new WindowManager.LayoutParams(); wm.addView(mDragPhotoView, mWindowLayoutParams);
至于跟着手移动,手触摸的坐标知道了,通过mWindowLayoutParams.y来设置y的坐标,并更新到界面上:
mWindowLayoutParams.y = newY; mWindowManager.updateViewLayout(mDragPhotoView, mWindowLayoutParams);
条目超过ListView,如何滚动?
ListView有很多方法可以实现滚动:
smoothScrollBy(int distance, int duration)
实现缓慢移动;
setSelectionFromTop(int position, int y)
来设定指定条目距离顶部位置。
为了美观,我这里选择了第一种方法以上4点就是最重要的,下面的主要是为了增加功能和提升用户体验
如何让移动到的位置,不显示条目,并与之前的位置进行交换?
不显示条目,也就是不显示View,但位置还得存在,这里可以使用View的setVisibility()来实现:
setVisibility(View.INVISIBLE)
交换位置就是适配器中的数据进行交换,我这里自定义了一个BaseAdapter子抽象类,并在内部实现了调换位置的方法。当然也可以使用List的先删除remove(int position)
,后添加add(int location, Object object)
的方法。public void swapData(int from, int to){ // mDragDatas是List实例对象 Collections.swap(mDragDatas, from, to); notifyDataSetChanged(); }
如何让快照只能在ListView中的可视条目范围内移动?
从此开始的问题,参考的资料中几乎没有,自己另外添加的功能,觉得能提升用户体验。快照必须跟条目一样,在ListView控件范围内,但快照的坐标是针对屏幕的。在onTouchEvent()里ev.getY()获取的是触摸点在控件内的Y轴坐标,ev.getRawY()获取的是在屏幕内的Y轴坐标点,so
mRawOffsetY = (int) (ev.getRawY() - mDownY);
就是ListView的左上角Y坐标,也就是快照的Y轴的最小值。
ListView的
getHeight()
就能获取底部高度,条目的总高度itemHeight是知道的(代码中,分割线的高度忘了加了,如果很小的话,不会有什么影响)。Y轴的最大值就是:mRawOffsetY + getHeight() - mDragItemHeight;
但有一点,如果条目很少,都没填充完ListView,怎么办?我们可以使用条目总高度*条目数量来确定所有条目的高度,与ListView的高度进行对比。这里,我用条目高度+分割线高度的办法来确定条目总高度。当然也可以使用一个条目的top到下一个条目的top距离来确定每个条目占的总高度。
/** * 判断ListView是否全部显示,即ListView无法上下滚动了 */ private boolean isShowAll() { if (getChildCount() == 0) { return true; } View firstChild = getChildAt(0); int itemAllHeight = firstChild.getBottom() - firstChild.getTop() + getDividerHeight(); return itemAllHeight * getAdapter().getCount() < getHeight(); } ... // 根据是否显示完全,设定快照在Y轴上可拖到的最大值 if (isShowAll()) { mMaxDragY = mRawOffsetY + getChildAt(getAdapter().getCount() - 1).getTop(); } else { mMaxDragY = mRawOffsetY + getHeight() - mDragItemHeight; }
如果条目很多,在拖拽时,有时需要快速滚动,有时需要慢速滚动,如何实现?
原理就是根据快照的位置距离上下边缘的位置,如果距离小于一个条目的高度,开始滚动,越靠近边缘滚动的越快。可通过设置smoothScrollBy(distance, duration)中的distance来达到调速的效果。设定一个在边缘时滚动的最大值,剩下的就是按比例来计算了。百分比计算参考下面的”主要代码”(不会用标签跳过去,知道的大侠麻烦告诉一声,谢谢)// 如果当前位置已经不到一个条目,则进行上或下的滚动。并根据距离边界的距离,设定滚动速度 int dragY = mMoveY - mItemOffsetY; if (dragY < mDragItemHeight) { int value = Math.max(0, dragY); // 防越界 float percent = estimatePercent(mDragItemHeight, 0, value); int distance = estimateInt(0, -mMaxDistance, percent); smoothScrollBy(distance, SMOOTH_SCROLL_DURATION); } else if (dragY > getHeight() - 2 * mDragItemHeight) { int value = Math.max(0, getHeight() - dragY - mDragItemHeight); // 防越界 float percent = estimatePercent(mDragItemHeight, 0, value); int distance = estimateInt(0, mMaxDistance, percent); smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
使用setVisibility(),把当前坐标的条目隐藏时,会出现闪烁,如何解决?
在触摸下去的时候,被触摸的条目设置了隐藏,快照显示出来前会有一段空白,导致闪烁的情况。个人觉得可能是快照还没完全显示出来。试了很多方法都不如意,最后决定还是用动画的来去闪烁。// 隐藏。为了防止隐藏时出现画面闪烁,使用动画去除闪烁效果 Animation aAnim = new AlphaAnimation(1f, DRAG_PHOTO_VIEW_ALPHA); aAnim.setDuration(50); aAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { // Move中有隐藏的功能,如果按下后快速移动,会出现该显示的又被隐藏了。所以要作判断 if (mIsDraging && mToPosition == mDragPosition) { itemView.setVisibility(View.INVISIBLE); } } @Override public void onAnimationRepeat(Animation animation) { } }); itemView.startAnimation(aAnim);
主要代码
开源项目中发现老外的代码注释很多,觉得还是很有必要的。上次自己写了一个自定义控件,涉及到一些数学公式,几个星期后要改进,结果自己都无法看懂了,最后使用了另外的方法去解决。
DragListView.java:
package com.zjun.draglistview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.support.annotation.FloatRange;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
/**
* 可拖拽排序ListView
* Created by Ralap on 2016/5/8.
*/
public class DragListView extends ListView {
private static final String LOG_TAG = "DragListView";
/**
* 拖拽快照的透明度(0.0f ~ 1.0f)。
*/
private static final float DRAG_PHOTO_VIEW_ALPHA = .8f;
/**
* 上下滚动时的时间
*/
private static final int SMOOTH_SCROLL_DURATION = 100;
/**
* 上下滚动时的最大距离,可进行设置
* @see #setMaxDistance(int)
* @see #getMaxDistance()
*/
private int mMaxDistance = 30;
/**
* 是否处于拖拽中
*/
private boolean mIsDraging;
/**
* 按下时的坐标位置
*/
private int mDownX;
private int mDownY;
/**
* 移动时的坐标
*/
private int mMoveX;
private int mMoveY;
/**
* 原生偏移量。也就是ListView的左上角相对于屏幕的位置
*/
private int mRawOffsetX;
private int mRawOffsetY;
/**
* 在条目中的位置
*/
private int mItemOffsetX;
private int mItemOffsetY;
/**
* 拖拽快照的垂直位置范围。根据条目数量和ListView的高度来确定
*/
private int mMinDragY;
private int mMaxDragY;
/**
* 拖拽条目的高度
*/
private int mDragItemHeight;
/**
* 被拖拽的条目位置
*/
private int mDragPosition;
/**
* 移动前的条目位置
*/
private int mFromPosition;
/**
* 移动后的条目位置
*/
private int mToPosition;
/**
* 窗口管理器,用于显示条目的快照
*/
private WindowManager mWindowManager;
/**
* 窗口管理的布局参数
*/
private WindowManager.LayoutParams mWindowLayoutParams;
/**
* 拖拽条目的快照图片
*/
private Bitmap mDragPhotoBitmap;
/**
* 正在拖拽的条目快照view
*/
private ImageView mDragPhotoView;
public DragListView(Context context) {
super(context);
}
public DragListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DragListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 获取第一个手指点的Action
int action = ev.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
// 获取当前触摸位置对应的条目索引
mDragPosition = pointToPosition(mDownX, mDownY);
// 如果触摸的坐标不在条目上,在分割线、或外部区域,则为无效值-1; 宽度3/4 以右的区域可拖拽
if (mDragPosition == AdapterView.INVALID_POSITION || mDownX < getWidth() * 3 / 4) {
return super.onTouchEvent(ev);
}
mIsDraging = true;
mToPosition = mFromPosition = mDragPosition;
mRawOffsetX = (int) (ev.getRawX() - mDownX);
mRawOffsetY = (int) (ev.getRawY() - mDownY);
// 开始拖拽的前期工作:展示item快照
startDrag();
break;
case MotionEvent.ACTION_MOVE:
mMoveX = (int) ev.getX();
mMoveY = (int) ev.getY();
if (mIsDraging) {
// 更新快照位置
updateDragView();
// 更新当前被替换的位置
updateItemView();
} else {
return super.onTouchEvent(ev);
}
break;
case MotionEvent.ACTION_UP:
if (mIsDraging) {
// 停止拖拽
stopDrag();
} else {
return super.onTouchEvent(ev);
}
break;
default:
break;
}
return true;
}
/**
* 开始拖拽
*/
private boolean startDrag() {
// 实际在ListView中的位置,因为涉及到条目的复用
final View itemView = getItemView(mDragPosition);
if (itemView == null) {
return false;
}
// 进行绘图缓存
itemView.setDrawingCacheEnabled(true);
// 提取缓存中的图片
mDragPhotoBitmap = Bitmap.createBitmap(itemView.getDrawingCache());
// 清除绘图缓存,否则复用的时候,会出现前一次的图片。或使用销毁destroyDrawingCache()
itemView.setDrawingCacheEnabled(false);
// 隐藏。为了防止隐藏时出现画面闪烁,使用动画去除闪烁效果
Animation aAnim = new AlphaAnimation(1f, DRAG_PHOTO_VIEW_ALPHA);
aAnim.setDuration(50);
aAnim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// Move中有隐藏的功能,如果按下后快速移动,会出现该显示的又被隐藏了。所以要作判断
if (mIsDraging && mToPosition == mDragPosition) {
itemView.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
itemView.startAnimation(aAnim);
mItemOffsetX = mDownX - itemView.getLeft();
mItemOffsetY = mDownY - itemView.getTop();
mDragItemHeight = itemView.getHeight();
mMinDragY = mRawOffsetY;
// 根据是否显示完全,设定快照在Y轴上可拖到的最大值
if (isShowAll()) {
mMaxDragY = mRawOffsetY + getChildAt(getAdapter().getCount() - 1).getTop();
} else {
mMaxDragY = mRawOffsetY + getHeight() - mDragItemHeight;
}
createDragPhotoView();
return true;
}
/**
* 判断ListView是否全部显示,即ListView无法上下滚动了
*/
private boolean isShowAll() {
if (getChildCount() == 0) {
return true;
}
View firstChild = getChildAt(0);
int itemAllHeight = firstChild.getBottom() - firstChild.getTop() + getDividerHeight();
return itemAllHeight * getAdapter().getCount() < getHeight();
}
/**
* 创建拖拽快照
*/
private void createDragPhotoView() {
// 获取当前窗口管理器
mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
// 创建布局参数
mWindowLayoutParams = new WindowManager.LayoutParams();
mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mWindowLayoutParams.gravity = Gravity.TOP | Gravity.START;
mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 期望的图片为半透明效果,但设置其他值并没有看到不一样的效果
// 下面这些参数能够帮助准确定位到选中项点击位置
mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mWindowLayoutParams.windowAnimations = 0; // 无动画
mWindowLayoutParams.alpha = DRAG_PHOTO_VIEW_ALPHA; // 微透明
mWindowLayoutParams.x = mDownX + mRawOffsetX - mItemOffsetX;
mWindowLayoutParams.y = adjustDragY(mDownY + mRawOffsetY - mItemOffsetY);
mDragPhotoView = new ImageView(getContext());
mDragPhotoView.setImageBitmap(mDragPhotoBitmap);
mWindowManager.addView(mDragPhotoView, mWindowLayoutParams);
}
/**
* 校正Drag的值,不让其越界
*/
private int adjustDragY(int y) {
if (y < mMinDragY) {
return mMinDragY;
} else if (y > mMaxDragY) {
return mMaxDragY;
}
return y;
}
/**
* 根据Adapter中的位置获取对应ListView的条目
*/
private View getItemView(int position) {
if (position < 0 || position >= getAdapter().getCount()) {
return null;
}
int index = position - getFirstVisiblePosition();
return getChildAt(index);
}
/**
* 更新快照的位置
*/
private void updateDragView() {
if (mDragPhotoView != null) {
mWindowLayoutParams.y = adjustDragY(mMoveY + mRawOffsetY - mItemOffsetY);
mWindowManager.updateViewLayout(mDragPhotoView, mWindowLayoutParams);
}
}
/**
* 更新条目位置、显示等
*/
private void updateItemView() {
int position = pointToPosition(mMoveX, mMoveY);
if (position != AdapterView.INVALID_POSITION) {
mToPosition = position;
}
// 调换位置,并把显示进行调换
if (mFromPosition != mToPosition) {
if (exchangePosition()) {
View view = getItemView(mFromPosition);
if (view != null) {
view.setVisibility(View.VISIBLE);
}
view = getItemView(mToPosition);
if (view != null) {
view.setVisibility(View.INVISIBLE);
}
mFromPosition = mToPosition;
}
}
// 如果当前位置已经不到一个条目,则进行上或下的滚动。并根据距离边界的距离,设定滚动速度
int dragY = mMoveY - mItemOffsetY;
if (dragY < mDragItemHeight) {
int value = Math.max(0, dragY); // 防越界
float percent = estimatePercent(mDragItemHeight, 0, value);
int distance = estimateInt(0, -mMaxDistance, percent);
smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
} else if (dragY > getHeight() - 2 * mDragItemHeight) {
int value = Math.max(0, getHeight() - dragY - mDragItemHeight); // 防越界
float percent = estimatePercent(mDragItemHeight, 0, value);
int distance = estimateInt(0, mMaxDistance, percent);
smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
}
}
/**
* 停止拖拽
*/
private void stopDrag() {
// 显示坐标上的条目
View view = getItemView(mToPosition);
if (view != null) {
view.setVisibility(View.VISIBLE);
}
// 移除快照
if (mDragPhotoView != null) {
mWindowManager.removeView(mDragPhotoView);
mDragPhotoView.setImageDrawable(null);
mDragPhotoBitmap.recycle();
mDragPhotoBitmap = null;
mDragPhotoView = null;
}
mIsDraging = false;
}
/**
* 调换位置
*/
private boolean exchangePosition() {
int itemCount = getAdapter().getCount();
if (mFromPosition >= 0 && mFromPosition < itemCount
&& mToPosition >= 0 && mToPosition < itemCount) {
getAdapter().swapData(mFromPosition, mToPosition);
return true;
}
return false;
}
/**
* 根据百分比,估算在指定范围内的值
*/
public static int estimateInt(int start ,int end, @FloatRange(from = 0.0f, to = 1.0f) float percent) {
return (int) (start + percent * (end - start));
}
/**
* 估算给定值在指定范围内的百分比
* @param start 始值
* @param end 末值
* @param value 要估算的值
* @return 0.0f ~ 1.0f。如果没有指定范围,或给定值不在范围内则返回-1
*/
public static float estimatePercent(float start, float end, float value) {
if (start == end
|| (value < start && value < end)
|| (value > start && value > end)){
return -1;
}
return (value - start) / (end - start);
}
@Override
public void setAdapter(ListAdapter adapter) {
if (!(adapter instanceof DragListViewAdapter)){
throw new RuntimeException("请使用自带的Adapter");
}
super.setAdapter(adapter);
}
@Override
public DragListViewAdapter getAdapter(){
return (DragListViewAdapter) super.getAdapter();
}
}
MainActivity.java
private void initView() {
dvl_drag_list = (DragListView) findViewById(R.id.dvl_drag_list);
tv_msg_drag_list = (TextView) findViewById(R.id.tv_msg_drag_list);
tv_msg_drag_list.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int size = mDataList.size();
String dataMsg;
if (size == 0) {
dataMsg = "没有数据了";
} else {
dataMsg = "数据大小:" + mDataList.size() + ", 最后一个:" + mDataList.get(mDataList.size() - 1);
}
tv_msg_drag_list.setText(dataMsg);
}
});
mListAdapter = new MyAdapter(this, mDataList);
dvl_drag_list.setAdapter(mListAdapter);
}
public class MyAdapter extends DragListViewAdapter<String> {
public MyAdapter(Context context, List<String> dataList) {
super(context, dataList);
}
@Override
public View getItemView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item_drag_list, parent, false);
viewHolder = new ViewHolder();
viewHolder.name = (TextView) convertView.findViewById(R.id.tv_name_drag_list);
viewHolder.desc = (TextView) convertView.findViewById(R.id.tv_desc_drag_list);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.name.setText(mDragDatas.get(position));
String s = mDragDatas.get(position) + "的描述";
viewHolder.desc.setText(s);
return convertView;
}
class ViewHolder{
TextView name;
TextView desc;
}
}
DragListViewAdapter.java
public abstract class DragListViewAdapter<T> extends BaseAdapter{
public Context mContext;
public List<T> mDragDatas;
public DragListViewAdapter(Context context, List<T> dataList){
this.mContext = context;
this.mDragDatas = dataList;
}
@Override
public int getCount() {
return mDragDatas == null ? 0 : mDragDatas.size();
}
@Override
public T getItem(int position) {
return mDragDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getItemView(position, convertView, parent);
}
public abstract View getItemView(int position, View convertView, ViewGroup parent);
public void swapData(int from, int to){
Collections.swap(mDragDatas, from, to);
notifyDataSetChanged();
}
public void deleteData(int position) {
mDragDatas.remove(position);
notifyDataSetChanged();
}
}
参考
开源项目DragSortListView:https://github.com/bauerca/drag-sort-listview
http://www.bkjia.com/Androidjc/995839.html
http://www.cnblogs.com/qianxudetianxia/archive/2011/06/12/2068761.html