说说Android桌面(Launcher应用)背后的故事(八)——让桌面的精灵穿越起来

博客搬家啦——为了更好地经营博客,本人已经将博客迁移至www.ijavaboy.com。这里已经不再更新,给您带来的不便,深感抱歉!这篇文章的新地址:点击我微笑 

       

        有了前面的工作,基本上这个桌面就已经像模像样了,但是,和系统自带的Launcher相比,还差得很远。其中,系统Launcher的桌面上的item是可以任意穿越(移动)的。同时,在其穿越的过程中,你也可以将其kill掉。在这篇文章中,就让我们来看看桌面上的精灵如何实现她们穿越的梦想….

         系统Launcher为了实现item的拖拽,可谓下了很大的功夫,面面俱到。为了实现拖拽的功能,其定义了一组比较抽象的概念:拖拽源(DragSource);拖拽目的地(DragTarget);拖拽控制器(DragController);和拖拽界面(DragLayer)。DragSource主要用来表示桌面上的item可以从哪些位置被拖动,这里的“哪些位置”就是用DragSource来标识。比如,系统Launcher中的Workspace,AllAppsGridView,Folder都是DragSource,也就是在这些位置里的item可以被拖拽。DragTarget则对应的,表示可以容纳一个item的位置。比如,系统Launcher中的Workspace,Folder,DeleteZone就是DragTarget,也就是我们可以拖着某个item在这些位置上放下,item就被移到这个位置了。同时,为了处理多个桌面之间的拖动,其定义了一个DragScroller来负责处理多个桌面之间拖动的时候的逻辑。一个item在桌面上的生命周期有来,自然也得有去的时候,拖拽的时候,桌面下方的某块区域固定为删除区域(DeleteZone),这样,当被拖拽的item在这个区域内被释放的时候,就意味着其在桌面上生命的结束。

有了这个基本的了解,再进入代码中寻求更细的答案就不再费事了。下面,就让我们继续实现我们未完成的功能:

我们知道,前面的部分,所有的动作都是始于用户长按桌面某个空白的区域,那么当用户长按的是桌面上的某个item,我们要处理什么呢?对,就是拖拽了。所以,在Launcher中的onLongClick事件中,我们要加入长按某个item的处理。完整的onLongClick方法如下:

  1. <SPAN style="FONT-SIZE: 13px">  /** 
  2.      * 在这个方法中需要判断当前长按的事件是在空白的区域还是某个item 
  3.      */  
  4.     @Override  
  5.     public boolean onLongClick(View v) {  
  6.         //ActivityUtils.alert(getApplication(), "长按");   
  7.         if(!(v instanceof UorderCellLayout)){  
  8.             v = (View)v.getParent(); //如果当前点击的是item,得到其父控件,即UorderCellLayout   
  9.         }  
  10.           
  11.         CellInfo cellInfo = (CellInfo)v.getTag(); //这里获取cellInfo信息   
  12.   
  13.         if(cellInfo == null){  
  14.             Log.v(TAG, "CellInfo is null");  
  15.             return true;  
  16.         }  
  17.           
  18.         //Log.v(TAG, ""+cellInfo.toString());   
  19.         /** 
  20.          * 注意,我们在CellLayout中获取当前位置信息的时候,就一并判断了当前位置上是否是item,如果是,则将 
  21.          * item保存在cellinfo.view中 
  22.          */  
  23.         if(cellInfo.view == null){  
  24.             //说明是空白区域   
  25.             Log.v(TAG, "onLongClick,cellInfo.valid:"+cellInfo.valid);  
  26.             if(cellInfo.valid){  
  27.                 //如果是有效的区域   
  28.                 addCellInfo = cellInfo;  
  29.                 showPasswordDialog(REQUEST_CODE_SETUP, null);     
  30.             }  
  31.         }else{  
  32.             //处理拖拽   
  33.             mWorkspace.startDrag(cellInfo);  
  34.         }  
  35.         return true;  
  36.     }</SPAN>  
	/**
	 * 在这个方法中需要判断当前长按的事件是在空白的区域还是某个item
	 */
	@Override
	public boolean onLongClick(View v) {
		//ActivityUtils.alert(getApplication(), "长按");
		if(!(v instanceof UorderCellLayout)){
			v = (View)v.getParent(); //如果当前点击的是item,得到其父控件,即UorderCellLayout
		}
		
		CellInfo cellInfo = (CellInfo)v.getTag(); //这里获取cellInfo信息

		if(cellInfo == null){
			Log.v(TAG, "CellInfo is null");
			return true;
		}
		
		//Log.v(TAG, ""+cellInfo.toString());
		/**
		 * 注意,我们在CellLayout中获取当前位置信息的时候,就一并判断了当前位置上是否是item,如果是,则将
		 * item保存在cellinfo.view中
		 */
		if(cellInfo.view == null){
			//说明是空白区域
			Log.v(TAG, "onLongClick,cellInfo.valid:"+cellInfo.valid);
			if(cellInfo.valid){
				//如果是有效的区域
				addCellInfo = cellInfo;
				showPasswordDialog(REQUEST_CODE_SETUP, null);	
			}
		}else{
			//处理拖拽
			mWorkspace.startDrag(cellInfo);
		}
		return true;
	}


 

