NestedScrollView 嵌套 RecyclerView 滚动到指定位置触发事件

当使用 NestedScrollView 嵌套 RecyclerView 时,需要采用不同的方法来实现滚动到指定位置并触发事件。以下是完整的解决方案:

1. 布局结构示例

<androidx.core.widget.NestedScrollView
    android:id="@+id/nestedScrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- 其他内容 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Header Content"/>
        
        <!-- RecyclerView -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        
        <!-- 其他内容 -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Footer Content"/>

    </LinearLayout>
</androidx.core.widget.NestedScrollView>

2. 核心工具类 - 计算和滚动到指定位置

public class NestedScrollViewUtils {

    /**
     * 滚动到 RecyclerView 的指定项
     */
    public static void scrollToRecyclerViewItem(NestedScrollView nestedScrollView, 
                                              RecyclerView recyclerView, 
                                              int targetPosition,
                                              Runnable onReachedCallback) {
        
        // 确保 RecyclerView 已经完成布局
        recyclerView.post(() -> {
            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
            if (layoutManager == null) return;

            View targetView = layoutManager.findViewByPosition(targetPosition);
            if (targetView != null) {
                // 项已经在屏幕上,直接触发回调
                if (onReachedCallback != null) {
                    onReachedCallback.run();
                }
                return;
            }

            // 计算目标项的位置
            int targetItemTop = calculateItemPosition(recyclerView, targetPosition);
            
            // 滚动到目标位置
            scrollToPosition(nestedScrollView, recyclerView, targetItemTop, onReachedCallback);
        });
    }

    /**
     * 计算指定项在 NestedScrollView 中的绝对位置
     */
    private static int calculateItemPosition(RecyclerView recyclerView, int position) {
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (!(layoutManager instanceof LinearLayoutManager)) return 0;

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        
        // 获取 RecyclerView 在父容器中的位置
        int recyclerViewTop = recyclerView.getTop();
        
        // 计算目标项在 RecyclerView 中的位置
        int itemPositionInRecyclerView = 0;
        for (int i = 0; i < position; i++) {
            View itemView = linearLayoutManager.findViewByPosition(i);
            if (itemView != null) {
                itemPositionInRecyclerView += itemView.getHeight();
            } else {
                // 如果视图不可见,使用估计高度
                itemPositionInRecyclerView += 100; // 估计高度,可根据实际情况调整
            }
        }
        
        return recyclerViewTop + itemPositionInRecyclerView;
    }

    /**
     * 执行滚动操作
     */
    private static void scrollToPosition(NestedScrollView nestedScrollView, 
                                       RecyclerView recyclerView, 
                                       int targetY, 
                                       Runnable onReachedCallback) {
        
        nestedScrollView.smoothScrollTo(0, targetY);
        
        // 添加滚动监听来检测是否到达目标位置
        nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                checkIfTargetReached(v, recyclerView, scrollY, targetY, onReachedCallback);
            }
        });
    }

    /**
     * 检查是否滚动到目标位置
     */
    private static void checkIfTargetReached(NestedScrollView nestedScrollView, 
                                           RecyclerView recyclerView, 
                                           int currentScrollY, 
                                           int targetY,
                                           Runnable onReachedCallback) {
        
        // 允许一定的误差范围
        int errorMargin = 10;
        
        if (Math.abs(currentScrollY - targetY) <= errorMargin) {
            // 到达目标位置,移除监听并执行回调
            nestedScrollView.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) null);
            
            if (onReachedCallback != null) {
                onReachedCallback.run();
            }
        }
    }
}

3. 监听 RecyclerView 项进入视图

public class RecyclerViewInNestedScrollObserver {

    private final NestedScrollView nestedScrollView;
    private final RecyclerView recyclerView;
    private final Set<Integer> triggeredPositions = new HashSet<>();

    public RecyclerViewInNestedScrollObserver(NestedScrollView nestedScrollView, 
                                            RecyclerView recyclerView) {
        this.nestedScrollView = nestedScrollView;
        this.recyclerView = recyclerView;
        setupScrollListener();
    }

    private void setupScrollListener() {
        nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
 public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                checkVisibleItems(scrollY);
            }
        });
    }

    /**
     * 检查哪些 RecyclerView 项当前可见
     */
    private void checkVisibleItems(int scrollY) {
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (!(layoutManager instanceof LinearLayoutManager)) return;

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        int firstVisible = linearLayoutManager.findFirstVisibleItemPosition();
        int lastVisible = linearLayoutManager.findLastVisibleItemPosition();

        if (firstVisible == RecyclerView.NO_POSITION || lastVisible == RecyclerView.NO_POSITION) return;

        // 获取 RecyclerView 在屏幕中的位置
        int[] recyclerViewLocation = new int[2];
        recyclerView.getLocationOnScreen(recyclerViewLocation);
        int recyclerViewTopOnScreen = recyclerViewLocation[1];
        int recyclerViewBottomOnScreen = recyclerViewTopOnScreen + recyclerView.getHeight();

        // 检查每个可见项
        for (int i = firstVisible; i <= lastVisible; i++) {
            View itemView = linearLayoutManager.findViewByPosition(i);
            if (itemView != null && !triggeredPositions.contains(i)) {
                int[] itemLocation = new int[2];
                itemView.getLocationOnScreen(itemLocation);
                int itemTop = itemLocation[1];
                int itemBottom = itemTop + itemView.getHeight();

                // 检查项是否完全在屏幕内
                if (itemTop >= recyclerViewTopOnScreen && itemBottom <= recyclerViewBottomOnScreen) {
                    triggeredPositions.add(i);
                    onItemBecameFullyVisible(i, itemView);
                }
            }
        }
    }

    /**
     * 当项完全可见时触发
     */
    protected void onItemBecameFullyVisible(int position, View itemView) {
        // 子类重写此方法实现具体逻辑
        Log.d("ScrollObserver", "Item at position " + position + " is fully visible");
    }

    /**
     * 重置触发状态
     */
    public void resetTriggeredPositions() {
        triggeredPositions.clear();
    }
}

