HardBirch

说说Android桌面(Launcher应用)背后的故事(六)——研究Launcher而实现的附属品(可以拖拽的ListView)

时间:11-12-01 栏目:安卓学习笔记 作者:张飞不张,文采横飞 评论:4 点击: 2,106 次

本来这一篇将写Android中Launcher是如何实现桌面上item的拖拽的,当研究了其机理之后,突然大脑发热,想实现一个可以拖拽的ListView,在理解了Launcher中item的拖拽,再来实现可以拖拽的ListView简直就是小菜一碟了。于是将此篇位于Launcher中拖拽之前,可以起到一个过渡理解的作用。只是,这里还有些不一样的地方就是Launcher上item拖拽后可以放到一个空的位置,而ListView中某个item被拖拽之后是需要交换新位置和原来位置上的item。下面,就不再废话了,直接上代码,具体需要注意的地方请看注释:

public class DragListView extends ListView{
	private static final String TAG = "DragListView";
	private static final int INVALID_POSITION = -1;
	private Bitmap mDragBitmap;

	private View mDragView;

	private UorderDeleteZone zone;

	private View footer;

	private Object srcContent;

	private int startPosition;

	private Paint mPaint = new Paint();

	private float mLastMotionX;
	private float mLastMotionY;

	private float mTouchOffsetX;
	private float mTouchOffsetY;

	private float mBitmapOffsetX;
	private float mBitmapOffsetY;

	private boolean mDragging = false;

	private Rect mDragRect = new Rect(); //当拖动一个item的时候,记录拖动经过的区域

	//是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
	private int mScaledTouchSlop;

	private float mTopScrollBound;//向上滑动超过这个边界的时候,上面的item向下滚动

	private float mBottomScrollBound;//向下滑动超过这个边界的时候,下面的item开始向上滚动

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

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

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

		/**
		 * 当某个item被拖拽时,将其颜色改变下,
		 */
        final int srcColor = context.getResources().getColor(R.color.drag_filter);
        mPaint.setColorFilter(new PorterDuffColorFilter(srcColor, PorterDuff.Mode.SRC_ATOP));

        //是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

	}

	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);
		if(mDragging && mDragBitmap != null){
			final int scrollX = getScrollX();
			final int scrollY = getScrollY();

			float left = scrollX + mLastMotionX-mTouchOffsetX-mBitmapOffsetX;
			float top = scrollY + mLastMotionY-mTouchOffsetY-mBitmapOffsetY;
			canvas.drawBitmap(mDragBitmap, left, top, mPaint);
		}

	}

	/**
	 * 在这个方法中判断当前用户按下事件所在的位置
	 * 如果该位置在右边30dip之内,就进行拖拽
	 */
	public boolean onInterceptTouchEvent(MotionEvent event){

		int action = event.getAction();
		final float x = event.getX();
		final float y = event.getY();

		if(getWidth()-x > 30){
			//不是位于拖拽区域,直接返回就OK了
			return super.onInterceptTouchEvent(event);
		}		

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastMotionX = x;
			mLastMotionY = y;
			break;
		default:
			break;
		}
		Log.e(TAG, "touchSlop:"+mScaledTouchSlop);
		//计算滚动边界
		/**
		 * 这里当向上拖拽到1/3屏幕高度的时候就滚动,但是这里如果开始拖拽的item就是位于这1/3屏幕内位置的
		 * 则,取当前小的一个.当然你也就可以设置成1/3的屏幕
		 */
		mTopScrollBound = Math.min(y-mScaledTouchSlop, getHeight()/3);
		mBottomScrollBound = Math.min(y + mScaledTouchSlop, getHeight()*2/3);