在这个方法中当判断当前用户长按的是一个item的时候,执行mWorkspace.startDrag(cellInfo);所以,我们进入这个方法中看看其处理逻辑:

  1. <SPAN style="FONT-SIZE: 13px">  /** 
  2.      * 开始拖拽,这个方法并不真正处理拖拽行为,而是交付给DragController来完成 
  3.      * @param cellInfo 
  4.      */  
  5.     public void startDrag(UorderCellLayout.CellInfo cellInfo){  
  6.         View child = cellInfo.view;  
  7.           
  8.         UorderCellLayout layout = (UorderCellLayout)getChildAt(mCurrentScreen);  
  9.         layout.onDragChild(child);  
  10.           
  11.         cellInfo.screen = mCurrentScreen;  
  12.         //启动拖拽   
  13.         mDragController.startDrag(this, child, cellInfo);  
  14.         invalidate();  
  15.     }</SPAN>  
	/**
	 * 开始拖拽,这个方法并不真正处理拖拽行为,而是交付给DragController来完成
	 * @param cellInfo
	 */
	public void startDrag(UorderCellLayout.CellInfo cellInfo){
		View child = cellInfo.view;
		
		UorderCellLayout layout = (UorderCellLayout)getChildAt(mCurrentScreen);
		layout.onDragChild(child);
		
		cellInfo.screen = mCurrentScreen;
		//启动拖拽
		mDragController.startDrag(this, child, cellInfo);
		invalidate();
	}


 

在这个方法中,只是做了一些辅助的处理,真正实现拖拽的是DragController接口的实现者。那么,到这里,就自然的追溯到Workspace布局控件的父控件DragLayer控件了。接下来,就让我们进入DragLayer中一探item移动的秘密:

由于DragLayer实现了DragController接口,所以,我们先从刚刚中断的地方开始,先来看看startDrag中的逻辑,在这个方法里,屏蔽了输入法,创建了移动的Bitmap,同时,放大了一定的倍数,并设置了相关的动画参数。完整的代码如下:

  1. <SPAN style="FONT-SIZE: 13px">  public void startDrag(UorderWorkspace workspace, View v, CellInfo cellInfo) {  
  2.         Log.v(TAG, "开始拖拽...");  
  3.         //隐藏输入法   
  4.         if(mInputManager == null){  
  5.             mInputManager = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);  
  6.         }  
  7.         mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);  
  8.           
  9.         if(mDragListener != null){  
  10.             mDragListener.onDragStart();  
  11.         }  
  12.         Log.w(TAG, "当前view的scrollX,scrollY:"+v.getScrollX()+","+v.getScrollY());  
  13.           
  14.         mDragRect.set(v.getScrollX(), v.getScrollY(), 00);  
  15.         offsetDescendantRectToMyCoords(v, mDragRect); //将孩子的坐标系移到父坐标系中   
  16.           
  17.         //这里的mLastMotionX是在onInterceptTouchEvent中获取的   
  18.         mTouchOffsetX = (int)(mLastMotionX - mDragRect.left);  
  19.         mTouchOffsetY = (int)(mLastMotionY - mDragRect.top);  
  20.           
  21.         v.clearFocus();  
  22.         v.setPressed(false);  
  23.           
  24.         /** 
  25.          * 下面需要创建View对应的Bitmap,移动过程你看到的被你拖拽的对象是一个Bitmap对象 
  26.          */  
  27.         //设置绘制缓存   
  28.         boolean willNotCache = v.willNotCacheDrawing();  
  29.         v.setWillNotCacheDrawing(false);  
  30.         v.buildDrawingCache();  
  31.           
  32.         Bitmap bitmap = v.getDrawingCache();  
  33.         int width = bitmap.getWidth();  
  34.         int height = bitmap.getHeight();  
  35.           
  36.         /** 
  37.          * 我们知道,在Launcher中,我们长按某个item的时候,首先,一个动画效果将其弹起,并放大一定的倍数 
  38.          * 这里,就是计算放大的倍数,并按照该放大倍数绘制Bitmap 
  39.          */  
  40.         Matrix matrix = new Matrix();  
  41.         float scaleFactor = v.getWidth();  
  42.         //计算缩放比例   
  43.         scaleFactor = (scaleFactor + SCALE_SIZE) /scaleFactor;  
  44.         matrix.setScale(scaleFactor, scaleFactor);  
  45.         //根据指定的缩放比例创建一个Bitmap   
  46.         mDragBitmap = Bitmap.createBitmap(bitmap, 00, width, height, matrix, true);  
  47.         //清除绘制缓存   
  48.         v.destroyDrawingCache();  
  49.         v.setWillNotCacheDrawing(willNotCache);  
  50.           
  51.         //计算缩放后的Bitmap与View的offset   
  52.         mBitmapOffsetX = (mDragBitmap.getWidth()-width) / 2;  
  53.         mBitmapOffsetY = (mDragBitmap.getHeight()-height) /2;  
  54.   
  55.         //将原来的item保存,并隐藏掉   
  56.         mOriginator = v;  
  57.         v.setVisibility(View.GONE);  
  58.           
  59.         //根据scaleFactor指定动画起始大小   
  60.         /** 
  61.          * 这个主要是为了让桌面上的item在被弹起的时候,有一个过渡的动画效果,以免突兀 
  62.          * 这里设置初始值,在dispatchDraw中处理动画逻辑 
  63.          */  
  64.         mAnimationTo = 1.0f;  
  65.         mAnimationFrom = 1.0f / scaleFactor;  
  66.         mAnimationState = ANIMATION_STATE_STARTING;  
  67.         mAnimationType = ANIMATION_TYPE_SCALE;  
  68.         mAnimationDuration = ANIMATION_DURATION;  
  69.           
  70.         mDragging = true;  
  71.         mDrawBitmaptPaint = mPaint;  
  72.         mWorkspace = workspace;  
  73.         mCellInfo = cellInfo;  
  74.           
  75.         invalidate();  
  76.     }</SPAN>  
	public void startDrag(UorderWorkspace workspace, View v, CellInfo cellInfo) {
		Log.v(TAG, "开始拖拽...");
		//隐藏输入法
		if(mInputManager == null){
			mInputManager = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
		}
		mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
		
		if(mDragListener != null){
			mDragListener.onDragStart();
		}
		Log.w(TAG, "当前view的scrollX,scrollY:"+v.getScrollX()+","+v.getScrollY());
		
		mDragRect.set(v.getScrollX(), v.getScrollY(), 0, 0);
		offsetDescendantRectToMyCoords(v, mDragRect); //将孩子的坐标系移到父坐标系中
		
		//这里的mLastMotionX是在onInterceptTouchEvent中获取的
		mTouchOffsetX = (int)(mLastMotionX - mDragRect.left);
		mTouchOffsetY = (int)(mLastMotionY - mDragRect.top);
		
		v.clearFocus();
		v.setPressed(false);
		
		/**
		 * 下面需要创建View对应的Bitmap,移动过程你看到的被你拖拽的对象是一个Bitmap对象
		 */
		//设置绘制缓存
		boolean willNotCache = v.willNotCacheDrawing();
		v.setWillNotCacheDrawing(false);
		v.buildDrawingCache();
		
		Bitmap bitmap = v.getDrawingCache();
		int width = bitmap.getWidth();
		int height = bitmap.getHeight();
		
		/**
		 * 我们知道,在Launcher中,我们长按某个item的时候,首先,一个动画效果将其弹起,并放大一定的倍数
		 * 这里,就是计算放大的倍数,并按照该放大倍数绘制Bitmap
		 */
		Matrix matrix = new Matrix();
		float scaleFactor = v.getWidth();
		//计算缩放比例
		scaleFactor = (scaleFactor + SCALE_SIZE) /scaleFactor;
		matrix.setScale(scaleFactor, scaleFactor);
		//根据指定的缩放比例创建一个Bitmap
		mDragBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
		//清除绘制缓存
		v.destroyDrawingCache();
		v.setWillNotCacheDrawing(willNotCache);
		
		//计算缩放后的Bitmap与View的offset
		mBitmapOffsetX = (mDragBitmap.getWidth()-width) / 2;
		mBitmapOffsetY = (mDragBitmap.getHeight()-height) /2;

		//将原来的item保存,并隐藏掉
		mOriginator = v;
		v.setVisibility(View.GONE);
		
		//根据scaleFactor指定动画起始大小
		/**
		 * 这个主要是为了让桌面上的item在被弹起的时候,有一个过渡的动画效果,以免突兀
		 * 这里设置初始值,在dispatchDraw中处理动画逻辑
		 */
		mAnimationTo = 1.0f;
		mAnimationFrom = 1.0f / scaleFactor;
		mAnimationState = ANIMATION_STATE_STARTING;
		mAnimationType = ANIMATION_TYPE_SCALE;
		mAnimationDuration = ANIMATION_DURATION;
		
		mDragging = true;
		mDrawBitmaptPaint = mPaint;
		mWorkspace = workspace;
		mCellInfo = cellInfo;
		
		invalidate();
	}


 

