在項(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 流處理:對接诗充,寫文件...