SweetCircularView循環(huán)滑動組件

在項(xiàng)目開發(fā)中葱绒,經(jīng)常有首頁輪播展示的需求谬以,通常我們使用ViewPager就能滿足需求凄诞。但經(jīng)常隨著需求變動圆雁,樣式或者動畫的修改,這時ViewPager往往改起來有點(diǎn)復(fù)雜了帆谍,并且要循環(huán)滑動時是最麻煩的伪朽。今天在這里為大家推薦一個專門為Banner設(shè)計的組件SweetCircularView,并且耦合度相當(dāng)?shù)脱打粋€類可以直接提取出來使用烈涮。

詳細(xì)介紹一下這個組件強(qiáng)大的功能支持,彌補(bǔ)了ViewPager在作為Banner時的功能缺陷患雇。天生支持以下兩個重要的特性:

  • 循環(huán)滑動
    手勢/定時自動循環(huán)滑動啦,使用BaseAdapter實(shí)現(xiàn)內(nèi)部視圖復(fù)用宇挫,減少內(nèi)存消耗滑動卡頓等問題苛吱。
    可配置屬性:自定義滑動動畫,手勢快速滑動器瘪,滑動方向垂直或水平翠储。
  • Item縮進(jìn)
    縮進(jìn)中心視圖,并且展示左右視圖橡疼,類似與PC網(wǎng)易云音樂首頁Banner的樣式援所。
    image.png

    當(dāng)然除了以上兩個屬性意外,基本的Banner空間特性肯定是有的欣除,比如彈性歸位住拭,慣性滑動,點(diǎn)擊選中等等。附上組件源碼:agility2/SweetCircularView滔岳,好用的話別忘了加顆閃亮的星星哦?????

下面來簡單的介紹一下組件實(shí)現(xiàn)的基本原理杠娱,首先基本思路是給予Adapter的視圖復(fù)用機(jī)制減少內(nèi)存開銷,然后重寫onTouchEvent谱煤,onDispatchEvent摊求,onInterceptTouchEvent,實(shí)現(xiàn)手勢滑動相關(guān)邏輯刘离,在滑動的時候?qū)赢嫿M件分離結(jié)偶室叉,方便以后定義滑動動畫,最后收尾的是視圖滑動之后的土蛱瑁靠邏輯茧痕。

  • Adapter的視圖復(fù)用,先貼關(guān)鍵代碼:
    public SweetCircularView setAdapter(BaseAdapter cycleAdapter) {
        if (adapter != null) {
            adapter.unregisterDataSetObserver(dataSetObserver);
        }
        if (cycleAdapter != null) {
            dataSetObserver = new AdapterDataSetObserver();
            cycleAdapter.registerDataSetObserver(dataSetObserver);
        }
        adapter = cycleAdapter;
        if (null != adapter) {
            adapter.notifyDataSetChanged();
        }
        return this;
    }

        void updateView() {
            if (adapter != null && dataIndex >= 0 && dataIndex < adapter.getCount() && state == NONE) {
                state = USING;
                View convertView = adapter.getView(dataIndex, view, SweetCircularView.this);
                if (convertView == view) {
                    // nothing to do
                } else {
                    // remove old view
                    removeView();
                    // add new view
                    if (convertView != null) {
                        if (convertView.getParent() != SweetCircularView.this) {
                            addView(convertView);
                        }
                    }
                }
                //  ...
            }
        }