到了这里,长按桌面某个item,item放大的原理想必就一目了然了。那么其,从原始大小向放大后的Bitmap是如何过渡的呢?这个,就让我们再到dispatchDraw中一探究竟。在这个方法中,处理了这个过渡的动画效果,其实,就是设定一个动画时间,然后在这么长时间内才从原始大小渐变到放大后的大小。完整代码如下:

  1. <SPAN style="FONT-SIZE: 13px">  /** 
  2.      * 在这个方法中,我们处理item从原始大小到放大后的大小的一个过渡效果 
  3.      */  
  4.     protected void dispatchDraw(Canvas canvas){  
  5.         super.dispatchDraw(canvas);  
  6.           
  7.         if(mDragging && mDragBitmap != null){  
  8.             if(mAnimationState == ANIMATION_STATE_STARTING){  
  9.                 /** 
  10.                  * 动画开始,记下当前时间 
  11.                  */  
  12.                 mAnimationState = ANIMATION_STATE_RUNNING;  
  13.                 mAnimationStartTime = SystemClock.uptimeMillis();  
  14.             }  
  15.               
  16.             int left = (int)(mScrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX);  
  17.             int top = (int)(mScrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY);  
  18.               
  19.             if(mAnimationState == ANIMATION_STATE_RUNNING){  
  20.                 //计算当前已经正常化了多少   
  21.                 float normalized = (float)(SystemClock.uptimeMillis()-mAnimationStartTime)/mAnimationDuration;  
  22.                 if(normalized >= 1.0f){  
  23.                     mAnimationState = ANIMATION_STATE_DONE;  
  24.                 }  
  25.                   
  26.                 normalized = Math.min(normalized, 1.0f);  
  27.                 float value = mAnimationFrom + (mAnimationTo - mAnimationFrom)*mAnimationDuration;  
  28.                   
  29.                 if(mAnimationType == ANIMATION_TYPE_SCALE){  
  30.                     //缩放当前的mDragBitmap   
  31.                     int width = mDragBitmap.getWidth();  
  32.                     int height = mDragBitmap.getHeight();  
  33.                       
  34.                     canvas.save();  
  35.                     canvas.translate(left, top);  
  36.                     canvas.translate(width*(1.0f-value), height*(1.0f-value));  
  37.                     canvas.scale(value, value);  
  38.                     canvas.drawBitmap(mDragBitmap, 0.0f, 0.0f, mDrawBitmaptPaint);//因为做了translate,所以这里是0.0   
  39.                     canvas.restore();  
  40.                 }  
  41.             }else{  
  42.                 //否则开始或者停止了,则按照当前的画   
  43.                   
  44.                 canvas.drawBitmap(mDragBitmap, left, top, mDrawBitmaptPaint);  
  45.             }  
  46.         }  
  47.     }</SPAN>  
	/**
	 * 在这个方法中,我们处理item从原始大小到放大后的大小的一个过渡效果
	 */
	protected void dispatchDraw(Canvas canvas){
		super.dispatchDraw(canvas);
		
		if(mDragging && mDragBitmap != null){
			if(mAnimationState == ANIMATION_STATE_STARTING){
				/**
				 * 动画开始,记下当前时间
				 */
				mAnimationState = ANIMATION_STATE_RUNNING;
				mAnimationStartTime = SystemClock.uptimeMillis();
			}
			
			int left = (int)(mScrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX);
			int top = (int)(mScrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY);
			
			if(mAnimationState == ANIMATION_STATE_RUNNING){
				//计算当前已经正常化了多少
				float normalized = (float)(SystemClock.uptimeMillis()-mAnimationStartTime)/mAnimationDuration;
				if(normalized >= 1.0f){
					mAnimationState = ANIMATION_STATE_DONE;
				}
				
				normalized = Math.min(normalized, 1.0f);
				float value = mAnimationFrom + (mAnimationTo - mAnimationFrom)*mAnimationDuration;
				
				if(mAnimationType == ANIMATION_TYPE_SCALE){
					//缩放当前的mDragBitmap
					int width = mDragBitmap.getWidth();
					int height = mDragBitmap.getHeight();
					
					canvas.save();
					canvas.translate(left, top);
					canvas.translate(width*(1.0f-value), height*(1.0f-value));
					canvas.scale(value, value);
					canvas.drawBitmap(mDragBitmap, 0.0f, 0.0f, mDrawBitmaptPaint);//因为做了translate,所以这里是0.0
					canvas.restore();
				}
			}else{
				//否则开始或者停止了,则按照当前的画
				
				canvas.drawBitmap(mDragBitmap, left, top, mDrawBitmaptPaint);
			}
		}
	}


 

