安卓-CeilingNestedScrollView

最近又学了安卓吸顶嵌套滑动recyclerview,感觉还不错,分享一下。

(其实是很早之前学的,一直没写demo,然后学了忘忘了学,这次终于敲出来了!)

介绍

常用于主页、个人信息页等页面框架。

一般是长这样,进来可上下滑动,tabbar滑上去的时候吸顶在上面的导航栏底部,tabbar下面的内容可以左右滑动翻页,吸顶后再滑动滑动的是tabbar内部的rv里面的内容。

冲突场景分析

页面存在2层可竖直滑动的view

  • 外层:CustomNestedScrollView

  • 内层:ViewPager2的RecyclerView

需要解决的问题

  • 手指上滑,谁先消费

  • 外层fling到底部,惯性如何传给内层

  • tabbar吸顶时机、怎么吸顶怎么取消吸顶状态

核心代码

1.解决滑动冲突

java 复制代码
public class CustomNestedScrollView extends NestedScrollView implements View.OnScrollChangeListener {
    /**
     * 用于判断recyclerview是否在fling
     */
    boolean isStartFling = false;
    /**
     * 记录当前滑动y轴加速度
     */
    private int velocityY = 0;
    FlingHelper mFlingHelper;
    int totalDy = 0;
    ViewPager2 mViewPager2;

    public CustomNestedScrollView(@NonNull Context context) {
        super(context);
        init();
    }

    public CustomNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public void init() {
        setOnScrollChangeListener(this);
        mFlingHelper = new FlingHelper(getContext());
    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        int headerViewHeight = getChildAt(0).getMeasuredHeight() - getMeasuredHeight();
        //向上滑动,若当前topview可见,需要将topview滑动至不可见
        boolean hideTop = dy > 0 && getScrollY() < headerViewHeight;
        if (hideTop) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

    @Override
    public void fling(int velocityY) {
        super.fling(velocityY);
        this.velocityY = velocityY;
        if (velocityY > 0) {
            isStartFling = true;
            totalDy = 0;
        }
    }

    @Override
    public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        //在recyclerview fling的情况下,记录当前recyclerview在y轴的偏移
        totalDy += scrollY - oldScrollY;
        if (scrollY == (getChildAt(0).getMeasuredHeight() - getMeasuredHeight())) {
            if (velocityY != 0) {
                Double splineFlingDistance = mFlingHelper.getSpineFlingDistance(velocityY);
                if (splineFlingDistance > totalDy) {
                    mViewPager2 = getChildRecyclerView(this, ViewPager2.class);
                    if (mViewPager2 != null) {
                        RecyclerView childRecyclerView = getChildRecyclerView(((ViewGroup) mViewPager2.getChildAt(0)).getChildAt(mViewPager2.getCurrentItem()), RecyclerView.class);
                        if (childRecyclerView != null) {
                            childRecyclerView.fling(0, mFlingHelper.getVelocityByDistance(splineFlingDistance - Double.valueOf(totalDy)));
                        }
                    }
                }
            }
            totalDy = 0;
            velocityY = 0;
        }
    }

    private <T> T getChildRecyclerView(View viewGroup, Class<T> targetClass) {
        if (viewGroup != null && viewGroup.getClass() == targetClass) {
            return (T) viewGroup;
        }
        if (viewGroup instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) viewGroup).getChildCount(); i++) {
                View view = ((ViewGroup) viewGroup).getChildAt(i);
                if (view instanceof ViewGroup) {
                    T result = getChildRecyclerView(view, targetClass);
                    if (result != null) {
                        return result;
                    }
                }
            }
        }
        return null;
    }
}

2.处理外层

java 复制代码
public class MainActivity extends AppCompatActivity {

