Android5.0后Google为Android的滑动机制提供了NestedScrolling特性,可以使我们对嵌套View更简单事件拦截处理。本篇就对NestedScrolling实际应用做一些讲解。
NestedScrolling与传统事件分发机制作对比:
- 比如某个外部的ViewGroup拦截掉内部View的事件,那本次事件会被ViewGroup消费并且不会向下传递,如果子view也想处理只能等待下一次手指再按下。
- NestedScrolling可以很好的解决传统事件拦截的缺点,内部View在滚动的时候通过NestedScrollingChild将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。
而NestedScrolling也是support.v4提供的支持类,在老版本也可以很好的兼容。
描述:
通过NestedScrolling可以实现哪些效果?
- 菜单悬停效果
- 下拉刷新、上拉加载更多
- 拖拽回弹
实现嵌套分发主要通过以下两个接口:
NestedScrollingParent
NestedScrollingChild
要使用 NestedScrolling机制,父View 需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。RecyclerView已经默认实现了NestedScrollingChild,如果RecyclerView不满足你的业务需求,那需要去实现NestedScrollingChild,这里我只对NestedScrollingParent实现。
public class SpringBackLayout extends LinearLayout implements NestedScrollingParent {
/**
* 回弹View的背景颜色
*/
private static final int COLOR_SPRING_BACK_VIEW_BG = 0xFF000000;
/**
* 动画时长
*/
private static final int ANIM_DURATION = 260;
/**
* 头View
*/
private final View mHeaderView;
/**
* 尾View
*/
private final View mFooterView;
private static final int MAX_WIDTH = 300;
/**
* 子View
*/
private View mChildView;
/**
* 解决多点触控问题
*/
private boolean isRunAnim;
public SpringBackLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.HORIZONTAL);
mHeaderView = new View(context);
mHeaderView.setBackgroundColor(COLOR_SPRING_BACK_VIEW_BG);
mFooterView = new View(context);
mFooterView.setBackgroundColor(COLOR_SPRING_BACK_VIEW_BG);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mChildView = getChildAt(0);
LayoutParams layoutParams = new LayoutParams(MAX_WIDTH, LayoutParams.MATCH_PARENT);
addView(mHeaderView, 0, layoutParams);
addView(mFooterView, getChildCount(), layoutParams);
// 左移
scrollBy(MAX_WIDTH, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = mChildView.getLayoutParams();
params.width = getMeasuredWidth();
}
/**
* 必须要复写 onStartNestedScroll后调用
*/
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
}
/**
* 返回true代表处理本次事件
*/
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) {
if (target instanceof RecyclerView && !isRunAnim) {
return true;
}
return false;
}
/**
* 复位初始位置
*/
@Override
public void onStopNestedScroll(@NonNull View target) {
startAnimation(new ProgressAnimation());
}
/**
* 回弹动画
*/
private class ProgressAnimation extends Animation {
private float startProgress = 0;
private float endProgress = 1;
private ProgressAnimation() {
isRunAnim = true;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float progress = startProgress + ((endProgress - startProgress) * interpolatedTime);
scrollBy((int) ((MAX_WIDTH - getScrollX()) * progress), 0);
if (progress == 1) {
isRunAnim = false;
}
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
setDuration(ANIM_DURATION);
setInterpolator(new AccelerateInterpolator());
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
// 如果在自定义ViewGroup之上还有父View交给我来处理
getParent().requestDisallowInterceptTouchEvent(true);
// dx>0 往左滑动 dx<0往右滑动
boolean hiddenLeft = dx > 0 && getScrollX() < MAX_WIDTH && !target.canScrollHorizontally(-1);
boolean showLeft = dx < 0 && !target.canScrollHorizontally(-1);
boolean hiddenRight = dx < 0 && getScrollX() > MAX_WIDTH && !target.canScrollHorizontally(1);
boolean showRight = dx > 0 && !target.canScrollHorizontally(1);
if (hiddenLeft || showLeft || hiddenRight || showRight) {
scrollBy(dx / 2, 0);
consumed[0] = dx;
}
// 限制错位问题
if (dx > 0 && getScrollX() > MAX_WIDTH && !target.canScrollHorizontally(-1)) {
scrollTo(MAX_WIDTH, 0);
}
if (dx < 0 && getScrollX() < MAX_WIDTH && !target.canScrollHorizontally(1)) {
scrollTo(MAX_WIDTH, 0);
}
}
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
// 当RecyclerView在界面之内交给它自己惯性滑动
if (getScrollX() == MAX_WIDTH) {
return false;
}
return true;
}
@Override
public int getNestedScrollAxes() {
return 0;
}
/**
* 限制滑动 移动x轴不能超出最大范围
*/
@Override
public void scrollTo(int x, int y) {
if (x < 0) {
x = 0;
} else if (x > MAX_WIDTH * 2) {
x = MAX_WIDTH * 2;
}
super.scrollTo(x, y);
}
}
具体实现思路就是给RecyclerView添加头和尾,并控制滑动距离然后通过NestedScrolling进行分发,最后在手指离开界面开启还原动画。