前面说了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就可以了。
[java] view plaincopy
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方法中显眼的位置,我们就可以一眼发现如下代码:
[java] view plaincopy
if(action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_STOPED){  
    return true;  
}

同时在ruturn的时候,其返回的是:mTouchState != TOUCH_STATE_STOPED;这个就是说,如果当前正在滑动,则返回true,交给onTouchEvent事件来处理滑动逻辑。
那么,我们就再来看看onTouchEvent中对ACTION_MOVE事件的处理:
[java] view plaincopy
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事件中的处理逻辑:
[java] view plaincopy
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处理的。那么我们就来看看这个好方法的逻辑:
[java] view plaincopy
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方法来看看其逻辑:
[java] view plaincopy
/**
 * 这个是当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();  
    }  
}

到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。
下一篇,将继续揭晓屏幕壁纸的添加,以及随着屏幕的移动,壁纸是如何跟着移动的。

转载于:https://www.cnblogs.com/tangchenglin/archive/2012/07/30/2615276.html

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

  1. 说说Android桌面(Launcher应用)背后的故事(一)——揭开她神秘的面纱

    最近由于项目需要自己定制一套管理系统,遂想到了Android的启动器,下来了源码,一编译到处是错,查了查原因,原来是引用了自家的成员,他们家开发的就是方便,想直接用就直接用.于是下载了个包,终于错误少 ...

  2. 说说Android桌面(Launcher应用)背后的故事(二)——应用程序的添加

    上篇中,讲到了第一个功能中需要获取应用程序的信息,然后添加到桌面.这里,先记录下如何获取Android中的应用程序信息. 一.调用系统快捷方式列表 [java] view plaincopy  Int ...

  3. 说说Android桌面(Launcher应用)背后的故事(九)——让我的桌面多姿多彩

    到这里我们的Launcher已经可以跑起来了,而且效果也如系统Launcher一般,但是,遗憾的是,我们的桌面上似乎都是一个摸样的Shortcut,而再看看系统桌面上,Search框,天气控件啊,各种 ...

  4. 说说Android桌面(Launcher应用)背后的故事(五)——桌面壁纸的添加

    上一篇中,我们了解了Workspace是如何处理多个CellLayout之间的滑动的.这篇,将记录如何将壁纸添加到桌面,以及Workspace如何处理滑动的时候,壁纸的滑动. 壁纸的添加,也是调用系统 ...

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

    有了前面的工作,基本上这个桌面就已经像模像样了,但是,和系统自带的Launcher相比,还差得很远.其中,系统Launcher的桌面上的item是可以任意穿越(移动)的.同时,在其穿越的过程中,你也可 ...

  6. 说说Android桌面(Launcher应用)背后的故事(大结局)——让Widget拥有Application同等的待遇

    前一篇中,演示了如何开发一个Widget以及如何开发一个WidgetHost应用.有了这个基础,我们就知道,要想在桌面上添加Widget,那么需要完成两件事情: 1.将桌面应用实现为一个WidgetH ...

  7. 说说Android桌面(Launcher应用)背后的故事(七)——又是一个附属品(可以转动的绚烂饼图)

    本来这一篇应该还是写Launcher中item拖拽的实现原理的,奈何,自从研究了Launcher,以前没有实现的,现在灵感全来了.这不,一个月前看到了著名记账软件随手记,看到android版中有一个炫 ...

  8. android 新闻编辑,超机访问:ZOL手机新闻编辑背后的故事

    在上周超机访问中,我们为大家介绍了手机频道评测刘宇航和手机的故事.通过上期节目,我们对这位评测编辑以及他的工作.使用手机的偏好有了一个全面的了解.本期节目我们再次请到了中关村在线的编辑,来讲讲他和手机 ...

  9. c语言控制安卓桌面,让你自己编写的Android的Launcher成为系统中第一个启动应用程序,也是唯一的Launcher...

    关注嵌入式安卓物联网行业及人才培养,每日更新,欢迎订阅及留言讨论~~~ 作者:倪键树,嵌入式安卓物联网讲师. 让你自己编写的Android的Launcher成为系统中第一个启动应用程序,也是唯一的La ...

最新文章

  1. 2021年大数据Spark(四十六):Structured Streaming Operations 操作
  2. 插件和代码两种方法搞定WordPress回复邮件通知
  3. 63、使用Timer类来实现定时任务
  4. python 安装虚拟环境virtualenv
  5. 【转】java字符串池和字符串堆内存分配
  6. 【scala】 scala xml 处理(⑨)
  7. 如何利用大数据做好数据分析
  8. 数据库中存储引擎 myISAM 与 innoDB 比较
  9. Win 10 蓝屏,出现DRIVER_POWER_STATE_FAILURE的解决方法
  10. adb冲突 傲软_手机投屏到电脑软件
  11. 智课雅思词汇---七、cur是什么意思
  12. html5怎么获取当前星期几,javascript如何获取今天是星期几?
  13. 手机APP从服务器获取列表和详情
  14. 【C++】Lambda 表达式详解
  15. 什么是C标签 为什么要用C 标签
  16. nginx 配置二级域名(阿里云)
  17. 苹果官网对xcode版本的要求
  18. 阮一峰小程序入门博客总结
  19. 微信小程序实现电子签名
  20. 贵在坚持,难在坚持,成在坚持。

热门文章

  1. Flask 参数简介
  2. vue canvas动效组件插件库制作
  3. WEB前端性能优化基本套路
  4. Mac系统下设置Maven环境
  5. [Python学习笔记][第八章Python异常处理结构与程序调试]
  6. Linq无聊练习系列7----Insert,delete,update,attach操作练习
  7. 修改mysql的最大连接数
  8. 磁盘管理,quota,RAID,LVM
  9. 【DFS】LeetCode 77. Combinations
  10. 程序员面试金典——17.4无判断max