说说Android桌面(Launcher应用)背后的故事(四)——揭秘Workspace

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



         前面说了Layout最主要的职责就是负责item的布局和空间的分配,这一节我们继续来看看CellLayout的父亲控件Workspace。手机的桌面是由几个屏幕的,你可以任意滑动的。这个布局就是一个Workspace。Launcher的Workspace主要的职责就是处理多个屏幕之间的滑动和壁纸的添加。

这里先提下,我们知道DragLayer包含了Workspace,Workspace又包含了几个CellLayout,那么我们首先应该知道,它们是如何各司其职而互不影响的。这个就是Android中事件的传递机制。我们知道,一个应用中,整个的布局是一个树状,那么当用户的一个Touch操作,比如点击事件,是如何从最外层的父亲控件传递到具体的子空间中去的。这个就要归功于View的onInterceptTouchEvent和onTouchEvent两个方法了。这两个方法的返回值决定了一个Touch事件的传递时序。onInterceptTouchEvent,顾名思义,就是起到一个拦截的作用。这两个方法是如何决定事件的传递的呢?

1、如果用户执行一个ACTION_DOWN事件,当前View的onInterceptTouchEvent返回true,则该事件和后续的ACTION_MOVE和,ACTION_UP将不再传递到View的子控件,而是直接交由该View的onTouchEvent来处理。

2、如果上面onInterceptTouchEvent返回false,则该事件和后续的ACTION_UP,ACTION_MOVE将也会透过onInterceptTouchEvent,继而传递到子控件的onInterceptTouchEvent。

3、如果该View的onInterceptTouchEvent返回false,事件传递到目标View,然后目标View的onTouchEvent又返回了false,那么事件将继续传递到目标View的上一级的onTouchEvent。如果目标View的onTouchEvent方法返回了true,说明此事件已被处理了。

了解了Android中Touch事件的传递机制,也就很容易弄清楚DragLayer,Workspace和CellLayout是如何做到各司其职的了。下面,就让我们一起打入Workspace的内部。

一、处理多个屏幕的滑动

我们知道Workspace是由几个CellLayout横向平铺组成的,那么简单点,就是实际的布局超出了手机的屏幕,那么就需要滑动,需要一个Scroller对象来计算每次滑动后的坐标以及处理滑动的状态。而且下了Launcher的源码,你会发现,报了很多红叉,其大部分是因为mScollX和mScollY错误,这是因为这两个属性是不公开的,子类无法直接使用,所以我们在实现的时候这部分注意,取mScollX和mScrollY的时候,用getScrollX和getScrollY,给mScrollX和mScrollY赋新值的时候,调用scrollBy()或者scrollTo函数来执行。

1、让几个CellLayout平铺,由于每个CellLayout的大小是手机的一屏幕大小,所以,这里让其横向平铺就很简单了,直接在onLayout中调用每个CellLayout的layout方法进行布局,按顺序布局的时候,只需要控制好每个CellLayout的left和right就可以了。

  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  2.     final int count = getChildCount();  
  3.     int childLeft = 0;  
  4.     //横向平铺CellLayout   
  5.     for(int i=0; i<count; i++){  
  6.         View child = getChildAt(i);  
  7.         final int width = child.getMeasuredWidth();  
  8.         final int height = child.getMeasuredHeight();  
  9.         if(child.getVisibility() != GONE){  
  10.             child.layout(childLeft, 0, childLeft+width, height);  
  11.             childLeft += width;  
  12.         }  
  13.     }  
  14. }  
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int count = getChildCount();
		int childLeft = 0;
		//横向平铺CellLayout
		for(int i=0; i<count; i++){
			View child = getChildAt(i);
			final int width = child.getMeasuredWidth();
			final int height = child.getMeasuredHeight();
			if(child.getVisibility() != GONE){
				child.layout(childLeft, 0, childLeft+width, height);
				childLeft += width;
			}
		}
	}


2、如何处理屏幕的滑动,屏幕滑动,莫非就需要在ACTION_MOVE事件中处理。我们在文章的开头介绍了Android的事件拦截机制,那么我们想,要让滑动事件让Workspace处理,而不会干扰到CellLayout,自然要在onInterceptTouchEvent中做一些处理了。那我们先从onInterceptTouchEvent方法入手,在onInterceptTouchEvent方法中显眼的位置,我们就可以一眼发现如下代码:

  1. if(action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_STOPED){  
  2.     return true;  
  3. }  
		if(action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_STOPED){
			return true;
		}
		


