需求
- 垂直方向的ViewPager+Fragment,ViewPager不能滑动,每个Fragment滑动到顶部或者底部,有类似IOS弹性效果,滑动一段距离可以跳到上一个或下一个Fragment。
- 实际效果:见懒人畅听标签列表页效果。
代码
public class PullSlideParent extends ViewGroup {
private final ViewDragHelper mDragHelper
private final ViewGroup mHeader
private final ViewGroup mFooter
private int mHeaderMeasuredHeight
private int mFooterMeasuredHeight
private View mDragContentView
private float lastY
private float deltaY
private OnPullListener onPullListener
private boolean up = false
private boolean down = false
private boolean canUp = true
private boolean canDown = true
private static final float SWIPE_BACK_FACTOR = 0.9f
private float swipeBackFraction
public PullSlideParent(Context context) {
this(context, null)
}
public PullSlideParent(Context context, AttributeSet attrs) {
this(context, attrs, 0)
}
public PullSlideParent(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr)
mDragHelper = ViewDragHelper.create(this, new PullSlideParent.DragHelperCallback())
mHeader = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.pull_side_header, this, false)
mFooter = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.pull_side_footer, this, false)
addView(mHeader)
addView(mFooter)
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
int childCount = getChildCount()
if (childCount != 3) {
throw new IllegalStateException("PullSlideParent must contains only three direct child.")
}
measureChildren(widthMeasureSpec, heightMeasureSpec)
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() != 3) {
return
}
mHeaderMeasuredHeight = mHeader.getMeasuredHeight()
mFooterMeasuredHeight = mFooter.getMeasuredHeight()
mHeader.layout(0, -mHeaderMeasuredHeight, getMeasuredWidth(), 0)
mFooter.layout(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight() + mFooterMeasuredHeight)
int childCount = getChildCount()
for (int i = 0
View child = getChildAt(i)
if (child instanceof RecyclerView) {
mDragContentView = child
mDragContentView.layout(0, 0, mDragContentView.getMeasuredWidth(), mDragContentView.getMeasuredHeight())
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = ev.getY()
deltaY = 0
mDragHelper.shouldInterceptTouchEvent(ev)
break
case MotionEvent.ACTION_MOVE:
deltaY = ev.getY() - lastY
lastY = ev.getY()
break
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mDragHelper.shouldInterceptTouchEvent(ev)
deltaY = 0
break
default:
}
if (canDown && deltaY > 0 && !mDragContentView.canScrollVertically(-1)) {
return mDragHelper.shouldInterceptTouchEvent(ev)
}
if (canUp && deltaY < 0 && !mDragContentView.canScrollVertically(1)) {
return mDragHelper.shouldInterceptTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev)
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event)
return true
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
public void setCanUp(boolean canUp) {
this.canUp = canUp
}
public void setCanDown(boolean canDown) {
this.canDown = canDown
}
public void setOnPullListener(PullSlideParent.OnPullListener onPullListener) {
this.onPullListener = onPullListener
}
public interface OnPullListener {
void onPullUp()
void onPullDown()
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return child == mDragContentView
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
return Math.max(mHeaderMeasuredHeight, mFooterMeasuredHeight)
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
if (top > 0) {
swipeBackFraction = 1.0f * top / mHeaderMeasuredHeight
mHeader.setTranslationY(top)
} else if (top < 0) {
swipeBackFraction = -1.0f * top / mFooterMeasuredHeight
mFooter.setTranslationY(top)
} else {
swipeBackFraction = 0f
down = false
up = false
mHeader.setTranslationY(top)
mFooter.setTranslationY(top)
}
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if (top > 0) {
down = true
return Math.min(mHeaderMeasuredHeight, top)
} else if (top < 0) {
up = true
return Math.max(top, -mFooterMeasuredHeight)
}
return 0
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (onPullListener != null && swipeBackFraction > SWIPE_BACK_FACTOR) {
if (up) {
onPullListener.onPullUp()
} else if (down) {
onPullListener.onPullDown()
}
}
mDragHelper.smoothSlideViewTo(releasedChild, 0, 0)
ViewCompat.postInvalidateOnAnimation(PullSlideParent.this)
}
}
}