在项目开发中,经常有首页轮播展示的需求,通常我们使用ViewPager就能满足需求。但经常随着需求变动,样式或者动画的修改,这时ViewPager往往改起来有点复杂了,并且要循环滑动时是最麻烦的。今天在这里为大家推荐一个专门为Banner设计的组件SweetCircularView,并且耦合度相当低,一个类可以直接提取出来使用。
详细介绍一下这个组件强大的功能支持,弥补了ViewPager在作为Banner时的功能缺陷。天生支持以下两个重要的特性:
- 循环滑动
手势/定时自动循环滑动啦,使用BaseAdapter实现内部视图复用,减少内存消耗滑动卡顿等问题。
可配置属性:自定义滑动动画,手势快速滑动,滑动方向垂直或水平。 - Item缩进
缩进中心视图,并且展示左右视图,类似与PC网易云音乐首页Banner的样式。
当然除了以上两个属性意外,基本的Banner空间特性肯定是有的,比如弹性归位,惯性滑动,点击选中等等。附上组件源码:agility2/SweetCircularView,好用的话别忘了加颗闪亮的星星哦✨✨✨✨✨
下面来简单的介绍一下组件实现的基本原理,首先基本思路是给予Adapter的视图复用机制减少内存开销,然后重写onTouchEvent,onDispatchEvent,onInterceptTouchEvent,实现手势滑动相关逻辑,在滑动的时候将动画组件分离结偶,方便以后定义滑动动画,最后收尾的是视图滑动之后的停靠逻辑。
- Adapter的视图复用,先贴关键代码:
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);
}
}
}
// ...
}
}
在新设置或调用adapter.notifyDataSetChanged时触发requestLayout使视图重新布局,在重新布局时先去出对应已经被添加的子视图使用adapter.getView进行视图刷新或创建,流程与ListView.setAdapter相同。
- 重写onTouchEvent,onDispatchTouchEvent,onInterceptTouchEvent
这三个方法是手势滑动的关键,主要思想是:首先在dispatch中判断事件是否需要进行拦截,在通过intercept返回true进行拦截,使事件进入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());
// 禁止父视图中的触摸事件,使事件派发到当前视图中
// 处理ListView,ScrollView 嵌套的手势事件派发问题
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) {
// 当手指垂直或水平移动距离大于移动阀值时,确定为需要拦截处理
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:
// 返回是否进行拦截,拦截的事件进入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:
// .......
// 最终调用 move方法进行移动
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();
}
// 根据手指离开视图时的速度计算惯性距离
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;
}
进行真是移动的move方法,由于是视图复用机制,所以需要在滑动的同时去更新视图的信息,更新视图基于中心视图向左右或上下两边延伸。
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) {
// 右/下滑动,复用视图下标逐个-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:使视图展示内容与adapter中的数据下标进行绑定,形成循环
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);
}
// 已中心视图作为参考点,向左右/上下两个方向更新视图
updateAllItemView(getCurrentIndex());
// 根据参数对齐视图位置和更新大小
alignAllItemPosition();
}
}
组件本身将动画效果实现结偶,自定义动画可以使用AnimationAdapter很方便精准的控制,以上大概讲述了组件的核心原理,最后付上链式调用方式,看上去十分简洁,最终的效果就是文章开头截图的效果啦~~
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) // 点击视图选中(将点击的非中心视图移动到中心)
.setDurationOnInertial(1000) // 自动滑动到下一视图的动画时间
.setDurationOnPacking(500) // 惯性停靠动画时间
.setOverRatio(0.2f) // 手指滑动停止之后视图归位的越界系数(>20%为滑动到下一个视图)
.setInertialRatio(0.01f) // 惯性滑动速度
.setAutoCycle(true, true) // 自动滑动
.setIntervalOnAutoCycle(4000) // 自动滑动间隔
.setIndent(320, 220, 320, 220) // 设置中心视图参考与父视图的缩进边距(默认铺满父视图)
.setAnimationAdapter(new SimpleCircularAnimator().setRotation(20)) // 设置动画适配器
.setRecycleItemSize(gallery.getRecycleItemSize() + 2) // 设置可复用的视图个数
.setOrientation(LinearLayout.HORIZONTAL) // 设置滑动方向(垂直/水平)
.setSpaceBetweenItems(gallery.getSpaceBetweenItems() - 20) // 设置相邻视图之间的间隙
// .setIndicator(<T extends IIndicator> T); // 绑定滑动指示器
.setOnItemScrolledListener((v, dataIndex, offset) -> logout(TAG, "scrolled: [" + dataIndex + ", " + offset + "]"))
.setOnItemSelectedListener((v, dataIndex) -> logout(TAG, "selected: [" + dataIndex + "]"));
}
最后推荐Android快速开发的工具库,Github:agility2
agility2/CommonTools 基础工具类:压缩图片,渲染文字...
agility2/DynamicProxy 动态代理
agility2/FieldUtils 反射
agility2/IOUtils 流处理:对接,写文件...