前面都只是拖拽的准备工作,接下来就要看看拖拽是如何实现的了,那么经过前面系列文章的分析,到这里,你应该有一个自然而然的直觉:到onTouchEvent中去寻找答案。呵呵,因为要处理拖拽,无非就是处理MOTION_MOVE等事件。在这里,处理了移动区域的重新绘制,以及判断当前item所在的位置是否为屏幕的边缘20dip以内,如果是的话,则需要将其移动到下一个屏幕。同时,也需要判断当前item所在的位置是否是删除区域,如果是删除区域的话则将绘制Bitmap的画笔加一个变色蒙版,也就是我们看到当我们拖拽item经过垃圾箱区域的时候,被捉拽的item就变成红色了。下面,看看该方法的完整逻辑:

  1. <SPAN style="FONT-SIZE: 13px">  public boolean onTouchEvent(MotionEvent event){  
  2.         if(!mDragging){  
  3.             Log.v(TAG, "拖拽已经停止,传递到孩子");  
  4.             return false;  
  5.         }  
  6.           
  7.         final int action = event.getAction();  
  8.         final float x = event.getX();  
  9.         final float y = event.getY();  
  10.           
  11.         switch (action) {  
  12.         case MotionEvent.ACTION_DOWN:  
  13.             mLastMotionX = x;  
  14.             mLastMotionY = y;  
  15.               
  16.             /** 
  17.              * 这里我们需要判断 
  18.              * 我们规定屏幕左边和右边20dip以内为非拖拽区,这样当item停留在边缘的时候 
  19.              * 就标示需要移到下一个屏幕,当然如果这个方向有下一个屏幕的话 
  20.              */  
  21.   
  22.             if(x<NON_SCROLL_ZONE || x > getWidth()-NON_SCROLL_ZONE){  
  23.                   
  24.                 Log.v(TAG, "滑动到边缘");  
  25.                   
  26.                 mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
  27.                 if(x<NON_SCROLL_ZONE){  
  28.                     scrollAction.setDirection(DIRECTION_LEFT);  
  29.                 }else{  
  30.                     scrollAction.setDirection(DIRECTION_RIGHT);  
  31.                 }  
  32.                 postDelayed(scrollAction, SCROLL_DELAY);  
  33.                   
  34.             }else{  
  35.                 mScrollState = SCROLL_STATE_IN_ZONE;  
  36.             }  
  37.               
  38.             break;  
  39.               
  40.         case MotionEvent.ACTION_MOVE:  
  41.             final int scrollX = getScrollX();  
  42.             final int scrollY = getScrollY();  
  43.               
  44.             final int touchOffsetX = mTouchOffsetX;  
  45.             final int touchOffsetY = mTouchOffsetY;  
  46.               
  47.             final int bitmapOffsetX = mBitmapOffsetX;  
  48.             final int bitmapOffsetY = mBitmapOffsetY;  
  49.               
  50.             //计算上一个位置   
  51.             int left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);  
  52.             int top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);  
  53.               
  54.             final int width = mDragBitmap.getWidth();  
  55.             final int height = mDragBitmap.getHeight();  
  56.               
  57.             //上一个位置信息   
  58.             mDragRect.set(left-1,top-1,width+left+1,height+top+1);  
  59.               
  60.             mLastMotionX = x;  
  61.             mLastMotionY = y;  
  62.               
  63.             left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);  
  64.             top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);  
  65.             //设置新的位置,将所有拖拽经过的区域叠加在一起   
  66.             mDragRect.union(left-1, top-1, width+left+1, height+top+1);  
  67.               
  68.             //这里判断当前被拖拽的item是否在垃圾箱区域   
  69.             if(mDragListener != null){  
  70.                 Rect rect = new Rect();  
  71.                 int rawX = (int)(mLastMotionX - touchOffsetX - bitmapOffsetX);  
  72.                 int rawY = (int)(mLastMotionY - touchOffsetY - bitmapOffsetY);  
  73.                   
  74.                 rect.set(rawX, rawY, rawX+width, rawY+height);  
  75.                   
  76.                 boolean inDeleteZone = mDragListener.onDragMove(rect);  
  77.                   
  78.                 /** 
  79.                  * 如果是在删除区域,则拖拽的item的绘制颜色需要加个变色蒙版,系统Launcher中是变红 
  80.                  */  
  81.                 if(inDeleteZone){  
  82.                     mDrawBitmaptPaint = mTrashPaint;  
  83.                 }else{  
  84.                     mDrawBitmaptPaint = mPaint;  
  85.                 }  
  86.             }  
  87.               
  88.             //请求重新绘制拖拽区   
  89.             invalidate(mDragRect);  
  90.               
  91.             /** 
  92.              * 这里判断是否在边缘,如果是,则滑动到下一个屏幕 
  93.              */  
  94.               
  95.             if(x<NON_SCROLL_ZONE){  
  96.                 //当当前状态从滑动区域滑到边缘的时候进行处理   
  97.                 if(mScrollState == SCROLL_STATE_IN_ZONE){  
  98.                     mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
  99.                     scrollAction.setDirection(DIRECTION_LEFT);  
  100.                      //延时一定的时间,就是我们看到在边缘时,其停顿了一段时间再滑向了下一个屏幕   
  101.                     postDelayed(scrollAction, SCROLL_DELAY);   
  102.                 }  
  103.                   
  104.             }else if (x > getWidth() - NON_SCROLL_ZONE){  
  105.                   
  106.                 if(mScrollState == SCROLL_STATE_IN_ZONE){  
  107.                     Log.w(TAG, "滑动到下一个屏幕");  
  108.                     mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
  109.                     scrollAction.setDirection(DIRECTION_RIGHT);  
  110.                     postDelayed(scrollAction, SCROLL_DELAY);  
  111.                 }  
  112.                   
  113.             }else{  
  114.                 mScrollState = SCROLL_STATE_IN_ZONE;  
  115.             }  
  116.               
  117.             break;  
  118.               
  119.         case MotionEvent.ACTION_UP:  
  120.             removeCallbacks(scrollAction);  
  121.             //stopDrag();   
  122.             int rawX = (int)(mLastMotionX - mTouchOffsetX - mBitmapOffsetX);  
  123.             int rawY = (int)(mLastMotionY - mTouchOffsetY - mBitmapOffsetY);  
  124.             Rect rect = new Rect();  
  125.             rect.set(rawX, rawY, rawX+mDragBitmap.getWidth(), rawY+mDragBitmap.getHeight());  
  126.               
  127.             stopDrag((int)x,(int)y, rect);  
  128.               
  129.             break;  
  130.   
  131.         default:  
  132.             break;  
  133.         }  
  134.           
  135.         return true;  
  136.     }</SPAN>  
	public boolean onTouchEvent(MotionEvent event){
		if(!mDragging){
			Log.v(TAG, "拖拽已经停止,传递到孩子");
			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;
			
			/**
			 * 这里我们需要判断
			 * 我们规定屏幕左边和右边20dip以内为非拖拽区,这样当item停留在边缘的时候
			 * 就标示需要移到下一个屏幕,当然如果这个方向有下一个屏幕的话
			 */

			if(x<NON_SCROLL_ZONE || x > getWidth()-NON_SCROLL_ZONE){
				
				Log.v(TAG, "滑动到边缘");
				
				mScrollState = SCROLL_STATE_WAITING_OUTSIZE;
				if(x<NON_SCROLL_ZONE){
					scrollAction.setDirection(DIRECTION_LEFT);
				}else{
					scrollAction.setDirection(DIRECTION_RIGHT);
				}
				postDelayed(scrollAction, SCROLL_DELAY);
				
			}else{
				mScrollState = SCROLL_STATE_IN_ZONE;
			}
			
			break;
			
		case MotionEvent.ACTION_MOVE:
			final int scrollX = getScrollX();
			final int scrollY = getScrollY();
			
			final int touchOffsetX = mTouchOffsetX;
			final int touchOffsetY = mTouchOffsetY;
			
			final int bitmapOffsetX = mBitmapOffsetX;
			final int bitmapOffsetY = mBitmapOffsetY;
			
			//计算上一个位置
			int left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);
			int top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);
			
			final int width = mDragBitmap.getWidth();
			final int height = mDragBitmap.getHeight();
			
			//上一个位置信息
			mDragRect.set(left-1,top-1,width+left+1,height+top+1);
			
			mLastMotionX = x;
			mLastMotionY = y;
			
			left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);
			top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);
			//设置新的位置,将所有拖拽经过的区域叠加在一起
			mDragRect.union(left-1, top-1, width+left+1, height+top+1);
			
			//这里判断当前被拖拽的item是否在垃圾箱区域
			if(mDragListener != null){
				Rect rect = new Rect();
				int rawX = (int)(mLastMotionX - touchOffsetX - bitmapOffsetX);
				int rawY = (int)(mLastMotionY - touchOffsetY - bitmapOffsetY);
				
				rect.set(rawX, rawY, rawX+width, rawY+height);
				
				boolean inDeleteZone = mDragListener.onDragMove(rect);
				
				/**
				 * 如果是在删除区域,则拖拽的item的绘制颜色需要加个变色蒙版,系统Launcher中是变红
				 */
				if(inDeleteZone){
					mDrawBitmaptPaint = mTrashPaint;
				}else{
					mDrawBitmaptPaint = mPaint;
				}
			}
			
			//请求重新绘制拖拽区
			invalidate(mDragRect);
			
			/**
			 * 这里判断是否在边缘,如果是,则滑动到下一个屏幕
			 */
			
			if(x<NON_SCROLL_ZONE){
				//当当前状态从滑动区域滑到边缘的时候进行处理
				if(mScrollState == SCROLL_STATE_IN_ZONE){
					mScrollState = SCROLL_STATE_WAITING_OUTSIZE;
					scrollAction.setDirection(DIRECTION_LEFT);
					 //延时一定的时间,就是我们看到在边缘时,其停顿了一段时间再滑向了下一个屏幕
					postDelayed(scrollAction, SCROLL_DELAY); 
				}
				
			}else if (x > getWidth() - NON_SCROLL_ZONE){
				
				if(mScrollState == SCROLL_STATE_IN_ZONE){
					Log.w(TAG, "滑动到下一个屏幕");
					mScrollState = SCROLL_STATE_WAITING_OUTSIZE;
					scrollAction.setDirection(DIRECTION_RIGHT);
					postDelayed(scrollAction, SCROLL_DELAY);
				}
				
			}else{
				mScrollState = SCROLL_STATE_IN_ZONE;
			}
			
			break;
			
		case MotionEvent.ACTION_UP:
			removeCallbacks(scrollAction);
			//stopDrag();
			int rawX = (int)(mLastMotionX - mTouchOffsetX - mBitmapOffsetX);
			int rawY = (int)(mLastMotionY - mTouchOffsetY - mBitmapOffsetY);
			Rect rect = new Rect();
			rect.set(rawX, rawY, rawX+mDragBitmap.getWidth(), rawY+mDragBitmap.getHeight());
			
			stopDrag((int)x,(int)y, rect);
			
			break;

		default:
			break;
		}
		
		return true;
	}


 

