前言:通过NestedScrollView嵌套RecyclerView可以轻松实现嵌套滑动,但我们会发现RecyclerView懒加载失效了。
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:descendantFocusability="blocksDescendants">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="这是头部" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
原因:NestedScrollView高度虽然为屏幕高度,但其对子布局会进行wrap_content方式测量,这里LinearLayout即使是match_parent也不是屏幕高度,因此传递给其子RecyclerView也wrap_content方式得到的高度,导致RecyclerView一次加载出全部数据。(NestedScrollView嵌套滑动原理是还是平面式的滑动,RecyclerView由于加载了全部数据,本身不再滑动而是随着将LinearLayout移动实现的)
方案:要使RecyclerView懒加载则不能使其高度包裹所有item,需要指定最大高度(本例中最大高度为屏幕高度,先是NestedScrollView响应滑动当LinearLayout至最底部时,此时RecyclerView正好显示全,接下来再滑动就由RecyclerView来完成,这才是真正的嵌套滑动)
1、自定义RecyclerView使其支持设置最大高度
public class MaxHeightRecyclerView extends RecyclerView {
private int mMaxHeight;
public MaxHeightRecyclerView(@NonNull Context context) {
super(context);
}
public MaxHeightRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mMaxHeight != 0){
super.onMeasure(widthSpec, View.MeasureSpec.makeMeasureSpec(mMaxHeight, View.MeasureSpec.AT_MOST));
}else {
super.onMeasure(widthSpec, heightSpec);
}
}
public void setMaxHeight(int maxHeight) {
this.mMaxHeight = maxHeight;
}
}
2、自定义NestedScrollView,使其给RecyclerView设置最大高度
public class LazyNestedScrollView extends androidx.core.widget.NestedScrollView{
private int mRecyclerViewId;
private MaxHeightRecyclerView mRecyclerView;
public LazyNestedScrollView(@NonNull Context context) {
super(context);
}
public LazyNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LazyNestedScrollView);
//获取嵌套RecyclerView控件的id
mRecyclerViewId = typedArray.getResourceId(R.styleable.LazyNestedScrollView_recyclerview_id, 0);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//在super前给RecyclerView设置最大值,然后通过super进行测量即可
int height = MeasureSpec.getSize(heightMeasureSpec);
if (mRecyclerView != null){
mRecyclerView.setMaxHeight(height);
}else {
findViewById(this);
if (mRecyclerView != null){
mRecyclerView.setMaxHeight(height);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//根据id递归查询RecyclerView
private void findViewById(View view){
if (view instanceof ViewGroup && !(view instanceof RecyclerView)){
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++){
View childView = viewGroup.getChildAt(i);
findViewById(childView);
}
}else {
if (view.getId() == mRecyclerViewId && view instanceof MaxHeightRecyclerView){
mRecyclerView = (MaxHeightRecyclerView) view;
}
}
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
if (mRecyclerView != null){
View child = getChildAt(0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//获取自己能够滑动的距离
int topHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight();
int scrollY = getScrollY();
boolean topIsShow = scrollY >=0 && scrollY < topHeight;
if(topIsShow) {
//由自己响应滑动
int remainScrollY= topHeight - scrollY;
int selfScrollY = remainScrollY > dy ? dy : remainScrollY;
scrollBy(0, selfScrollY);
//告诉RecyclerView,自己滑动了多少距离
consumed[1] = selfScrollY;
} else {
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
}else {
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
}
}
注:
(1)RecyclerView的setNestedScrollingEnabled()应为true
(2)嵌套滑动是通过NestedScrollingParent3和NestedScrollingChild3来实现的,这里RecyclerView已经继承NestedScrollingChild3而NestedScrollView也已继承NestedScrollingParent3,RecyclerView先接收滑动事件然后先询问NestedScrollView来滑动(即onNestedPreScroll方法)然后将其滑动距离告诉RecyclerView,RecyclerView再对剩下的距离进行滑动
3、惯性滑动(NestedScrollView滑动到底部,将其滑动速度转化成惯性距离,计算子控件应滑距离=父惯性距离-父已滑距离,将子控件应滑距离转化成速度交给子控件进行惯性滑动)
(1)记录NestedScrollView惯性速度
@Override
public void fling(int velocityY) {
super.fling(velocityY);
mVelocityY = velocityY;
}
(2)将剩余的惯性速度传递给RecyclerView
@Override
protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
if (mRecyclerView != null){
View child = getChildAt(0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//判断是否滑动到底部
if (scrollY == child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin - getMeasuredHeight()){
if (mVelocityY > 0){
//将惯性速度转化为滑动距离
double distance = FlingHelper.getInstance(getContext()).getSplineFlingDistance(mVelocityY);
if (distance > scrollY){
//将剩余滑动距离转化为惯性速度
int velocityY = FlingHelper.getInstance(getContext()).getVelocityByDistance(distance - scrollY);
//将剩余惯性速度传递给RecyclerView
mRecyclerView.fling(0, velocityY);
//重置惯性速度
mVelocityY = 0;
}
}
}
}
}
(3)惯性速度和滑动距离转化工具类
public class FlingHelper {
private static FlingHelper mFlingHelper;
private static final double DECELERATION_RATE = Math.log(0.78) / Math.log(0.9);
private float mFlingFriction = ViewConfiguration.getScrollFriction();
private float mPhysicalCoeff;
private FlingHelper(Context context){
mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
}
public static FlingHelper getInstance(Context context){
if (mFlingHelper == null){
mFlingHelper = new FlingHelper(context);
}
return mFlingHelper;
}
public double getSplineFlingDistance(int i){
return Math.exp(getSplineDeceleration(i) * (DECELERATION_RATE / (DECELERATION_RATE - 1.0))) * (mFlingFriction * mPhysicalCoeff);
}
private double getSplineDeceleration(int i){
return Math.log(0.35f * Math.abs(i) / (mFlingFriction * mPhysicalCoeff));
}
public int getVelocityByDistance(double d){
return (int) Math.abs((Math.exp(getSplineDecelerationByDistance(d)) * mFlingFriction * mPhysicalCoeff / 0.3499999940395355));
}
private double getSplineDecelerationByDistance(double d){
return (DECELERATION_RATE - 1.0) * Math.log(d / (mFlingFriction * mPhysicalCoeff)) / DECELERATION_RATE;
}
}
|