要实现这种效果,使用CoordinatorLayout,AppBarLayout,RecyclerView很容易就能完成。由于当前开发的工程由于一些原因不能使用AndroidDesignSupport包。只能自己解决滑动嵌套问题,实现了这个功能,顺便学习了下NestedScrollingParent,NestedScrollingChild的用法。如果想结合代码看文章源码链接在底部。
简单的说下NestedScrollingParent,NestedScrollingChild就是两个接口,在新的android.support.v4包中,两个接口定义了一些操作,然后通过NestedScrollingChildHelper把两者联系起来。
在子View中要联动滚动之前需要调用startNestedScroll(),这个时候NestedScrollingChildHelper中就会向父View寻找实现了NestedScrollingParent的View,并把他保存起来。当滑动事件传递到子View的时候,子View一般要去询问父View是否要滚动,然后方法返回后子View在决定自身是否要滚动。
子View可以通过传给helper的consumed,offsetInWindow数组得到父View消耗的距离,与自身在屏幕的偏移距离。这样子View根据父View的返回在决定自己是否滚动。大概调用关系如下图。
动画中的View布局关系如下图,先滚动1(也有可能不滚动),在滚动2.
这里是NestedScrollParentLayout的简单实现,调用scrollBy方法滚动
public class NestedScrollParentLayout extends RelativeLayout implements NestedScrollingParent {
private NestedScrollingParentHelper mParentHelper;
private int mTitleHeight;
private View mTitleTabView;
public NestedScrollParentLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedScrollParentLayout(Context context) {
super(context);
init();
}
private void init() {
mParentHelper = new NestedScrollingParentHelper(this);
}
//获取子view
@Override
protected void onFinishInflate() {
mTitleTabView = this.findViewById(R.id.title_input_container);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTitleHeight = mTitleTabView.getMeasuredHeight();
super.onMeasure(widthMeasureSpec, heightMeasureSpec + mTitleHeight);
}
//接口实现--------------------------------------------------
//在此可以判断参数target是哪一个子view以及滚动的方向,然后决定是否要配合其进行嵌套滚动
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if (target instanceof NestedListView) {
return true;
}
return false;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
}
//先于child滚动
//前3个为输入参数,最后一个是输出参数
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (dy > 0) {//手势向上滑动
if (getScrollY() < mTitleHeight) {
scrollBy(0, dy);//滚动
consumed[1] = dy;//告诉child我消费了多少
}
} else if (dy < 0) {//手势向下滑动
if (getScrollY() > 0) {
scrollBy(0, dy);//滚动
consumed[1] = dy;//告诉child我消费了多少
}
}
}
//后于child滚动
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
//返回值:是否消费了fling
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
//返回值:是否消费了fling
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
// if (!consumed) {
// return true;
// }
return false;
}
@Override
public int getNestedScrollAxes() {
return mParentHelper.getNestedScrollAxes();
}
//scrollBy内部会调用scrollTo
//限制滚动范围
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTitleHeight) {
y = mTitleHeight;
}
super.scrollTo(x, y);
}
}
这里是NestedListView的实现,onTouchEvent部分代码主要来自RecyclerView的onTouchEvent中
public class NestedListView extends ListView implements NestedScrollingChild {
private NestedScrollingChildHelper mChildHelper;
private int[] mNestedOffsets = new int[2];
private int[] mScrollConsumed = new int[2];
private int[] mScrollOffset = new int[2];
private int mScrollPointerId;
private int mLastTouchX;
private int mLastTouchY;
private final static String TAG = "NestedListView";
public NestedListView(Context context) {
super(context);
init();
}
public NestedListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public NestedListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public NestedListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void init() {
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private boolean isFirst = true;//DOWN事件没执行暂时
private int lastDy;//暂时解决第一次MOVE与后序符号相反,导致的抖动问题
@Override
public boolean onTouchEvent(MotionEvent e) {
//下述代码主要复制于RecyclerView
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
//不知道为啥没有执行
resetScroll(e);
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
}
break;
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (isFirst) {//暂时解决第次dy与后序符号相反导致的闪动问题
Log.i("pyt", "FIRST");
isFirst = false;
resetScroll(e);
return true;
}
if (!isSignOpposite(lastDy, dy)) {//解决手机触摸在屏幕上不松开一直抖动的问题
lastDy = dy;
Log.i("pyt", "move lastY" + mLastTouchY + ",y=" + y + ",dy=" + dy);
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
}
}
break;
case MotionEvent.ACTION_UP: {
stopNestedScroll();
// resetTouch();
isFirst = true;
}
break;
case MotionEvent.ACTION_CANCEL: {
// cancelTouch();
}
break;
}
super.onTouchEvent(e);
return true;
}
private void resetScroll(MotionEvent e) {
lastDy = 0;
mNestedOffsets[0] = mNestedOffsets[1] = 0;
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mLastTouchX = (int) (e.getX() + 0.5f);
mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
startNestedScroll(nestedScrollAxis);
}
/**
* 判断符号是否相反,可以改成异或
*
* @param f
* @param s
* @return
*/
private boolean isSignOpposite(int f, int s) {
if (f > 0 && s < 0 || f < 0 && s > 0) {
return true;
}
return false;
}
//以下为接口实现--------------------------------------------------
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}