上面的代码中在MOVE事件中进行了移动过程的处理。代码中,应该看到了,当item被拖到边缘20dip以内的位置,其执行的逻辑是:

  1. <SPAN style="FONT-SIZE: 13px">scrollAction.setDirection(DIRECTION_RIGHT);  
  2. postDelayed(scrollAction, SCROLL_DELAY);</SPAN>  
scrollAction.setDirection(DIRECTION_RIGHT);
postDelayed(scrollAction, SCROLL_DELAY);

其中,scollAction就是一个线程,由它来完成从一个屏幕滑动到另一个屏幕。这里需要注意的是:你所看到的滑动到下一个屏幕,其实,不是你在拖拽着item滑动到下一个屏幕的,而是,下一个屏幕滑动到当前手机窗口下的。所以,这个逻辑,不是应该由DragLayer来实现,而是由Workspace来实现。所以,Workspace实现了DragScroller接口,负责处理item滑动到下一个屏幕的逻辑。其中scrollAction对应的线程类代码如下:

  1. <SPAN style="FONT-SIZE: 13px">  class ScrollRunnable implements Runnable{  
  2.         private int mDirection;  
  3.         @Override  
  4.         public void run() {  
  5.             if(mDragScroller != null){  
  6.                 if(mDirection == DIRECTION_LEFT){  
  7.                     mDragScroller.scrollLeft();  
  8.                 }else{  
  9.                     mDragScroller.scrollRight();  
  10.                 }  
  11.             }  
  12.         }  
  13.           
  14.         public void setDirection(int dir){  
  15.             this.mDirection = dir;  
  16.         }  
  17.           
  18.     }</SPAN>  
	class ScrollRunnable implements Runnable{
		private int mDirection;
		@Override
		public void run() {
			if(mDragScroller != null){
				if(mDirection == DIRECTION_LEFT){
					mDragScroller.scrollLeft();
				}else{
					mDragScroller.scrollRight();
				}
			}
		}
		
		public void setDirection(int dir){
			this.mDirection = dir;
		}
		
	}

 