在新設(shè)置或調(diào)用adapter.notifyDataSetChanged時觸發(fā)requestLayout使視圖重新布局疲憋,在重新布局時先去出對應(yīng)已經(jīng)被添加的子視圖使用adapter.getView進(jìn)行視圖刷新或創(chuàng)建凿渊,流程與ListView.setAdapter相同。

  • 重寫onTouchEvent缚柳,onDispatchTouchEvent埃脏,onInterceptTouchEvent
    這三個方法是手勢滑動的關(guān)鍵,主要思想是:首先在dispatch中判斷事件是否需要進(jìn)行攔截秋忙,在通過intercept返回true進(jìn)行攔截彩掐,使事件進(jìn)入onTouch完成移動。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
        boolean superState = super.dispatchTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                needIntercept = false;
                lastPoint.set(event.getX(), event.getY());
                // 禁止父視圖中的觸摸事件灰追,使事件派發(fā)到當(dāng)前視圖中
                // 處理ListView堵幽,ScrollView 嵌套的手勢事件派發(fā)問題
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;// can not return superState.
            case MotionEvent.ACTION_MOVE:
                float absXDiff = Math.abs(event.getX() - lastPoint.x);
                float absYDiff = Math.abs(event.getY() - lastPoint.y);
                if (orientation == LinearLayout.HORIZONTAL) {
                    if (absXDiff > absYDiff && absXDiff > MOVE_SLOP) {
                        // 當(dāng)手指垂直或水平移動距離大于移動閥值時,確定為需要攔截處理
                        needIntercept = true;
                    } else if (absYDiff > absXDiff && absYDiff > MOVE_SLOP) {
                        // restore touch event in parent
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                } else if (orientation == LinearLayout.VERTICAL) {
                    // 垂直滑動模式下 使用Y值
                    if (absYDiff > absXDiff && absYDiff > MOVE_SLOP) {
                        needIntercept = true;
                    } else if (absXDiff > absYDiff && absXDiff > MOVE_SLOP) {
                        // restore touch event in parent
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }
                // pause auto switch
                interceptAutoCycle();
                return superState;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // ......
        }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean superState = super.onInterceptTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                // 返回是否進(jìn)行攔截弹澎,攔截的事件進(jìn)入onTouchEvent 
                return needIntercept;
                // ......  其它 case 不攔截
        }
}
@Override
public boolean onTouchEvent(MotionEvent event) {
        boolean superState = super.onTouchEvent(event);
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
               // ......
            case MotionEvent.ACTION_MOVE:
                // .......
                // 最終調(diào)用 move方法進(jìn)行移動
                    if (orientation == LinearLayout.HORIZONTAL && absXDiff > absYDiff) {
                        move((int) -xDiff);
                    } else if (orientation == LinearLayout.VERTICAL && absYDiff > absXDiff) {
                        move((int) -yDiff);
                    }
                }
               // ......
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (isMoving) {
                    // 使用VelocityTracker獲取手指離開時的滑動速度
                    if (LinearLayout.HORIZONTAL == orientation) {
                        offset = getScrollX();
                        velocity = velocityTracker.getXVelocity();
                    } else {
                        offset = getScrollY();
                        velocity = velocityTracker.getYVelocity();
                    }
                    // 根據(jù)手指離開視圖時的速度計算慣性距離
                    int inertialDis = -(int) (velocity * durationOnInertial * inertialRatio);
                    if (Math.abs(inertialDis) + Math.abs(offset) <= maxOffset) {
                        inertialDis = 0;
                    }
                    // 開始自動滑動慣性距離
                    autoMove(inertialDis, durationOnInertial, new Runnable() {
                        @Override
                        public void run() {
                            // 自動滑動(慣性)之后推酉拢靠
                            autoPacking();
                        }
                    });
                }
                velocityTracker.clear();
                break;
            default:
                return superState;
        }
        return true;
}

進(jìn)行真是移動的move方法,由于是視圖復(fù)用機(jī)制苦蒿,所以需要在滑動的同時去更新視圖的信息殴胧,更新視圖基于中心視圖向左右或上下兩邊延伸。

protected final void move(final int offset) {
        isMoving = true;
        int scrolled, maxOffset;
        if (orientation == LinearLayout.VERTICAL) {
            scrollBy(0, offset);
            scrolled = getScrollY();
            maxOffset = getItemHeight() + spaceBetweenItems;
        } else { // HORIZONTAL
            scrollBy(offset, 0);
            scrolled = getScrollX();
            maxOffset = getItemWidth() + spaceBetweenItems;
        }
        notifyOnItemScrolled(offset);
        final int overOffset = Math.abs(scrolled) - maxOffset;
        if (overOffset >= 0) {
            final int size = getRecycleItemSize();
            ItemWrapper item;
            if (scrolled > 0) {
                // 右/下滑動佩迟,復(fù)用視圖下標(biāo)逐個-1
                for (int i = 0; i < size; i++) {
                    item = findItem(i);
                    item.itemIndex -= 1;
                }
            } else if (scrolled < 0) {
                for (int i = size - 1; i >= 0; i--) {
                    item = findItem(i);
                    item.itemIndex += 1;
                }
            }
            // cycleItemIndex:使視圖展示內(nèi)容與adapter中的數(shù)據(jù)下標(biāo)進(jìn)行綁定团滥,形成循環(huán)
            for (ItemWrapper tmp : items) {
                tmp.itemIndex = cycleItemIndex(tmp.itemIndex);
            }
            if (orientation == LinearLayout.VERTICAL) {
                scrollTo(0, scrolled > 0 ? overOffset : -overOffset);
            } else { // HORIZONTAL
                scrollTo(scrolled > 0 ? overOffset : -overOffset, 0);
            }
            // 已中心視圖作為參考點(diǎn),向左右/上下兩個方向更新視圖
            updateAllItemView(getCurrentIndex());
            // 根據(jù)參數(shù)對齊視圖位置和更新大小
            alignAllItemPosition();
        }
}

