Android Gallery EcoGallery 与 FancyCoverFlow 的详细分析

Gallery被标记为过时

一切的起源源于Gallery被标记为过时,所以才有本文的出现。

过时的原因,可以参考下面链接。

https://stackoverflow.com/questions/11868503/the-type-gallery-is-deprecated-whats-the-best-alternative

I suspect that Gallery was deprecated because it did not properly use convertView with its adapter. Which meant that it had to create a new view for every item which was a drain on performance.

Another option you have is to use the 3rd party created EcoGallery which Joseph Earl created to overcome the issue, this version does recycle its views properly.

关于RecycleBin

在分析Gallery控件时,需要先了解RecycleBin。RecycleBin是一个容器类,用来管理可回收的View。

不像Adapter/ArrayList等,RecycleBin有很多同名类,它作为内部类,存在于多个不同的类里面,有着不同的实现方式。(目前只发现下面两个类里面有RecycleBin内部类)

比如,AbsSpinner里面的RecycleBin:

    class RecycleBin {
        private final SparseArray<View> mScrapHeap = new SparseArray<View>();

        public void put(int position, View v) {
            mScrapHeap.put(position, v);
        }
        
        View get(int position) {
            // System.out.print("Looking for " + position);
            View result = mScrapHeap.get(position);
            if (result != null) {
                // System.out.println(" HIT");
                mScrapHeap.delete(position);
            } else {
                // System.out.println(" MISS");
            }
            return result;
        }

        void clear() {
            final SparseArray<View> scrapHeap = mScrapHeap;
            final int count = scrapHeap.size();
            for (int i = 0; i < count; i++) {
                final View view = scrapHeap.valueAt(i);
                if (view != null) {
                    removeDetachedView(view, true);
                }
            }
            scrapHeap.clear();
        }
    }

比如,AbsListView里面的RecycleBin:

class RecycleBin {
        private RecyclerListener mRecyclerListener;

        /**
         * The position of the first view stored in mActiveViews.
         */
        private int mFirstActivePosition;

        /**
         * Views that were on screen at the start of layout. This array is populated at the start of
         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
         * Views in mActiveViews represent a contiguous range of Views, with position of the first
         * view store in mFirstActivePosition.
         */
        private View[] mActiveViews = new View[0];

        /**
         * Unsorted views that can be used by the adapter as a convert view.
         */
        private ArrayList<View>[] mScrapViews;

        private int mViewTypeCount;

        private ArrayList<View> mCurrentScrap;

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }
            //noinspection unchecked
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }

        public void markChildrenDirty() {
            if (mViewTypeCount == 1) {
                final ArrayList<View> scrap = mCurrentScrap;
                final int scrapCount = scrap.size();
                for (int i = 0; i < scrapCount; i++) {
                    scrap.get(i).forceLayout();
                }
            } else {
                final int typeCount = mViewTypeCount;
                for (int i = 0; i < typeCount; i++) {
                    final ArrayList<View> scrap = mScrapViews[i];
                    final int scrapCount = scrap.size();
                    for (int j = 0; j < scrapCount; j++) {
                        scrap.get(j).forceLayout();
                    }
                }
            }
        }

        public boolean shouldRecycleViewType(int viewType) {
            return viewType >= 0;
        }
        // 代码太多,省略
    }

RecycleBin主要用于列表视图,比如GridView/ListView等,这些列表视图通常需要维护一个和可见窗口同样大小的View列表,当列表滑动时,不是不断创建新的View,而是重用滑出显示区域的那些Views。RecycleBin容器相当于回收区,用于管理滑出这个显示区域的View,表示可以重用的Views列表。(从命名字面意思也可以想到)

不同的列表控件,有不同的回收机制,所以RecycleBin的实现也不同。反过来,如果使用了不当的RecycleBin,会导致列表控件不能真正的利用到回收区的View,或者说,不当的RecycleBin导致列表控件无法从回收区“命中”重用的View,导致每次都创建新的View。(从这个角度看,RecycleBin更像是一个Cache)

所以,RecycleBin扮演着一个回收区和缓存的角色。

Gallery控件的缺陷