其根据在ACTION_MOVE中计算的位置和方向来判断其滑动到下一个屏幕的方向。上面说了,眼前的滑动行为是下一个屏幕显示到了当前手机的窗口下,而不是当前的item真的被你拖到下一个屏幕的。所以,我们需要找到DragScroller的实现者:Workspace。在Workspace中,其执行了scrollLeft和scrollRight的逻辑:

  1. <SPAN style="FONT-SIZE: 13px">  /** 
  2.      * {@inheritDoc} 
  3.      */  
  4.     @Override  
  5.     public void scrollLeft() {  
  6.         if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen > 0 && mScroller.isFinished()){  
  7.             snapToScreen(mCurrentScreen-1);  
  8.         }  
  9.     }  
  10.   
  11.     /** 
  12.      * {@inheritDoc} 
  13.      */  
  14.     @Override  
  15.     public void scrollRight() {  
  16.         if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen < getChildCount()-1 && mScroller.isFinished()){  
  17.             snapToScreen(mCurrentScreen+1);  
  18.         }  
  19.     }</SPAN>  
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void scrollLeft() {
		if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen > 0 && mScroller.isFinished()){
			snapToScreen(mCurrentScreen-1);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void scrollRight() {
		if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen < getChildCount()-1 && mScroller.isFinished()){
			snapToScreen(mCurrentScreen+1);
		}
	}

 

