Android 自定义可拖拽ListView,思想最重要。

这篇博客探讨了在Android开发中如何自定义可拖拽的ListView,强调了编程中的思想重要性。作者通过介绍关键方法,如获取触摸位置对应的item及跟随手指移动的布局,来解释实现拖拽功能的思路。示例代码展示了如何处理触摸事件,更新item的位置,以及在Activity中实现数据交换,从而实现ListView元素的拖拽效果。
摘要由CSDN通过智能技术生成

        最近好多小伙伴都结婚了,甚至还有二婚的,我终于总结出为什么如此热爱生活的同时,钱包却一直不见涨的原因。想想以后的日子,结婚的生孩子,二婚的生孩子,然后有的结婚,有的二婚,有的多婚。。深深感叹:时不我待啊!!!

        高中老师的谆谆教诲,一直牢记在心中,那时他说:兔子不是窝边草,你看看你们,都在自己班级处对象!那时看他痛心疾首,一副恨铁不成钢的样子,使我深有感触,一直坚持到现在。最近受伙伴们的刺激,我终于发觉,老师的话是错的,现在也只有窝边有草了。。。。于是乎,我冒出个想法,写完此篇博客,我要逛遍公司六层。不要问我为什么,这战乱的年代,我看看窝边还有没有能吃的草。。。

        人们都说有压力才有动力,而我认为有动力未必有灵感,所以保持心情愉悦,灵感自来。

        吹牛结束,开始正文:

        如题,在我接触变成的时候,第一位导师常常挂在嘴边的话就是:思想最重要。对此我郁闷了很久很久。终于在日积月累的代码生涯中,领悟了这句话。至于什么时候会有思想,这点不好说,也许吃饭的时候,也许在卫生间思考的时候,也许做梦的时候。。

        Android 自定义可拖拽ListView,思想最重要。

        先看效果,由于模拟器问题,List的下划线显示的有些乱。真机上是没问题的

                               

        开篇之前,需要知道几个有关系的方法,当知道这个方法后,相信很多人就能够自己去实现了。

        

/**
     * Maps a point to a position in the list.
     *
     * @param x X in local coordinate
     * @param y Y in local coordinate
     * @return The position of the item which contains the specified point, or
     *         {@link #INVALID_POSITION} if the point does not intersect an item.
     */
    public int pointToPosition(int x, int y) 
        此方法的官方解释:映射一个点到列表的位置。

        就是说:参数是手指按在屏幕的坐标,返回值是这个坐标所存在的item的position值。

        既然想拖拽item,就必须要获取到这个item的对象。就需要用到下面的方法:

    /**
     * Returns the view at the specified position in the group.
     *
     * @param index the position at which to get the view from
     * @return the view at the specified position or null if the position
     *         does not exist within the group
     */
    public View getChildAt(int index)
        此方法是根据获得的position值来获取对应position的item的View对象。这里需要注意一点,在我们日常的ListView开发中,经常需要对View对象进行复用优化。那么着就引起了接下来的问题,对象被复用后,在传入position这个位置值时,那么我们获取的对象就有可能是之前复用的。换句话说,我们的ListView优化后,屏幕上显示多少个item,也就有多少个View对象,如果有10个View对象的话,当我点击第18个item的时候,通过此方法,返回的对象实际上就是第八个item的实例,那么如何解决这个问题,Google提供了下面这个方法:

        

    /**
     * Returns the position within the adapter's data set for the first item
     * displayed on screen.
     *
     * @return The position within the adapter's data set
     */
    public int getFirstVisiblePosition()

        这里返回的值,是当前屏幕上显示的所有item中得第一个item的position值。所以我们的用法是:

        

getChildAt(position - getFirstVisiblePosition())

        这样我们得到的对象就是当前点击的item正在使用的View对象(如果复用,就是复用后的)。

        当我们拖动item的时候,为了知道此时是正在拖动的状态和拖动的item是哪个,就需要让这个item的整体布局跟着手指来移动。那么我们获取到item的布局,然后让他跟着手指移动?很抱歉,布局已经绘制了,状态是locked。重新new一个布局来跟着手指拖动?先不说这样很麻烦,对于应用中得各种各样的item布局,难道还要绘制各种各样的View来对应?万能的谷姐同样提供了这样的方法:

        

    /**
     * <p>Enables or disables the drawing cache. When the drawing cache is enabled, the next call
     * to {@link #getDrawingCache()} or {@link #buildDrawingCache()} will draw the view in a
     * bitmap. Calling {@link #draw(android.graphics.Canvas)} will not draw from the cache when
     * the cache is enabled. To benefit from the cache, you must request the drawing cache by
     * calling {@link #getDrawingCache()} and draw it on screen if the returned bitmap is not
     * null.</p>
     *
     * <p>Enabling the drawing cache is similar to
     * {@link #setLayerType(int, android.graphics.Paint) setting a layer} when hardware
     * acceleration is turned off. When hardware acceleration is turned on, enabling the
     * drawing cache has no effect on rendering because the system uses a different mechanism
     * for acceleration which ignores the flag. If you want to use a Bitmap for the view, even
     * when hardware acceleration is enabled, see {@link #setLayerType(int, android.graphics.Paint)}
     * for information on how to enable software and hardware layers.</p>
     *
     * <p>This API can be used to manually generate
     * a bitmap copy of this view, by setting the flag to <code>true</code> and calling
     * {@link #getDrawingCache()}.</p>
     *
     * @param enabled true to enable the drawing cache, false otherwise
     *
     * @see #isDrawingCacheEnabled()
     * @see #getDrawingCache()
     * @see #buildDrawingCache()
     * @see #setLayerType(int, android.graphics.Paint)
     */
    public void setDrawingCacheEnabled(boolean enabled)

        一看这么一大段注释就头疼,其实就是说,将View的绘制进行缓存。缓存成什么东西呢?

        

    /**
     * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p>
     *
     * @return A non-scaled bitmap representing this view or null if cache is disabled.
     *
     * @see #getDrawingCache(boolean)
     */
    public Bitmap getDrawingCache() 

        缓存成Bitmap,恍然大明白,缓存成图像啊,那这回简单了,拖动哪个item,将它变成皂片就可以了。

        OK,了解了以上的方法,再了解自定义View的常用方法。那么只要肯下功夫,Anyone can do。如果看到这里还有Anyone 不 can do。那就接着往下看吧。

        首先定义一个响亮而又文雅的名字,这样才显着有范。然后是构造方法,初始化常量,然后处理事件。处理事件的逻辑是,在按下一定时间后,显示出对应item的皂片,咱后根据手指拖动,实时跟新皂片的位置,并进行item的更新,以达到替换效果。然后在停止拖动的时候释放资源。逻辑理清后,看看代码实现,注释都已经写在里面了:

        

package com.qiyuan.activity.view;

import android.R.integer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;

public class CanDragListView extends ListView {

	private ListAdapter mAdapter;
	private WindowManager mWindowManager;
	/**
	 * item镜像的布局参数
	 */
	private WindowManager.LayoutParams mWindowLayoutParams;
	private WindowManager.LayoutParams mNewWindowLayoutParams;
	/**
	 * 振动器
	 */
	private Vibrator mVibrator;
	/**
	 * 选中的item的position
	 */
	private int mSelectedPosition;
	/**
	 * 选中的item的View对象
	 */
	private View mItemView;
	/**
	 * 用于拖拽的镜像,这里直接用一个ImageView装载Bitmap
	 */
	private ImageView mDragIV;
	private ImageView mNewDragIv;
	/**
	 * 选中的item的镜像Bitmap
	 */
	private Bitmap mBitmap;
	/**
	 * 按下的点到所在item的上边缘的距离
	 */
	private int mPoint2ItemTop;

	/**
	 * 按下的点到所在item的左边缘的距离
	 */
	private int mPoint2ItemLeft;

	/**
	 * CanDragListView距离屏幕顶部的偏移量
	 */
	private int mOffset2Top;
	/**
	 * CanDragListView自动向下滚动的边界值
	 */
	private int mDownScrollBorder;

	/**
	 * CanDragListView自动向上滚动的边界值
	 */
	private int mUpScrollBorder;
	/**
	 * CanDragListView自动滚动的速度
	 */
	private static final int speed = 20;