//		mTopScrollBound = getHeight()/3;
//		mBottomScrollBound = getHeight()*2/3;

		Log.e(TAG, "bound scroll:"+mTopScrollBound+","+mBottomScrollBound);

		//获取当前事件坐标所在的item项,ListView中拖动是上下拖动的,所以,和x坐标关系不大
		int position = this.pointToPosition((int)x, (int)y);

		if(position == INVALID_POSITION){
			return super.onInterceptTouchEvent(event);
		}

		startPosition = position;

		/**
		 * 这里获取当前被拖拽的item,注意,因为ListView中并不是每个item都是一个View,这个View是重用的
		 */
		mDragView = getChildAt(position-getFirstVisiblePosition());
		srcContent = getItemAtPosition(position);

		if(mDragView != null){

			mDragBitmap = createViewBitmap(mDragView);
			//当item被拖拽后,删除原来位置上的item,其实就是将Adapter中该位置的内容给删掉
			/**
			 * 注意,该ListView用的Adapter是ArrayAdapter,其有remove(Object item)方法
			 * 但是,经常处理ListView显示数据的应该知道,ArrayList的remove(object)方法实现了,
			 * 但是我们使用ArrayAdatper的时候,如果我们的数据传递到Adapter用的是数组,那么ArrayAdapter内
			 * 默认使用Arrays.asList(Object[])方法将数组转换为List对象,这样,当我们调用ArrayList的
			 * removeItem方法的时候,就会出现java.lang.UnsupportedOperationException异常。这是因为
			 * Arrays.asList(Object[])转换后的List是Arrays.ArrayList对象,而不是java.utils.ArrayList
			 * Arrays.ArrayList并没有实现remove方法,所以,执行父类AbstractList默认的remove方法,而
			 * AbstractList的remove方法就是抛出一个UnsupportedOperationException异常
			 */
			removeItem(position);

			mDragging = true;
		}

		return true;
	}

	@SuppressWarnings("unchecked")
	private void removeItem(int position){
		ArrayAdapter<Object> adapter = (ArrayAdapter<Object>)this.getAdapter();
		adapter.remove(adapter.getItem(position));
	}

	@SuppressWarnings("unchecked")
	private void addItem(int position){

		ArrayAdapter<Object> adapter = (ArrayAdapter<Object>)this.getAdapter();
		adapter.insert((String)srcContent, position);
	}

	public boolean onTouchEvent(MotionEvent event){
		super.onTouchEvent(event);

		if(!mDragging){
			return false;
		}

		final int action = event.getAction();
		final float x = event.getX();
		final float y = event.getY();

		switch(action){
		case MotionEvent.ACTION_DOWN:
			//mLastMotionX = x;
			mLastMotionY = y;
			break;
		case MotionEvent.ACTION_MOVE:
			final int scrollX = getScrollX();
			final int scrollY = getScrollY();

			int left = (int)(scrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX);
			int top = (int)(scrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY);

			final int width = mDragBitmap.getWidth();
			final int height = mDragBitmap.getHeight();

			mDragRect.set(left-1, top-1, width+left+1, top+height+1);
			Log.e(TAG, "每次move的距离:"+(y-mLastMotionY));
			//mLastMotionX = x;
			mLastMotionY = y;

			left = (int)(scrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX);
			top = (int)(scrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY);			

			mDragRect.union(left-1, top-1, width+left+1, top+height+1);

			invalidate(mDragRect);

			int scrollHeight = 0;

			if(y < mTopScrollBound){
				/**
				 * 如果当前move到的y位置为mTopScrollBound之上,则下面获取当前位置的item,
				 * 并将该item在当前位置的基础上,向下移动15个dip的偏移量,这样,当向上drag的时候
				 * 后面的item向下在滚动
				 *
				 * 向下拖拽的时候同理
				 */
				scrollHeight = 15;
			}else if(y > mBottomScrollBound){
				scrollHeight = -15;
			}

			if(scrollHeight != 0){
				//调用Listview方法实现滚动
				int position = this.pointToPosition((int)x, (int)y);
				if(position != INVALID_POSITION){
					/**
					 * 将当前位置的item向上或者向下移动scrollHeight个单位的距离
					 */
					setSelectionFromTop(position, getChildAt(position - getFirstVisiblePosition()).getTop()+scrollHeight);
				}

			}

			break;
		case MotionEvent.ACTION_UP:

			int position = this.pointToPosition((int)x, (int)y);

			if(position == INVALID_POSITION){
				position = startPosition;
			}

			float listTop = getChildAt(0).getTop();

			//这个是ListView可视区域的最下方,但是不一定数据集中的最后一项的位置
			float listBottom = getChildAt(getChildCount()-1).getBottom();

			if(y<listTop){

				position = 0;

			}else if(y > listBottom){
				/**
				 * 因为向下拖动的时候,ListView是向下滑动的
				 * 这里当item被往下拖到最下面时,将其添加到数据集中最后一个位置
				 * 这里注意是getCount()而不是getCount()-1。是向最后插入一个
				 */
				position = getAdapter().getCount();
			}

			stopDrag(position);
			break;
		}

		return true;
	}

	private void stopDrag(int newPosition) {
		mDragging = false;
		if(mDragBitmap != null){
			mDragBitmap.recycle();
		}

		//将拖动的item加入新的位置
		addItem(newPosition);

		invalidate();
	}

	/**
	 * 用当前View的绘制缓冲区创建一个Bitmap
	 * 同时进行一定的缩放
	 * @param view
	 * @return
	 */
	private Bitmap createViewBitmap(View view) {

		final int left = view.getLeft();
		final int top = view.getTop();

		//当前按住的位置相对于View本身的偏移量
		mTouchOffsetX = mLastMotionX - left;
		mTouchOffsetY = mLastMotionY - top;

		view.clearFocus();
		view.setPressed(false);

		boolean willNotCache = view.willNotCacheDrawing();
		view.setWillNotCacheDrawing(false);
		view.buildDrawingCache();

		Bitmap bitmap = view.getDrawingCache();
		final int width = bitmap.getWidth();
		final int height = bitmap.getHeight();

		//使用一个简单的Scale缩放
		Matrix matrix = new Matrix();
		float scaleFactor = view.getHeight();
		scaleFactor = (scaleFactor+40)/scaleFactor;
		matrix.setScale(1.0f, scaleFactor);

		Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);

		view.destroyDrawingCache();
		view.setWillNotCacheDrawing(willNotCache);

		//创建的缩放后的Bitmap和原Bitmap的padding
		mBitmapOffsetX = (newBitmap.getWidth()-width)/2;
		mBitmapOffsetY = (newBitmap.getHeight()-height)/2;

		return newBitmap;
	}

}

 

说说Android桌面(Launcher应用)背后的故事(六)——研究Launcher而实现的附属品(可以拖拽的ListView):目前有4 条留言

  1. 真的是感谢楼主!对自己帮助很大!谢谢了!!!

    2012-04-14 17:09 [回复]
  2. 地板
    freshwind90:

    楼主,求源码?

    2012-05-21 17:51 [回复]
  3. 板凳
    lhs286266503:

    LZ,看了你的这个。对自己帮助很大。谢谢了。但是我觉得里面有点逻辑不是很对。 mBottomScrollBound = Math.min(y + mScaledTouchSlop, getHeight()*2/3); 这句代码应该是最大值吧??否则按下时,接近顶部位置(就是在1/3内),往下一点点就滚动了,应是距离底部1/3的时候才滚吧

    2012-07-21 18:16 [回复]
  4. 沙发
    j15029990601:

    楼主,求源码,能否割爱?

    2012-08-01 11:35 [回复]

发表评论


QQ群互动

Linux系统与内核学习群:194051772

WP建站技术学习交流群:194062106

魔豆之路QR

魔豆的Linux内核之路

魔豆的Linux内核之路

优秀工程师当看优秀书籍

优秀程序员,要看优秀书!

赞助商广告

友荐云推荐