想必您还记得前面在Workspace分析的时候,其中的snapToScreen方法,不记得的话,可以到那里去寻找答案。好了,现在这个拖动的过程就这么实现了。那么,当我们停止拖拽释放了item,其又是如何被添加到新的位置或者被删除的呢?这个看上面ACTION_UP中的部分,调用了stopDrag方法。那么我们看看这个方法的逻辑。其实,这里很简单了,就是判断当前位置是否是删除区域,如果是执行删除逻辑,如果不是,则执行移动位置逻辑。代码如下:

  1. <SPAN style="FONT-SIZE: 13px">  public void stopDrag(int x, int y, Rect dragRect){  
  2.         if(mDragging){  
  3.               
  4.             mDragging = false;  
  5.               
  6.             if(mDragBitmap != null){  
  7.                 mDragBitmap.recycle();  
  8.             }  
  9.               
  10.             UorderCellLayout screen = mWorkspace.getCurrentCellLayout();  
  11.             Log.e(TAG, "当前屏幕是:"+mWorkspace.getCurrentScreen());  
  12.             Log.e(TAG, "被移动项所在的屏幕是"+mCellInfo.screen);  
  13.               
  14.             if(mDragListener != null){  
  15.                 boolean inDeleteZone = mDragListener.onDragStop(dragRect);  
  16.                 if(inDeleteZone){  
  17.                     //删除这个item   
  18.                     screen.removeView(mOriginator);  
  19.                     invalidate();  
  20.                     ItemInfo info = (ItemInfo)mOriginator.getTag();  
  21.                     UorderDBUtils.deleteItemFromDB(getContext(), info);  
  22.                     return;  
  23.                 }  
  24.             }  
  25.               
  26.             //将当前x,y所在的坐标转换为当前屏幕中的单元格   
  27.               
  28.             final int[] cellXY = new int[2];  
  29.             screen.pointToCellExact(x, y, cellXY);  
  30.             final CellInfo cellInfo = mCellInfo;  
  31.             cellInfo.cellX = cellXY[0];  
  32.             cellInfo.cellY = cellXY[1];  
  33.             cellInfo.cellHSpan = 1;  
  34.             cellInfo.cellVSpan = 1;  
  35.               
  36.             Log.e(TAG, "移动后的单元格:"+cellXY[0]+","+cellXY[1]);  
  37.               
  38.             //调用Workspace的方法,来移动当前的项   
  39.             mWorkspace.moveShortcutOnDesk(mOriginator, cellInfo);  
  40.               
  41.               
  42.             if(mOriginator != null){  
  43.                 mOriginator.setVisibility(View.VISIBLE);  
  44.             }  
  45.               
  46.         }  
  47.           
  48.         invalidate();         
  49.     }</SPAN>  
	public void stopDrag(int x, int y, Rect dragRect){
		if(mDragging){
			
			mDragging = false;
			
			if(mDragBitmap != null){
				mDragBitmap.recycle();
			}
			
			UorderCellLayout screen = mWorkspace.getCurrentCellLayout();
			Log.e(TAG, "当前屏幕是:"+mWorkspace.getCurrentScreen());
			Log.e(TAG, "被移动项所在的屏幕是"+mCellInfo.screen);
			
			if(mDragListener != null){
				boolean inDeleteZone = mDragListener.onDragStop(dragRect);
				if(inDeleteZone){
					//删除这个item
					screen.removeView(mOriginator);
					invalidate();
					ItemInfo info = (ItemInfo)mOriginator.getTag();
					UorderDBUtils.deleteItemFromDB(getContext(), info);
					return;
				}
			}
			
			//将当前x,y所在的坐标转换为当前屏幕中的单元格
			
			final int[] cellXY = new int[2];
			screen.pointToCellExact(x, y, cellXY);
			final CellInfo cellInfo = mCellInfo;
			cellInfo.cellX = cellXY[0];
			cellInfo.cellY = cellXY[1];
			cellInfo.cellHSpan = 1;
			cellInfo.cellVSpan = 1;
			
			Log.e(TAG, "移动后的单元格:"+cellXY[0]+","+cellXY[1]);
			
			//调用Workspace的方法,来移动当前的项
			mWorkspace.moveShortcutOnDesk(mOriginator, cellInfo);
			
			
			if(mOriginator != null){
				mOriginator.setVisibility(View.VISIBLE);
			}
			
		}
		
		invalidate();		
	}

 