Gallery控件就是这么回事,RecycleBin回收区设置不合理,导致View的利用率很低。从代码看,回收区以position为key,所以只有往回滑动才能命中,往前滑动position是递增的,每次都需要创建新的View。

实际上,经过测试,Gallery控件是每次都创建新的View,所以连往回滑动都没有“命中”。通过代码查看,除了最致命的选择position作为回收区key外,在每次layout都对回收区进行了clear,是导致连往回滚动都无法命中的原因。反正,这应该是Gallery控件的一个Bug,重用View的机制没有生效。(测试Demo见附录)

Gallery控件另一个Bug是,层叠效果。

Gallery控件通过重写ViewGroup的getChildDrawingOrder来使得绘制从两边到中间,从而使列表项有层叠的效果。然而,重写的这个方法并不正确。重叠效果并不是越靠近中心的层级越高。如果你需要越靠近中心的层级越高,越靠近边界的层级越低,那么你需要重写这个方法。

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        int selectedIndex = mSelectedPosition - mFirstPosition;
        
        // Just to be safe
        if (selectedIndex < 0) return i;
        
        if (i == childCount - 1) {
            // Draw the selected child last
            return selectedIndex;
        } else if (i >= selectedIndex) {
            // Move the children after the selected child earlier one
            return i + 1;
        } else {
            // Keep the children before the selected child the same
            return i;
        }
    }

假设3是选中的,childCount是5,那么上面这个方法的绘制顺序是:1 2 4 5 3
而通常我们需要的是下面的绘制顺序:1 2 5 4 3

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        int selectedIndex = mSelectedPosition - mFirstPosition;
        if (i < selectedIndex) {
            return i;
        } else if (i >= selectedIndex) {
            return childCount - 1 - i + selectedIndex;
        } else {
            return i;
        }
    }

EcoGallery替代Gallery

Gallery已经被标记为过时,至于过时的原因,据说就是上面的View没有重用导致效率低下。虽然Gallery控件有效率问题,但它的展示效果还是比较独特的:

  1. 选中居中效果
  2. 触摸滑动切换效果
  3. 多页面Flip的效果(ViewPager只有3页可见)
  4. 页面层叠效果(间距与层级的控制)

想快速完成上述效果,使用Gallery还是比较合适的,相比官方建议的HorizontalScrollView and ViewPager,还需要进行一定量的修改。

EcoGallery的诞生就是因为Gallery被过时,但仍旧需要一个替代Gallery的控件。
EcoGallery并不神秘,它其实是Gallery源码的一个拷贝。作者将Gallery源码从Android原生Widget中抽离出来,并对上述View重用的缺陷进行了改进,实现了另一个RecycleBin。关于如何抽离Android原生控件,可以参考附录。

    class RecycleBin {
        private SparseArray<View> mScrapHeap = new SparseArray<View>();

        public void put(int position, View v) {
            mScrapHeap.put(position, v);
        }
 
        public void add(int position, View v) {
            mScrapHeap.put(mScrapHeap.size(), v);
        }
        public View get() {
            if (mScrapHeap.size() < 1) return null;
           
            View result = mScrapHeap.valueAt(0);
            int key = mScrapHeap.keyAt(0);
           
            if (result != null) {
                    mScrapHeap.delete(key);
            }
            return result;
        }
        // 剩下的是没用的
    }

这个RecycleBin回收区是不管position,只要回收区有View,就拿出来重用。可以认为,这里仅仅将RecycleBin当作是回收区,而没有作为缓存存在(缓存更接近于拿出来直接用,回收区重用会进行一次初始化)。

Gallery另一个层叠的问题,EcoGallery也存在。

FancyCoverFlow酷炫效果

如果想要下面3D Gallery的效果,那么就需要这个控件。FancyCoverFlow继承于Gallery,而Gallery已经过时,所以可以修改使其继承EcoGallery。

FancyCoverFlow.png

附录:
分析Demo https://github.com/jokinkuang/GalleryRecycle.git
EcoGallery https://github.com/falnatsheh/EcoGallery
FancyCoverFlow https://github.com/davidschreiber/FancyCoverFlow

关于抽离Android原生控件,搜索抽离Android原生控件的方法。
关于更深入的理解分析,搜索CGallery控件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容