同时在ruturn的时候,其返回的是:mTouchState != TOUCH_STATE_STOPED;这个就是说,如果当前正在滑动,则返回true,交给onTouchEvent事件来处理滑动逻辑。

那么,我们就再来看看onTouchEvent中对ACTION_MOVE事件的处理:

  1. case MotionEvent.ACTION_MOVE:  
  2.     /** 
  3.      * 这里是处理滑动的地方 
  4.      * 注意,手指向右滑动的时候,屏幕是向左滑动的 
  5.      *  
  6.      */  
  7.     if(mTouchState == TOUCH_STATE_SCROLLING){  
  8.         final int xDiff = (int)(mLastMotionX - x);  
  9.         mLastMotionX = x; //注意更新mLastMotionX   
  10.           
  11.         /** 
  12.          * 向右滑动的时候,scrollX的值=上一次scrollX+xDiff 
  13.          */  
  14.         Log.v(TAG, "当前scrollX的大小:"+getScrollX());  
  15.         Log.v(TAG, "当前差值大小:"+xDiff);  
  16.         //下面判断是向左还是向右滑动   
  17.         if(xDiff < 0){  
  18.             //屏幕向左   
  19.             Log.v(TAG, "当前向左滑动");  
  20.             if(getScrollX()>0){  
  21.                 //取差值小的一个   
  22.                 /** 
  23.                  * xDiff是负数,所以 
  24.                  * 和向右滑动类似,当在第一个屏幕的时候,再向左滑动的时候,就会出现xDiff的绝对值大余scrollX的情况 
  25.                  * 这个时候scrollX的值接近于0,而xDiff的绝对值很可能大于0的。所以,这里做了如下的限制 
  26.                  */  
  27.                 int xDelta = Math.max(xDiff, -getScrollX());  
  28.                 scrollBy(xDelta, 0);  
  29.             }                     
  30.         }else if(xDiff > 0){  
  31.             //屏幕向右   
  32.             Log.v(TAG, "当前向右滑动");  
  33.             final int available = getChildAt(getChildCount()-1).getRight();  
  34.             Log.v(TAG, "当前可以滑动的最右边:"+available);  
  35.               
  36.             final int availableSroll = available-getScrollX()-getWidth();  
  37.             Log.v(TAG, "当前最大可以滑动的距离:"+availableSroll);  
  38.             if(availableSroll > 0){  
  39.                 /** 
  40.                  * 注意: 
  41.                  *  
  42.                  * 当滑动倒数第二个屏幕的时候,就有可能出现xDiff>availableScoll的情况 
  43.                  * 因为scrollX最大为最后一个屏幕的最左边 
  44.                  * available-getWidth就是scrollX的最大取值范围M 
  45.                  * 所以,availableSroll=M-当前已经滑动的距离(scrollX); 
  46.                  * 这样当在最后一个屏幕的时候,再向右就不能滑动了 
  47.                  */  
  48.                 scrollBy(Math.min(availableSroll, xDiff), 0);  
  49.             }  
  50.         }  
  51.     }  
  52.     break;  
		case MotionEvent.ACTION_MOVE:
			/**
			 * 这里是处理滑动的地方
			 * 注意,手指向右滑动的时候,屏幕是向左滑动的
			 * 
			 */
			if(mTouchState == TOUCH_STATE_SCROLLING){
				final int xDiff = (int)(mLastMotionX - x);
				mLastMotionX = x; //注意更新mLastMotionX
				
				/**
				 * 向右滑动的时候,scrollX的值=上一次scrollX+xDiff
				 */
				Log.v(TAG, "当前scrollX的大小:"+getScrollX());
				Log.v(TAG, "当前差值大小:"+xDiff);
				//下面判断是向左还是向右滑动
				if(xDiff < 0){
					//屏幕向左
					Log.v(TAG, "当前向左滑动");
					if(getScrollX()>0){
						//取差值小的一个
						/**
						 * xDiff是负数,所以
						 * 和向右滑动类似,当在第一个屏幕的时候,再向左滑动的时候,就会出现xDiff的绝对值大余scrollX的情况
						 * 这个时候scrollX的值接近于0,而xDiff的绝对值很可能大于0的。所以,这里做了如下的限制
						 */
						int xDelta = Math.max(xDiff, -getScrollX());
						scrollBy(xDelta, 0);
					}					
				}else if(xDiff > 0){
					//屏幕向右
					Log.v(TAG, "当前向右滑动");
					final int available = getChildAt(getChildCount()-1).getRight();
					Log.v(TAG, "当前可以滑动的最右边:"+available);
					
					final int availableSroll = available-getScrollX()-getWidth();
					Log.v(TAG, "当前最大可以滑动的距离:"+availableSroll);
					if(availableSroll > 0){
						/**
						 * 注意:
						 * 
						 * 当滑动倒数第二个屏幕的时候,就有可能出现xDiff>availableScoll的情况
						 * 因为scrollX最大为最后一个屏幕的最左边
						 * available-getWidth就是scrollX的最大取值范围M
						 * 所以,availableSroll=M-当前已经滑动的距离(scrollX);
						 * 这样当在最后一个屏幕的时候,再向右就不能滑动了
						 */
						scrollBy(Math.min(availableSroll, xDiff), 0);
					}
				}
			}
			break;