    final String[] labels = {"苹果", "香蕉", "番茄", "草莓"};

    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        ViewPagerAdapter viewPagerAdapter = new ViewPagerAdapter(this, getPageFragments());
        binding.viewpagerView.setAdapter(viewPagerAdapter);
        new TabLayoutMediator(binding.tablayout, binding.viewpagerView, new TabLayoutMediator.TabConfigurationStrategy() {
            @Override
            public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                tab.setText(labels[position]);
            }
        }).attach();
        binding.tablayoutViewpager.post(new Runnable() {

            @Override
            public void run() {
                binding.tablayoutViewpager.getLayoutParams().height = binding.scrollview.getMeasuredHeight();
                int height = binding.scrollview.getMeasuredHeight();
                if (Build.VERSION.SDK_INT >= 35) {
                    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
                    if (resourceId > 0) {
                        height -= getResources().getDimensionPixelSize(resourceId);
                    }
                }
                binding.tablayoutViewpager.getLayoutParams().height = height;
                binding.tablayoutViewpager.requestLayout();
            }
        });
    }

    private List<Fragment> getPageFragments() {
        ArrayList<Fragment> data = new ArrayList<>();
        data.add(new RecyclerViewFragment());
        data.add(new RecyclerViewFragment());
        data.add(new RecyclerViewFragment());
        data.add(new RecyclerViewFragment());
        return data;
    }
}

2.fling转距离

java 复制代码
public class FlingHelper {
    private static float DECELERATION_RATE = ((float) (Math.log(0.78d) / Math.log(0.9d)));
    private static float mFlingFriction = ViewConfiguration.getScrollFriction();
    private static float mPhysicalCoeff;

    public FlingHelper(Context context) {
        mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
    }

    private double getSplineDeceleration(int i) {
        return Math.log((double) ((0.35f * ((float) Math.abs(i))) / (mFlingFriction * mPhysicalCoeff)));
    }

    private double getSplineDecelerationByDistance(double d) {
        return ((((double) DECELERATION_RATE) - 1.0f) * Math.log(d / ((double) (mFlingFriction * mPhysicalCoeff)))) / ((double) DECELERATION_RATE);
    }

    public double getSpineFlingDistance(int i) {
        return Math.exp(getSplineDeceleration(i) * (((double) DECELERATION_RATE) / (((double) DECELERATION_RATE) - 1.0d))) * ((double) (mFlingFriction * mPhysicalCoeff));
    }

    public int getVelocityByDistance(double d) {
        return Math.abs((int) (((Math.exp(getSplineDecelerationByDistance(d)) * ((double) mFlingFriction)) * ((double) DECELERATION_RATE)) / 0.34999999999d));
    }
}

注意点

1.targetSDK 35及以上版本开启了edge-to-edge,内容会绘制到actionbar和状态栏后面导致

scrollview.getMeasuredHeight()拿到的是全屏高度,导致tabbar被遮挡,因此计算时需要减去这部分高度

java 复制代码
binding.tablayoutViewpager.getLayoutParams().height = binding.scrollview.getMeasuredHeight();
int height = binding.scrollview.getMeasuredHeight();
if (Build.VERSION.SDK_INT >= 35) {
  int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
  if (resourceId > 0) {
     height -= getResources().getDimensionPixelSize(resourceId);
  }
}
binding.tablayoutViewpager.getLayoutParams().height = height;
binding.tablayoutViewpager.requestLayout();

2.使用的databinding,需要在build.gradle里面配置

java 复制代码
 dataBinding {
        enabled true
    }

源码

https://gitee.com/flying-guy/daily-practice/blob/master/AndroidProject/MyApplication/CeilingNestScrollView/src/main/java/com/lwj/ceilingnestscrollview/MainActivity.java

效果视频

CeilingNestedRecyclerview

相关推荐
帅次2 小时前
Android 高级工程师面试参考答案:语言基础与并发
android·面试·职场和发展
凤年徐2 小时前
自动化构建工具:make 与 Makefile
android·java·linux·自动化
三少爷的鞋2 小时前
从 Callback 到 Coroutines:Android 异步并发方案的演进
android
鹏程十八少2 小时前
4. 2026金三银四 Android OkHttp 面试核心 45 问:从源码到架构深度解析
android·前端·面试
90后的晨仔11 小时前
Android Studio 项目模板完全指南
android
summerkissyou198711 小时前
Android-SurfaceView-投屏-常见问题
android·surfaceview
明天就是Friday11 小时前
Android实战项目④ OkHttp WebSocket开发即时通讯App 完整源码详解
android·websocket·okhttp
吉哥机顶盒刷机12 小时前
好物分享:DNA-Android-4.0.5安卓固件解包、打包工具
android·好物分享
三棱球13 小时前
App逆向学习笔记(三)——Android开发入门课
android·笔记