上面的代码很简单,就是判断了其是否在删除区域内,如果不是调用mWorkspace.moveShortcutOnDesk(mOriginator, cellInfo);方法来完成位置的移动。那么,我们来看看这个方法,这个方法中的逻辑就是分两种情况,如果是在同一个屏幕中移动,则直接根据新的位置重新绘制就可以了,但是如果移动前和移动后不在同一个屏幕,则需要将原来的屏幕上的给删掉,在新的屏幕上添加。代码如下:

  1. <SPAN style="FONT-SIZE: 13px">  /** 
  2.      * 用户长拖拽一个item移动,当其停止的时候,就根据其最新的位置,将其显示在当前最新的位置 
  3.      * 同时需要更新数据库 
  4.      * @param v 
  5.      * @param cellInfo 
  6.      */  
  7.     public void moveShortcutOnDesk(View v, CellInfo cellInfo){  
  8.           
  9.         int screen = cellInfo.screen;  
  10.           
  11.           
  12.         /** 
  13.          * 这里需要分为两种情况来进行处理, 
  14.          * 1、如果在同一个CellLayout中移动,则很简单,直接设置新的位置,然后让该CellLayout重新绘制就行了 
  15.          * 2、如果移动到了另一个CellLayout中,则首先需要将View从原来的CellLayout中删除掉,然后再将其添加到 
  16.          * 当前屏幕的CellLayout中 
  17.          */  
  18.         if(screen != mCurrentScreen){  
  19.             UorderCellLayout lastScreen = getCellLayout(screen);  
  20.             lastScreen.removeView(v);  
  21.             this.addInScreen(v, mCurrentScreen, cellInfo.cellX, cellInfo.cellY, cellInfo.cellHSpan, cellInfo.cellVSpan, false);  
  22.           
  23.         }else{  
  24.               
  25.             UorderCellLayout group = getCurrentCellLayout();  
  26.             UorderCellLayout.LayoutParams lp = (UorderCellLayout.LayoutParams)v.getLayoutParams();  
  27.             lp.cellX = cellInfo.cellX;  
  28.             lp.cellY = cellInfo.cellY;  
  29.             lp.cellHSpan = cellInfo.cellHSpan;  
  30.             lp.cellVSpan = cellInfo.cellVSpan;  
  31.               
  32.             group.invalidate();           
  33.         }  
  34.           
  35.   
  36.   
  37.         /** 
  38.          * 同时需要将移动后的新位置同步到数据库 
  39.          */  
  40.         ItemInfo item = (ItemInfo)v.getTag();  
  41.         item.cellX = cellInfo.cellX;  
  42.         item.cellY = cellInfo.cellY;  
  43.         item.spanX = cellInfo.cellHSpan;  
  44.         item.spanY = cellInfo.cellVSpan;  
  45.         item.screen = mCurrentScreen;     
  46.           
  47.         UorderDBUtils.moveItemInDB(getContext(), item);  
  48.     }</SPAN>  
	/**
	 * 用户长拖拽一个item移动,当其停止的时候,就根据其最新的位置,将其显示在当前最新的位置
	 * 同时需要更新数据库
	 * @param v
	 * @param cellInfo
	 */
	public void moveShortcutOnDesk(View v, CellInfo cellInfo){
		
		int screen = cellInfo.screen;
		
		
		/**
		 * 这里需要分为两种情况来进行处理,
		 * 1、如果在同一个CellLayout中移动,则很简单,直接设置新的位置,然后让该CellLayout重新绘制就行了
		 * 2、如果移动到了另一个CellLayout中,则首先需要将View从原来的CellLayout中删除掉,然后再将其添加到
		 * 当前屏幕的CellLayout中
		 */
		if(screen != mCurrentScreen){
			UorderCellLayout lastScreen = getCellLayout(screen);
			lastScreen.removeView(v);
			this.addInScreen(v, mCurrentScreen, cellInfo.cellX, cellInfo.cellY, cellInfo.cellHSpan, cellInfo.cellVSpan, false);
		
		}else{
			
			UorderCellLayout group = getCurrentCellLayout();
			UorderCellLayout.LayoutParams lp = (UorderCellLayout.LayoutParams)v.getLayoutParams();
			lp.cellX = cellInfo.cellX;
			lp.cellY = cellInfo.cellY;
			lp.cellHSpan = cellInfo.cellHSpan;
			lp.cellVSpan = cellInfo.cellVSpan;
			
			group.invalidate();			
		}
		


		/**
		 * 同时需要将移动后的新位置同步到数据库
		 */
		ItemInfo item = (ItemInfo)v.getTag();
		item.cellX = cellInfo.cellX;
		item.cellY = cellInfo.cellY;
		item.spanX = cellInfo.cellHSpan;
		item.spanY = cellInfo.cellVSpan;
		item.screen = mCurrentScreen;	
		
		UorderDBUtils.moveItemInDB(getContext(), item);
	}

 

如果你对其中的一些关于拖拽的细节还不是很清楚,那么你可以看看前面两篇关于拖拽的文章,它们可以帮你很好的理解拖拽的细节性问题。

到这里,完整的item拖拽过程就介绍完毕了。我们的桌面也越来越接近系统Launcher了。但是,我们现在只支持向桌面添加Application和Shortcut,是只占据一个单元格的最简单的item了。还无法向桌面添加各式各样的Widget以及文件夹等功能。但是不要心急,什么事情都有个先后顺序,都是从简单到复杂的。

下一篇我们就要介绍如何让我们的桌面也支持各种Widget(小部件)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值