在这里,根据新的坐标位置,就算是向左还是向右滑动。同时处理滑动操作。那么,当我们停下的时候,它又是怎么做的呢?看ACTION_UP事件中的处理逻辑:

  1. if(mTouchState == TOUCH_STATE_SCROLLING){  
  2.     final VelocityTracker tracker = mVelocityTracker;  
  3.     tracker.computeCurrentVelocity(1000); //使用pix/s为单位   
  4.     int velX = (int)tracker.getXVelocity();  
  5.       
  6.     Log.v(TAG, "当前滑动的速度:"+velX);  
  7.       
  8.     if(velX > SNAP_VELOCITY && mCurrentScreen > 0){  
  9.         //向左   
  10.         snapToScreen(mCurrentScreen-1);  
  11.     }else if(velX < -SNAP_VELOCITY && mCurrentScreen < getChildCount()-1){  
  12.         //向右   
  13.         snapToScreen(mCurrentScreen+1);  
  14.     }else{  
  15.         //否则,看哪个屏幕显示的部分更多,就滑动到哪个屏幕   
  16.         final int screenWidth = getWidth();  
  17.           
  18.         //分析这里为什么可以这么算   
  19.         final int whichScreen = (getScrollX()+screenWidth/2)/screenWidth;  
  20.         /** 
  21.          * 其实很简单,就是以当前屏幕为基准,如果scrollX超出了一半,就滑倒下一个屏幕 
  22.          * 如果没有超过一半就停留在该屏幕 
  23.          * 所以,getScrollX()+screenWidth/2/screenWidth的思想就是 
  24.          * 如果scollX超过了屏幕的一半,再加上个半个屏幕的大小,在除以整个屏幕的大小就是下一屏了 
  25.          * 否则,就还是scrollX所在的屏幕 
  26.          */  
  27.         Log.w(TAG, "当前srollX的值:"+getScrollX());  
  28.           
  29.         snapToScreen(whichScreen);  
  30.     }  
  31. }  
			if(mTouchState == TOUCH_STATE_SCROLLING){
				final VelocityTracker tracker = mVelocityTracker;
				tracker.computeCurrentVelocity(1000); //使用pix/s为单位
				int velX = (int)tracker.getXVelocity();
				
				Log.v(TAG, "当前滑动的速度:"+velX);
				
				if(velX > SNAP_VELOCITY && mCurrentScreen > 0){
					//向左
					snapToScreen(mCurrentScreen-1);
				}else if(velX < -SNAP_VELOCITY && mCurrentScreen < getChildCount()-1){
					//向右
					snapToScreen(mCurrentScreen+1);
				}else{
					//否则,看哪个屏幕显示的部分更多,就滑动到哪个屏幕
					final int screenWidth = getWidth();
					
					//分析这里为什么可以这么算
					final int whichScreen = (getScrollX()+screenWidth/2)/screenWidth;
					/**
					 * 其实很简单,就是以当前屏幕为基准,如果scrollX超出了一半,就滑倒下一个屏幕
					 * 如果没有超过一半就停留在该屏幕
					 * 所以,getScrollX()+screenWidth/2/screenWidth的思想就是
					 * 如果scollX超过了屏幕的一半,再加上个半个屏幕的大小,在除以整个屏幕的大小就是下一屏了
					 * 否则,就还是scrollX所在的屏幕
					 */
					Log.w(TAG, "当前srollX的值:"+getScrollX());
					
					snapToScreen(whichScreen);
				}
			}


