• 2010-12-17

    [Code] Android备忘3 - Tricks - [Code]

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/sgzxy-logs/89992098.html

    1、代替Timer的Trick   

          要定时更新UI首先会想到使用Timer+TimerTask+Handler,然而试验了半天发现TimerTask无法复用,就是每次使用前TimerTask需要重新创建。这点让人非常不爽,遂Google,从老外那觅得一简单高效的trick,原文地址:http://cart.kolix.de/?p=1438 。该trick的关键步骤是这样的:

    (1)首先创建一个Handler,并延迟将Runnable加入消息队列

    private Handler handler = new Handler();
    handler.postDelayed(runnable, 100);

    (2)然后创建如下格式的Runnable

    private Runnable runnable = new Runnable() {
       @Override
       public void run() {
          /* do what you need to do */
          foobar();
          /* and here comes the "trick" */
          handler.postDelayed(this, 100);
       }
    };

    (3)要清除队列中的Runnable时,调用handler.removeCallbacks(runnable)即可。

    -------------------------------------------n天后的分隔线-START-----------------------

    好了,经过实践,发现该trick有一个bug,对于postDelayed延时非常短的时候一般不会出现,但如果这个延时达到4、5秒的时候就会非常明显:

    虽然Runnable本身是运行在UI线程,并未开启新线程,按说不存在线程同步的问题,但postDelayed体现的非阻塞效果却证明系统背后一定用了另外一个线程来post(没有研究源码,如果谁发现我的想当然是错误的,请指出)。于是当post延时较长时就会出现bug:当你用handler.removeCallbacks来清除队列中的runnable时,队列很可能是空的,但这并不意味着没有runnable要post了,只是这个post还没到达而已。于是你虽然做了remove操作,runnable后面还是会继续运行,如果你后面再次调用handler.postDelay(runnable)的话,每次消息队列处理就会启动两个runnable,并随着你反复调用就会越来越多。

    解决办法:要彻底解决该bug需要两步操作:

    【1】在上述(2)的代码中,run函数在handler.postDelayed(this, 100);之前增加一行handler.removeCallbacks(this)。

    【2】增加一个shouldRun值来判断run函数是否该运行(也就是当你手动调用handler.removeCallbacks前将该值设为false),并在每次run函数开头check该值,如果为false则立刻退出。

    -------------------------------------------n天后的分隔线-END-----------------------

    2、细化Gallery对ItemSelectedChanged响应的trick

        用Gallery可以很方便地满足对horizontal listview这种控件的需求,而且它还有个很体贴的功能:setCallbackDuringFling(boolean)。因为一般用gallery都要考虑用户快速滑动的操作,这个时候selected item会频繁切换,如果你有个操作代价较高的callback去响应,则势必非常影响效率。而setCallbackDuringFling为false会启用Gallery中一个suppress item selected changed的功能,也就是当这个item selected changed为comfirmed的时候(也就是fling快速滑动停止后)才会调用callback。

         然而现在有这么个需求,为了让用户拥有滑动Gallery的爽快感,我需要有两种item selected changed事件:一种confirmed(setCallbackDuringFling为false);一种unconfirmed(setCallbackDuringFling为true )。我希望在前者的callback完成大数据量操作,在后者的callback完成迅速的外观上的改变(这样才爽)。

         这种事件的细分在public API上无法完成,于是我继承Gallery试图在protected级的方法上解决。然而研究试验了半天Gallery的源码,也没有找到合适的方法,而且还看到Gallery中一段诡异的源码(版本1.6):

        @Override
        void selectionChanged() {
            if (!mSuppressSelectionChanged) {
                super.selectionChanged();
            }
        }

        就是说Gallery竟然继承了父类一个私有方法?!完全不知道这个东西是怎么被成功编译的,反正我自己copy出来不能用。。这个就不管了,我再看啊看,终于找到了trick:getChildStaticTransformation这个protected的方法。

        这个方法从理解上来说可以想到,当Gallery需要呈现一个新的child view时,就需要调用这个方法。那么Gallery什么时候需要呈现新的child view?当然是Gallery滚动超过一个slot距离的时候,于是调用该方法的频率是大于等于unconfirmed item selected changed的频率的。于是后面不用说了,自己再暴露一个listener接口,然后继承上面的方法来callback(可以额外存个mSelectedPosition来判断保证不会冗余调用)。

        P.S. 经过实验后发现Gallery内部实现的对快速滑动时冗余selected item changed响应的suppress机制一点也不好用,经常会出现这种情况:你fling操作的前面稍微慢了一些,于是被识别成一个scroll+fling,于是前面的scroll会产生callback,于是这绝对不是我们想要的效果。最后我用上述的trick 1实现了selected item稳固了一定时间后才会产生callback的效果,效果很好。

    分享到:

    历史上的今天: