小紅書效果

分析
- 思路一:可以通過自定義view來實作類似 (比較麻煩,參考這裡)
- 思路二:借助 NestScrolling 機制實作 (比較簡單,關于嵌套滑動概述參考這裡)
下面就用思路二來分析一下如何實作
- recyclerView 上滑時,如果手指沒有滑過 recyclerView 的頂部,那麼 recyclerView 自己消費滑動事件;否則開始嵌套滑動(整體上滑)。當手指擡起時候如果滑動距離大于 50dp ,通過慣性滑動滑到頂部,否則滑回到初始位置
- recyclerView 下滑時,如果recyclerView的第一個 item 不是完全可見,那麼 recyclerView 自己消費滑動事件;否則開始嵌套滑動(整體下滑)。當手指擡起時候如果滑動距離大于 50dp ,通過慣性滑動滑到底部,否則滑回到初始位置
實作
NestedRecyclerView(NestScrollChild)
public class NestedRecyclerView extends RecyclerView {
private float topPadding = Utils.dp2Px(50);
private float scrollSlop = Utils.dp2Px(50); //滑動超過這個距離松手後自動自動滑動到頂部
private float downY;
private float moveY;
private float deltaMoveY;
private float deltaDownMoveY;
private float lastMoveY;
public OverScroller mScroller;
private int firstCompletelyVisiblePosition;
public NestedRecyclerView(@NonNull Context context) {
this(context, null);
}
public NestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mScroller = new OverScroller(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("NestedRecyclerView", "ACTION_DOWN");
downY = event.getRawY();
lastMoveY = event.getRawY();
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("NestedRecyclerView", "ACTION_DOWN");
downY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
moveY = event.getRawY();
deltaMoveY = moveY - lastMoveY;
deltaDownMoveY = moveY - downY;
//向上滑動 手指滑動到 recyclerview 頂部或者以上 觸發嵌套滑動
//event.getY() <= 0 且 getTop() - ((View) getParent()).getScrollY() > topPadding
if (deltaMoveY < 0) {
if (event.getY() <= 0) {
if (getTop() - ((View) getParent()).getScrollY() > topPadding) {
setNestedScrollingEnabled(true);
startNestedScroll(SCROLL_AXIS_VERTICAL);
} else {
setNestedScrollingEnabled(false);
}
} else {
setNestedScrollingEnabled(false);
}
} else {
//向下滑動
//先向下滑動 recyclerview,等待 recyclerview 全部展現後觸發嵌套滑動
GridLayoutManager layoutManager = ((GridLayoutManager) getLayoutManager());
firstCompletelyVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstCompletelyVisiblePosition == 0 && ((View) getParent()).getScrollY() > 0) {
setNestedScrollingEnabled(true);
startNestedScroll(SCROLL_AXIS_VERTICAL);
} else {
setNestedScrollingEnabled(false);
}
}
lastMoveY = moveY;
break;
case MotionEvent.ACTION_UP:
int parentScrollY = ((View) getParent()).getScrollY();
int startY = parentScrollY;
int dy;
if (deltaDownMoveY < 0) {
//向上滑動切滑動距離超過 scrollSlop 滑動到頂部
//向上滑動切滑動距離不超過 scrollSlop 滑回到底部
if (parentScrollY >= scrollSlop) {
dy = (int) (getTop() - ((View) getParent()).getScrollY() - topPadding);
} else {
dy = -parentScrollY;
}
} else {
//向下滑動切滑動距離超過 scrollSlop 滑動到底部
//向下滑動切滑動距離不超過 scrollSlop 滑回到頂部
if (getTop() - ((View) getParent()).getScrollY() > scrollSlop + topPadding) {
dy = -parentScrollY;
} else {
dy = (int) (getTop() - ((View) getParent()).getScrollY() - topPadding);
}
}
mScroller.startScroll(0, startY, 0
, dy, 300);
((View) getParent()).invalidate();
break;
}
return super.onTouchEvent(event);
}
public OverScroller getScroller() {
return mScroller;
}
}
由于RecyclerView預設實作了NestedScrollingChild2,不需要我們自己實作NestedScrollingChild。
重寫RecyclerView的onTouchEvent方法,在ACTION_MOVE裡面合适的時候開啟/禁止彈性滑動,具體邏輯參考前面流程圖和代碼注釋。
在ACTION_UP的時候通過 Scroller 實作彈性滑動的效果,關鍵是計算出 dy,代碼注釋裡面寫的很清楚。
關于 Scroller 的使用參考這裡
LinearLayoutNestScrollParent(NestScrollParent)
public class LinearLayoutNestScrollParent extends LinearLayout implements NestedScrollingParent {
private NestedScrollingParentHelper mParentHelper;
private View imgView;
private NestedRecyclerView recyclerview;
public LinearLayoutNestScrollParent(Context context) {
this(context, null);
}
public LinearLayoutNestScrollParent(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mParentHelper = new NestedScrollingParentHelper(this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
imgView = findViewById(R.id.img);
recyclerview = findViewById(R.id.rv_photos);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int imgHeight = imgView.getMeasuredHeight();
//重新測量高度
int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() + imgHeight, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if (target instanceof RecyclerView) {
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 滾動
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
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) {
return false;
}
@Override
public int getNestedScrollAxes() {
return mParentHelper.getNestedScrollAxes();
}
@Override
public void computeScroll() {
OverScroller scroller = recyclerview.getScroller();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
}
實作NestedScrollingParent接口,隻要實作onNestedScroll這個方法,全部消費 NestedScrollingChild傳來的 dy 。
另外需要注意的onMeasure方法,重新測量高度,否則上滑時候RecyclerView下面部分會出現空白。
布局檔案
<?xml version="1.0" encoding="utf-8"?>
<cn.feng.xhsimageview.views.LinearLayoutNestScrollParent xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<cn.feng.xhsimageview.views.NestedRecyclerView
android:id="@+id/rv_photos"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="2dp"
android:layout_weight="1" />
</cn.feng.xhsimageview.views.LinearLayoutNestScrollParent>