注意了,这里用VelocityTracker 计算了滑动的速度,因为,我们在滑动桌面的时候,应该注意到一个细节,当我们不是拖着桌面滑动,而是很快的滑动的时候,屏幕之间滑动到下一个屏幕的。这个就是通过VelocityTracker 计算滑动速度,如果滑动速度大于某个值,就直接滑动到下一个屏幕。具体的滑动到哪一个屏幕,是由方法snapToScreen处理的。那么我们就来看看这个好方法的逻辑:

  1. private void snapToScreen(int screen){  
  2.       
  3.     Log.w(TAG, "当前的屏幕:"+mCurrentScreen+"滑倒的屏幕是:"+screen);  
  4.       
  5.     enableChildrenCache();  
  6.     screen = Math.max(0, Math.min(screen, getChildCount()-1));  
  7.     boolean screenChange = screen != mCurrentScreen;  
  8.       
  9.     mNextScreen = screen;  
  10.       
  11.   
  12.     View focusedChild = getFocusedChild();  
  13.     if(focusedChild != null && screenChange && focusedChild == getChildAt(mCurrentScreen)){  
  14.         focusedChild.clearFocus(); //当屏幕切换时需要将当前屏幕的focus去掉   
  15.     }  
  16.       
  17.     final int newX = screen*getWidth();//当前需要滑到的屏幕的左边x坐标   
  18.     final int scrollX = getScrollX();//当前滑轮所在的位置   
  19.     final int delta = newX - scrollX; //偏差,〉0向右,<0向左   
  20.     mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta)*2);  
  21.       
  22.     Log.w(TAG, "startScroll yes");  
  23.       
  24.     invalidate();  
  25. }  
    private void snapToScreen(int screen){
    	
    	Log.w(TAG, "当前的屏幕:"+mCurrentScreen+"滑倒的屏幕是:"+screen);
    	
    	enableChildrenCache();
    	screen = Math.max(0, Math.min(screen, getChildCount()-1));
    	boolean screenChange = screen != mCurrentScreen;
    	
    	mNextScreen = screen;
    	

    	View focusedChild = getFocusedChild();
    	if(focusedChild != null && screenChange && focusedChild == getChildAt(mCurrentScreen)){
    		focusedChild.clearFocus(); //当屏幕切换时需要将当前屏幕的focus去掉
    	}
    	
    	final int newX = screen*getWidth();//当前需要滑到的屏幕的左边x坐标
    	final int scrollX = getScrollX();//当前滑轮所在的位置
    	final int delta = newX - scrollX; //偏差,〉0向右,<0向左
    	mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta)*2);
    	
    	Log.w(TAG, "startScroll yes");
    	
    	invalidate();
    }