	/**
	 * CanDragListView距离屏幕左边的偏移量
	 */
	private int mOffset2Left;
	/**
	 * 状态栏的高度
	 */
	private int mStatusHeight;
	/**
	 * 按下的系统时间
	 */
	private long mActionDownTime = 0;
	/**
	 * 移动的系统时间
	 */
	private long mActionMoveTime = 0;
	/**
	 * 默认长按事件时间是1000毫秒
	 */
	private long mLongClickTime = 1000;
	/**
	 * 是否可拖拽,默认为false
	 */
	private boolean isDrag = false;
	/**
	 * 按下是的x坐标
	 */
	private int mDownX;
	/**
	 * 按下是的y坐标
	 */
	private int mDownY;

	/**
	 * item发生变化回调的接口
	 */
	private OnChanageListener onChanageListener;

	/**
	 * 设置回调接口
	 * 
	 * @param onChanageListener
	 */
	public void setOnChangeListener(OnChanageListener onChanageListener) {
		this.onChanageListener = onChanageListener;
	}

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

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

	public CanDragListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		mAdapter = getAdapter();
		mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
		mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		mStatusHeight = getStatusHeight(context); // 获取状态栏的高度
	}

	private Handler mHandler = new Handler();

	// 用来处理长按的Runnable
	private Runnable mLongClickRunnable = new Runnable() {

		@Override
		public void run() {
			isDrag = true; // 设置可以拖拽
			mVibrator.vibrate(100); // 震动100毫秒
			if (mItemView != null) {
				mItemView.setVisibility(View.INVISIBLE);// 隐藏该item
			}
			Log.i("CanDragListView", "**mLongClickRunnable**");
			// 根据我们按下的点显示item镜像
			createDragImage(mBitmap, mDownX, mDownY);
		}
	};

	/**
	 * 当mDownY的值大于向上滚动的边界值,触发自动向上滚动 当mDownY的值小于向下滚动的边界值,触犯自动向下滚动 否则不进行滚动
	 */
	private Runnable mScrollRunnable = new Runnable() {

		@Override
		public void run() {
			int scrollY;
			if (mDownY > mUpScrollBorder) {
				scrollY = speed;
				mHandler.postDelayed(mScrollRunnable, 25);
			} else if (mDownY < mDownScrollBorder) {
				scrollY = -speed;
				mHandler.postDelayed(mScrollRunnable, 25);
			} else {
				scrollY = 0;
				mHandler.removeCallbacks(mScrollRunnable);
			}

			// 所以我们在这里调用下onSwapItem()方法来交换item
			onSwapItem(mDownY, mDownY);

			smoothScrollBy(scrollY, 10);
		}
	};

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// Log.i("CanDragListView", mSelectedPosition+"****"+mItemView);
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			mActionDownTime = event.getDownTime();
			mDownX = (int) event.getX();
			mDownY = (int) event.getY();

			// 根据按下的坐标获取item对应的position
			mSelectedPosition = pointToPosition(mDownX, mDownY);
			// 如果是无效的position,即值为-1
			if (mSelectedPosition == AdapterView.INVALID_POSITION) {
				return super.onTouchEvent(event);
			}
			// 根据position获取对应的item
			mItemView = getChildAt(mSelectedPosition - getFirstVisiblePosition());
			// 使用Handler延迟mLongClickTime执行mLongClickRunnable
			mHandler.postDelayed(mLongClickRunnable, mLongClickTime);
			if (mItemView != null) {
				// 下面这几个距离大家可以参考我的博客上面的图来理解下
				mPoint2ItemTop = mDownY - mItemView.getTop();
				mPoint2ItemLeft = mDownX - mItemView.getLeft();

				mOffset2Top = (int) (event.getRawY() - mDownY);
				mOffset2Left = (int) (event.getRawX() - mDownX);

				// 获取CanDragListView自动向上滚动的偏移量,小于这个值,CanDragListView向下滚动
				mDownScrollBorder = getHeight() / 4;
				// 获取CanDragListView自动向下滚动的偏移量,大于这个值,CanDragListView向上滚动
				mUpScrollBorder = getHeight() * 3 / 4;

				// 将该item进行绘图缓存
				mItemView.setDrawingCacheEnabled(true);
				// 从缓存中获取bitmap
				mBitmap = Bitmap.createBitmap(mItemView.getDrawingCache());
				// 释放绘图缓存,避免出现重复的缓存对象
				mItemView.destroyDrawingCache();
			}

			// Log.i("CanDragListView", "****"+isDrag);
			break;
		case MotionEvent.ACTION_MOVE:
			// TODO
			if (isDrag) {
				int moveX = (int) event.getX();
				int moveY = (int) event.getY();
				if (!isOnTouchInItem(mItemView, moveX, moveY)) {
					mHandler.removeCallbacks(mLongClickRunnable);
				}
				mDownX = moveX;
				mDownY = moveY;
				onDragItem(moveX, moveY);
			}
			break;
		case MotionEvent.ACTION_UP:
			onStopDrag();
			mHandler.removeCallbacks(mLongClickRunnable);
			mHandler.removeCallbacks(mScrollRunnable);

			isDrag = false;
			break;
		default:
			break;
		}

		return super.onTouchEvent(event);
	}

	/**
	 * 判断手指按下的坐标是否在item范围内
	 * 
	 * @param view
	 * @param downX
	 * @param downY
	 * @return
	 */
	private boolean isOnTouchInItem(View view, int downX, int downY) {
		if (view == null) {
			return false;
		}
		int leftX = view.getLeft();
		int topY = view.getTop();
		if (downX < leftX || downX > leftX + view.getWidth()) {
			return false;
		}
		if (downY < topY || downY > topY + view.getHeight()) {
			return false;
		}
		return true;
	}

	/**
	 * 创建拖动的镜像
	 * 
	 * @param bitmap
	 * @param downX
	 *            按下的点相对父控件的X坐标
	 * @param downY
	 *            按下的点相对父控件的X坐标
	 */
	private void createDragImage(Bitmap bitmap, int downX, int downY) {
		mWindowLayoutParams = new WindowManager.LayoutParams();
		mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
		mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
		mWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
		mWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
		mWindowLayoutParams.alpha = 0.55f; // 透明度
		mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
		mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
		mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

		mNewWindowLayoutParams = new WindowManager.LayoutParams();
		mNewWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 图片之外的其他地方透明
		mNewWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
		mNewWindowLayoutParams.x = downX - mPoint2ItemLeft + mOffset2Left;
		mNewWindowLayoutParams.y = downY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
		// mNewWindowLayoutParams.alpha = 0.55f; // 透明度
		mNewWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
		mNewWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
		mNewWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

		mDragIV = new ImageView(getContext());
		mNewDragIv = new ImageView(getContext());
		mDragIV.setImageBitmap(bitmap);
		mWindowManager.addView(mDragIV, mWindowLayoutParams);
		mWindowManager.addView(mNewDragIv, mNewWindowLayoutParams);
	}

	/**
	 * 移除镜像
	 */
	private void removeDragImage() {
		if (mDragIV != null) {
			mWindowManager.removeView(mDragIV);
			mDragIV = null;
		}
		if (mNewDragIv != null) {
			mWindowManager.removeView(mNewDragIv);
			mNewDragIv = null;
		}
	}

	/**
	 * 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及ListView的自行滚动
	 * 
	 * @param x
	 * @param y
	 */
	private void onDragItem(int moveX, int moveY) {
		if (mWindowLayoutParams != null && mDragIV != null) {
			mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left;
			mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top - mStatusHeight;
			mWindowManager.updateViewLayout(mDragIV, mWindowLayoutParams); // 更新镜像的位置
		}
		onSwapItem(moveX, moveY);
		// ListView自动滚动
		mHandler.post(mScrollRunnable);

	}

	/**
	 * 交换item,并且控制item之间的显示与隐藏效果
	 * 
	 * @param moveX
	 * @param moveY
	 */
	private void onSwapItem(int moveX, int moveY) {
		// 获取我们手指移动到的那个item的position
		int position = pointToPosition(moveX, moveY);

		// 假如tempPosition 改变了并且tempPosition不等于-1,则进行交换
		if (position != mSelectedPosition && position != AdapterView.INVALID_POSITION) {
			
			// mAdapter.getItem(mSelectedPosition);
			View newItem = getChildAt(position - getFirstVisiblePosition());
			View oldItem = getChildAt(mSelectedPosition - getFirstVisiblePosition());

			mNewWindowLayoutParams.x = moveX - (moveX - oldItem.getLeft()) + mOffset2Left;
			mNewWindowLayoutParams.y = moveY - (moveY - oldItem.getTop()) + mOffset2Top - mStatusHeight;

			newItem.setDrawingCacheEnabled(true);
			Bitmap bitmap = Bitmap.createBitmap(newItem.getDrawingCache());
			newItem.destroyDrawingCache();
			mNewDragIv.setImageBitmap(bitmap);
			if (newItem != null && oldItem != null) {
				newItem.setVisibility(INVISIBLE);// 隐藏拖动到的位置的item
				 oldItem.setVisibility(VISIBLE);//显示之前的
				mWindowManager.updateViewLayout(mNewDragIv, mNewWindowLayoutParams); // 更新镜像的位置
				if (onChanageListener != null) {
					 Log.i("CanDragListView", "**onSwapItem**");
					onChanageListener.onChange(mSelectedPosition, position);
				}
			}

			mSelectedPosition = position;
		}
	}

	/**
	 * 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除
	 */
	private void onStopDrag() {
		View view = getChildAt(mSelectedPosition - getFirstVisiblePosition());
		if (view != null) {
			view.setVisibility(View.VISIBLE);
		}
		// ((DragAdapter)this.getAdapter()).setItemHide(-1);
		removeDragImage();
	}

	/**
	 * 获取状态栏的高度
	 * 
	 * @param context
	 * @return
	 */
	private static int getStatusHeight(Context context) {
		int statusHeight = 0;
		Rect localRect = new Rect();
		((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
		statusHeight = localRect.top;
		if (0 == statusHeight) {
			Class<?> localClass;
			try {
				localClass = Class.forName("com.android.internal.R$dimen");
				Object localObject = localClass.newInstance();
				int i5 = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString());
				statusHeight = context.getResources().getDimensionPixelSize(i5);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return statusHeight;
	}

	/**
	 * 监听数据拖拽的接口,用来更新数据显示
	 */
	public interface OnChanageListener {

		/**
		 * 当item交换位置的时候回调的方法,在此处实现数据的交换
		 * 
		 * @param start
		 *            开始的position
		 * @param to
		 *            拖拽到的position
		 */
		public void onChange(int start, int to);
	}

}

        在代码里定义了改变的回调接口,目的是对item进行交换并且更新。开始我是想将item的更新都固定在自定义的ListView中,这样拿来就能直接用。思考良久,无法完美的实现。如果谁有好的办法,记得教我一下。

对于item的更新,一定要在Activity中实现OnChanageListener后,在方法里对数据集合进行元素位置交换。这里用的方法是:

        

    /**
     * Swaps the elements of list {@code list} at indices {@code index1} and
     * {@code index2}.
     *
     * @param list
     *            the list to manipulate.
     * @param index1
     *            position of the first element to swap with the element in
     *            index2.
     * @param index2
     *            position of the other element.
     *
     * @throws IndexOutOfBoundsException
     *             if index1 or index2 is out of range of this list.
     * @since 1.4
     */
    @SuppressWarnings("unchecked")
    public static void swap(List<?> list, int index1, int index2)

        参数是要交换数据的集合,和要交换的两个元素的位置。用法是:

		mListView.setOnChangeListener(new OnChanageListener() {
			
			@Override
			public void onChange(int start, int to) {
				//数据交换
                if(start < to){  
                    for(int i=start; i<to; i++){  
                        Collections.swap(mList, i, i+1);  
                    }  
                }else if(start > to){  
                    for(int i=start; i>to; i--){  
                        Collections.swap(mList, i, i-1);  
                    }  
                }
                mAdapter.notifyDataSetChanged();
			}
		});

        到此,可拖拽的ListView已经完成。

        布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <include
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/include_title" />

    <com.qiyuan.activity.view.CanDragListView
        android:id="@+id/main_activity_lv"
        android:dividerHeight="0.2dp"
        android:divider="@color/blueviolet"
        android:footerDividersEnabled="false"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>


Android 自定义可拖拽ListView,思想最重要。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值