組件本身將動畫效果實(shí)現(xiàn)結(jié)偶报强,自定義動畫可以使用AnimationAdapter很方便精準(zhǔn)的控制灸姊,以上大概講述了組件的核心原理,最后付上鏈?zhǔn)秸{(diào)用方式秉溉,看上去十分簡潔力惯,最終的效果就是文章開頭截圖的效果啦~~

    private final BaseAdapter adapter = new ArrayAdapter() {
        @Override
        public View getView(int i, View view, ViewGroup parent) {
            if (null == view) {
                view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_item, null);
            }
            view.setOnClickListener(v -> logout(TAG, "onClick: [" + i + "]"));
            // TODO ......
            return view;
        }
    };

    private void initGallery(SweetCircularView circular) {
        circular.setAdapter(adapter)
                .setClick2Selected(false)                   // 點(diǎn)擊視圖選中(將點(diǎn)擊的非中心視圖移動到中心)
                .setDurationOnInertial(1000)                // 自動滑動到下一視圖的動畫時間
                .setDurationOnPacking(500)                  // 慣性屯胗靠動畫時間
                .setOverRatio(0.2f)                         // 手指滑動停止之后視圖歸位的越界系數(shù)(>20%為滑動到下一個視圖)
                .setInertialRatio(0.01f)                    // 慣性滑動速度
                .setAutoCycle(true, true)                   // 自動滑動
                .setIntervalOnAutoCycle(4000)               // 自動滑動間隔
                .setIndent(320, 220, 320, 220)              // 設(shè)置中心視圖參考與父視圖的縮進(jìn)邊距(默認(rèn)鋪滿父視圖)
                .setAnimationAdapter(new SimpleCircularAnimator().setRotation(20)) // 設(shè)置動畫適配器
                .setRecycleItemSize(gallery.getRecycleItemSize() + 2) // 設(shè)置可復(fù)用的視圖個數(shù)
                .setOrientation(LinearLayout.HORIZONTAL)    // 設(shè)置滑動方向(垂直/水平)
                .setSpaceBetweenItems(gallery.getSpaceBetweenItems() - 20) // 設(shè)置相鄰視圖之間的間隙
                // .setIndicator(<T extends IIndicator> T);    // 綁定滑動指示器
                .setOnItemScrolledListener((v, dataIndex, offset) -> logout(TAG, "scrolled: [" + dataIndex + ", " + offset + "]"))
                .setOnItemSelectedListener((v, dataIndex) -> logout(TAG, "selected: [" + dataIndex + "]"));
    }

最后推薦Android快速開發(fā)的工具庫,Github:agility2
agility2/CommonTools 基礎(chǔ)工具類:壓縮圖片夯膀,渲染文字...
agility2/DynamicProxy 動態(tài)代理
agility2/FieldUtils 反射
agility2/IOUtils 流處理:對接诗充,寫文件...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诱建,隨后出現(xiàn)的幾起案子蝴蜓,更是在濱河造成了極大的恐慌,老刑警劉巖俺猿,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茎匠,死亡現(xiàn)場離奇詭異,居然都是意外死亡押袍,警方通過查閱死者的電腦和手機(jī)诵冒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谊惭,“玉大人汽馋,你說我怎么就攤上這事∪” “怎么了豹芯?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驱敲。 經(jīng)常有香客問我铁蹈,道長,這世上最難降的妖魔是什么众眨? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任握牧,我火速辦了婚禮,結(jié)果婚禮上娩梨,老公的妹妹穿的比我還像新娘沿腰。我一直安慰自己,他們只是感情好狈定,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布颂龙。 她就那樣靜靜地躺著,像睡著了一般掸冤。 火紅的嫁衣襯著肌膚如雪厘托。 梳的紋絲不亂的頭發(fā)上友雳,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天稿湿,我揣著相機(jī)與錄音,去河邊找鬼押赊。 笑死饺藤,一個胖子當(dāng)著我的面吹牛包斑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涕俗,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼罗丰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了再姑?” 一聲冷哼從身側(cè)響起萌抵,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎元镀,沒想到半個月后绍填,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栖疑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年讨永,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遇革。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡卿闹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萝快,到底是詐尸還是另有隱情锻霎,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布杠巡,位于F島的核電站量窘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氢拥。R本人自食惡果不足惜蚌铜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嫩海。 院中可真熱鬧冬殃,春花似錦、人聲如沸叁怪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奕谭。三九已至涣觉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間血柳,已是汗流浹背官册。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留难捌,地道東北人膝宁。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓鸦难,卻偏偏與公主長得像,于是被迫代替她去往敵國和親员淫。 傳聞我的和親對象是個殘疾皇子合蔽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359