4. 使用示例

public class MainActivity extends AppCompatActivity {

    private NestedScrollView nestedScrollView;
    private RecyclerView recyclerView;
    private MyAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        nestedScrollView = findViewById(R.id.nestedScrollView);
        recyclerView = findViewById(R.id.recyclerView);

        // 设置 RecyclerView
        adapter = new MyAdapter(createData());
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        
        // 设置嵌套滚动
        recyclerView.setNestedScrollingEnabled(false);

        // 初始化观察者
        setupScrollObserver();

        // 示例:滚动到第15项
        findViewById(R.id.btn_scroll_to_item).setOnClickListener(v -> {
            scrollToItem(15, () -> {
                Toast.makeText(this, "已滚动到第15项", Toast.LENGTH_SHORT).show();
            });
        });
    }

    private void setupScrollObserver() {
        new RecyclerViewInNestedScrollObserver(nestedScrollView, recyclerView) {
            @Override
            protected void onItemBecameFullyVisible(int position, View itemView) {
                // 特殊项处理
                if (position == 5) {
                    highlightSpecialItem(itemView);
                }
                
                // 分组处理
                if (position >= 10 && position <= 15) {
                    onGroupVisible(10, 15);
                }
            }
        };
    }

    private void scrollToItem(int position, Runnable callback) {
        NestedScrollViewUtils.scrollToRecyclerViewItem(
            nestedScrollView, 
            recyclerView, 
            position, 
            callback
        );
    }

    private void highlightSpecialItem(View itemView) {
        itemView.setBackgroundColor(Color.YELLOW);
        // 其他高亮逻辑
    }

    private void onGroupVisible(int start, int end) {
        Log.d("Group", "Group from " + start + " to " + end + " is visible");
        // 分组可见时的处理逻辑
    }
}

5. 分组触发实现

public class GroupVisibilityTracker extends RecyclerViewInNestedScrollObserver {
    
    private final int groupStart;
    private final int groupEnd;
    private boolean groupTriggered = false;

    public GroupVisibilityTracker(NestedScrollView nestedScrollView, 
                                 RecyclerView recyclerView, 
                                 int groupStart, 
                                 int groupEnd) {
        super(nestedScrollView, recyclerView);
        this.groupStart = groupStart;
        this.groupEnd = groupEnd;
    }

    @Override
    protected void onItemBecameFullyVisible(int position, View itemView) {
        // 检查整个分组是否可见
        if (!groupTriggered && isWholeGroupVisible()) {
            groupTriggered = true;
            onGroupFullyVisible();
        }
    }

    private boolean isWholeGroupVisible() {
        RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
        if (!(layoutManager instanceof LinearLayoutManager)) return false;

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        
        for (int i = groupStart; i <= groupEnd; i++) {
            View itemView = linearLayoutManager.findViewByPosition(i);
            if (itemView == null || !isViewFullyVisible(itemView)) {
                return false;
            }
        }
        return true;
    }

    private boolean isViewFullyVisible(View view) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int itemTop = location[1];
        int itemBottom = itemTop + view.getHeight();

        int[] recyclerViewLocation = new int[2];
        getRecyclerView().getLocationOnScreen(recyclerViewLocation);
        int rvTop = recyclerViewLocation[1];
        int rvBottom = rvTop + getRecyclerView().getHeight();

        return itemTop >= rvTop && itemBottom <= rvBottom;
    }

    protected void onGroupFullyVisible() {
        // 子类重写实现分组可见逻辑
        Log.d("GroupTracker", "Group from " + groupStart + " to " + groupEnd + " is fully visible");
    }
}

6. 注意事项

  1. 性能优化:在滚动监听中避免频繁的复杂计算
  2. 布局完成:确保在 RecyclerView 完成布局后再进行位置计算
  3. 内存管理:及时移除不再需要的监听器
  4. 误差处理:由于嵌套滚动的复杂性,需要适当的误差容限
  5. RecyclerView 高度:确保 RecyclerView 的 layout_height 不是 match_parent

这种方法可以有效地在 NestedScrollView 嵌套 RecyclerView 的场景中实现精确的滚动触发事件。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容