在这个snapToScreen的方法中,逻辑很简单,主要就是调用了Scroller的startScroll方法,以当前滑动的位置和目标位置作为参数,启动滑动。但是,仅仅这样,这个方法起不到任何的效果,因为startScroll方法只是开始滑动,并不会不断的更新数据和处理滑动中的事情,这些事情是由computeScroll方法完成的。下面,我们再进入computeScroll方法来看看其逻辑:

  1. /** 
  2.  * 这个是当mScroller在滑动到某個屏幕的時候調用的 
  3.  * 我们调用ScrollToScreen这个方法,我们调用了startScroll()这个方法,但是,如果不重写computeScroll 
  4.  * 你会发现,{@link #snapToScreen(int)}没有效果的,原因就是 
  5.  * 在其自己滑动的时候,我们调用startScroll的时候,只是设定了我们希望滑倒的位置,但是其滑动过程中 
  6.  * 怎么滑动,还是在这个方法里。 
  7.  * 当mScroller.computeScrollOffset返回真,说明还没有滑倒目的地,就继续计算 
  8.  * 当返回假的时候,就说明滑动startScroll设定的终点了 
  9.  *  
  10.  * 奶奶的,想了半天才想明白,哎,杯具! 
  11.  * @see #snapToScreen(int) 
  12.  */  
  13. public void computeScroll(){  
  14.     if(mScroller.computeScrollOffset()){  
  15.         //mScroller.computeScrollOffset计算当前新的位置   
  16.         //返回true,说明scroll还没有停止   
  17.         int newX = mScroller.getCurrX();  
  18.         int newY = mScroller.getCurrY();  
  19.           
  20.         /** 
  21.          * 其实这里是不用scrollTo的,只需要设置mScrollX和mScrollY的值分别为 
  22.          * mScroller.getCurrX()和mScroller.getCurrY()就行了 
  23.          * 但是我们无法直接设置,所以用scrollTo完成 
  24.          */  
  25.         scrollTo(newX, newY);  
  26.           
  27.         /** 
  28.          * 这里需要调用postInvalidate,否则滑动的时候,你会发现 
  29.          * 界面会在两个屏幕的中间位置卡住 
  30.          */  
  31.            postInvalidate();  
  32.         Log.v(TAG, "computeScroll was called:the scrollX and scrollY are"+getScrollX()+","+getScrollY());  
  33.           
  34.     }else if(mNextScreen != INVALID_SCREEN){  
  35.         //scroll停止了,则滑动到合法且合适的屏幕   
  36.   
  37.         mCurrentScreen = Math.min(Math.max(mNextScreen, 0), getChildCount()-1);  
  38.         mNextScreen = INVALID_SCREEN;  //标记mNextScreen为无效状态   
  39.         //UorderLauncher.setScreen(mCurrentScreen);   
  40.         //清除子控件绘制缓存   
  41.         Log.v(TAG, "scroll is stop");  
  42.         clearChildrenCache();  
  43.     }  
  44. }  
	/**
	 * 这个是当mScroller在滑动到某個屏幕的時候調用的
	 * 我们调用ScrollToScreen这个方法,我们调用了startScroll()这个方法,但是,如果不重写computeScroll
	 * 你会发现,{@link #snapToScreen(int)}没有效果的,原因就是
	 * 在其自己滑动的时候,我们调用startScroll的时候,只是设定了我们希望滑倒的位置,但是其滑动过程中
	 * 怎么滑动,还是在这个方法里。
	 * 当mScroller.computeScrollOffset返回真,说明还没有滑倒目的地,就继续计算
	 * 当返回假的时候,就说明滑动startScroll设定的终点了
	 * 
	 * 奶奶的,想了半天才想明白,哎,杯具!
	 * @see #snapToScreen(int)
	 */
	public void computeScroll(){
		if(mScroller.computeScrollOffset()){
			//mScroller.computeScrollOffset计算当前新的位置
			//返回true,说明scroll还没有停止
			int newX = mScroller.getCurrX();
			int newY = mScroller.getCurrY();
			
			/**
			 * 其实这里是不用scrollTo的,只需要设置mScrollX和mScrollY的值分别为
			 * mScroller.getCurrX()和mScroller.getCurrY()就行了
			 * 但是我们无法直接设置,所以用scrollTo完成
			 */
			scrollTo(newX, newY);
			
			/**
			 * 这里需要调用postInvalidate,否则滑动的时候,你会发现
			 * 界面会在两个屏幕的中间位置卡住
			 */
            postInvalidate();
			Log.v(TAG, "computeScroll was called:the scrollX and scrollY are"+getScrollX()+","+getScrollY());
			
		}else if(mNextScreen != INVALID_SCREEN){
			//scroll停止了,则滑动到合法且合适的屏幕

			mCurrentScreen = Math.min(Math.max(mNextScreen, 0), getChildCount()-1);
			mNextScreen = INVALID_SCREEN;  //标记mNextScreen为无效状态
			//UorderLauncher.setScreen(mCurrentScreen);
			//清除子控件绘制缓存
			Log.v(TAG, "scroll is stop");
			clearChildrenCache();
		}
	}
	


到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。

下一篇,将继续揭晓屏幕壁纸的添加,以及随着屏幕的移动,壁纸是如何跟着移动的。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值