news 2026/9/29 3:35:56

ScrollView滑不动?五步排查法从测量到事件分发彻底解决

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ScrollView滑不动?五步排查法从测量到事件分发彻底解决

1. 别急着改代码,先搞懂ScrollView为什么会“锁死”

1.1 ScrollView滚动究竟靠什么

先说一个我自己的经历。这个ScrollView无法滑动的问题,前前后后困扰了我一年多。第一次遇到是在一个商城项目的商品详情页里,外层是ScrollView,里面塞了图文详情、规格参数、用户评价列表,偏偏在部分测试机上就是滑不动。当时第一反应是机型兼容性问题,换了台设备试又好了,于是就没深究。直到后面项目越做越大,这个“偶发”问题反复出现,我才意识到,ScrollView滑不动绝不是玄学,而是它的测量和事件分发机制在某些布局组合下被“卡死”了。

要理解这个问题,得先看ScrollView的滚动原理。ScrollView是一个FrameLayout的子类,它会用MeasureSpec.UNSPECIFIED模式去测量它的直接子View,也就是说,子View想多高就多高,不受父容器高度限制,然后ScrollView自己通过纵向滚动来展示超出屏幕的部分。这个设计本身很巧妙:子View的高度决定了内容的总高度,ScrollView负责“窗口”和“滚动条”。

但你注意一个细节:ScrollView的滚动范围,完全取决于它测量出来的子View高度。如果子View的高度没有被正确测量,比如被父容器限制成了屏幕高度,那内容再多也滚不起来。我见过最典型的写法就是这样:

<ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 这里塞了很多内容 --> </LinearLayout> </ScrollView>

LinearLayout写成了match_parent,它就只会被测量成屏幕那么高,多余的内容直接超出边界,但ScrollView认为“内容高度就这么点”,自然不给你滚动。这个坑我踩了不止一次,很多同事写布局的时候习惯性复制粘贴,子View高度一直留着match_parent,ScrollView就只能安静地当一个“固定容器”。

1.2 顺藤摸瓜:先画清你的视图树

后来我养成了一个习惯:排查ScrollView问题之前,先在脑子里把视图树画一遍。ScrollView下面是谁?是LinearLayout、RelativeLayout还是ConstraintLayout?它的高度是wrap_content还是match_parent?它底下又嵌套了什么?RecyclerView?ViewPager?还是自定义View?

画视图树有两个好处。第一,能迅速排除“层级过高”导致的测量问题。Android的measure流程是递归的,父View的MeasureSpec会传给子View,子View再传给孙View,只要中间一层把测量结果“截胡”了,后面全乱套。第二,能帮你理清事件分发链路。你知道Touch事件是先执行父View的onInterceptTouchEvent,再决定要不要交给子View的,如果中间某个View把事件拦截了,或者onTouchEvent直接返回了true消费掉事件,ScrollView就再也拿不到滑动事件了。

所以我的建议是:遇到“Unable to scroll”这种问题,别急着在XML里改属性,先拿一张纸把布局层级画出来,标出每一层的宽高设置、滑动方向、是否有自定义触摸处理。80%的问题在画图的瞬间就能看出来。

2. 五步排查法:从现象到根因

2.1 先判现象:是完全不动还是滑一半卡住

排查ScrollView问题,第一步不是看代码,而是区分“现象”。因为“不能滚动”和“滚动被中断”是两种完全不同的故障,排查方向天差地别。

  • 完全不能滚动:手指上下滑,页面纹丝不动,ScrollBar也不出现。这种情况优先查测量问题、事件拦截问题。
  • 滑一半卡住:能滚一小段,但到某个位置就滚不动了,或者会回弹。这时候优先查嵌套滑动冲突、RecyclerView抢占触摸事件。
  • 滚动时页面乱跳:滚一下弹回去,或者滚动位置不稳定。注意查android:fillViewport设置、子View焦点变化、ScrollView内部自定义View的测量缓存。

我用一个简单的办法区分:在ScrollView上设置android:scrollbars="vertical",如果滚动条从不出现,说明ScrollView根本没进入“可滚动”状态,优先查高度测量;如果滚动条出现了但手滑不动,说明它认为自己可以滚,但事件被拦截了,优先查事件分发。

这个区分法帮我省了大量时间。之前有个项目反馈“ScrollView不能滑动”,我远程看半天布局没发现问题,后来让同事录了个屏,才发现滚动条是有出现的,就是手不跟手。最后定位到是内部某个自定义View的onTouchEvent未处理ACTION_MOVE之外的ACTION_UP,导致父View一直处于被抢占状态。

2.2 再验遮挡:是不是事件被顶层View吃掉了

如果确认ScrollView本身可以滚,但手指触控没有作用,第二个要查的是遮挡和事件消费链。最常见的肇事者有两个:一个是叠加在上层的透明View,一个是自定义View里不合理的onTouchEvent返回值。

透明View遮挡这个问题,在复杂页面里特别隐蔽。比如你给页面加了一个全局的水印层,或者一个悬浮的调试按钮,忘记把它的宽高改成wrap_content,而是写了个match_parent的透明背景板,它就会静悄悄地把所有触摸事件都接走。ScrollView连事件都收不到,自然一动不动。我在一个IM聊天项目里遇到过:用户反馈消息列表不能滑了,查到最后是一个临时加的“新消息提示浮层”,LinearLayout背景透明、高度match_parent,把整个ListView的手指事件全拦截了。

排查方法也简单,Android Studio的Layout Inspector可以看当前屏幕上的View层级,直接看哪个View的bounds覆盖了整个屏幕,再逐个关掉验证。验证的时候不要只改颜色,要真正设置android:visibility="gone"来测试。

还有一类情况是开发者在自定义View里重写了onTouchEvent:

@Override public boolean onTouchEvent(MotionEvent event) { // 只处理了自己需要的事件 return true; // 问题出在这 }

return true意味着这个View把事件消费掉了,事件不会再传给父视图的ScrollView。如果这个自定义View正好是ScrollView的子View,又恰好覆盖了用户手指触摸的区域,那滑动就完全失效。正确的做法是:不需要处理的事件一定要返回false,或者干脆不重写这个方法。我见过团队里有人为了让某个View“点击”更灵敏,无脑return true,结果把整个页面的滑动搞坏了。

2.3 然后验测量:布局高度有没有“物理锁死”

排除遮挡和事件抢占之后,就要回到测量问题。前面说了ScrollView的子View高度决定了可滚动范围,那么所有把子View高度“锁死”的写法,都是嫌疑对象。

最经典的是match_parent问题,已经讲过了。还有几个变体:

  • 父布局是LinearLayout且子布局设置了layout_weight,但高度没有写成0dp。在LinearLayout里,如果子View同时设置了layout_weight和layout_height="wrap_content",系统会先按wrap_content测量一次,再按weight分配剩余空间,这个过程中ScrollView的测量结果可能不符合预期。
  • ScrollView嵌套ScrollView,内外层高度都是wrap_content,内层ScrollView内容比外屏高,外层的测量被内层撑满,结果内外都不滚。
  • 设置了android:layout_height="fill_parent"(旧版属性),效果和match_parent一样。

如果你用ConstraintLayout作为ScrollView的子布局,也要检查约束项。约束不完整时,ConstraintLayout可能被测量成一个极小值,ScrollView就会认为内容只有那么高,一样滚不动。我建议所有ConstraintLayout子项都检查上下左右四个约束,尤其是垂直方向。

还有android:fillViewport这个属性也值得讲一下。它默认是false,ScrollView不会自动把子View拉伸到填满整个viewport。如果你的目的是让子View在内容不足时也能铺满屏幕,你可能会写:

<ScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true">

这时候子View被强制拉伸到屏幕高度,如果内容较多,反而是好事;如果内容少,也不会影响滚动。但如果你的子View已经有match_parent或weight=1之类的设置了,配合fillViewport="true"可能造成内容高度计算混乱。这属于“能滚但不听话”的范畴,可以先关掉试试。

2.4 接着验嵌套:RecyclerView和NestedScrollView打架

现在Android页面里列表控件用得越来越多,ScrollView套RecyclerView这种结构简直成了重灾区。你在ScrollView里放了一个高度wrap_content的RecyclerView,RecyclerView会把所有item都测量出来并一次性铺开,看起来是“展开了整个列表”,但你的ScrollView已经完全失去了滚动能力。

原因在于:RecyclerView自己是支持滚动的,在嵌套结构里它会和父容器竞争滚动事件。如果RecyclerView设置了android:nestedScrollingEnabled="false",它不参与嵌套滑动机制,事件会直接交给外层的ScrollView处理,外层就能正常滚动。问题是,这个方案有性能隐患:内层列表的所有item都在同一时间创建和测量,item一多就会卡顿。

更推荐的做法是用NestedScrollView代替ScrollView。NestedScrollView实现了NestedScrollingParent接口,它和RecyclerView之间有一套“协商”机制:RecyclerView滚动到边缘时,会“移交”事件给外层NestedScrollView,两段滚动不会互相打架。这是Android官方推荐的方向,也是我近两年用下来最稳的方案。

但说实话,NestedScrollView不是万能的。它和嵌套在里面的目标高度如果是wrap_content,而item数量巨大,一样有测量压力。我之前在订单列表页就吃过这个亏,五十个item一次性全渲染,直接卡到50帧以下。后面我把列表拆成了独立的部分,外层不再用ScrollView包整个页面,而是用CoordinatorLayout配合AppBarLayout来做联动,才真正解决问题。

所以排查嵌套问题时,你做的不是“换一个控件”,而是先问自己:这个页面真的需要ScrollView套RecyclerView吗?如果只是想要“顶部轮播图+列表”,完全可以用CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout + RecyclerView来实现;如果只是想在一个长页面里嵌入固定几个条目,那直接把数据铺成LinearLayout,别用RecyclerView。

2.5 最后验代码:有哪些监听过界了

布局本身没问题,但还是滑不动的时候,就得去翻代码了。我总结过几个常见的“代码级元凶”:

第一,setOnTouchListener返回了true。如果开发者在ScrollView上设置了一个触摸监听,并且返回了true,相当于你亲自告诉系统“这个事件我处理了”,ScrollView内部的onTouchEvent根本执行不到。很多新手在不知道onTouchListener和onTouchEvent区别的情况下,就在里面写了个return true,直接废掉了滚动功能。

第二,setScrollEnabled(false)被调用过。有些人为了方便做滚动控制,自定义了一个DisabledScrollView,里面暴露了一个开关:

public void setScrollEnabled(boolean enabled) { this.scrollEnabled = enabled; }

如果某个逻辑误调用了setScrollEnabled(false),并且没有再恢复,那ScrollView自然就死了。排查这类问题要全局搜索这个自定义类,看看有没有谁调用了关闭方法。

第三,在onInterceptTouchEvent里面无脑返回true,或者返回了super.onInterceptTouchEvent(event)但手势识别逻辑写错了。这个一般在自定义View里发生,一出现就是“列表里面的横向滑动完全失效”,但也可能把纵向滑动也吃掉了。

我在排查这种问题的时候,会在关键位置打Log:

@Override public boolean onTouchEvent(MotionEvent event) { Log.d("ScrollDebug", "onTouchEvent: " + event.getAction() + " return " + super.onTouchEvent(event)); return super.onTouchEvent(event); }

打几次Log,事件的流动路径清清楚楚。谁的Log没有打出来,谁就没收到事件,卡点在逻辑上就找到了。

3. 四个真实场景:从现象到修复的完整过程

3.1 场景一:RecyclerView套在ScrollView里,外层彻底失灵

曾经有个阅读类App的用户反馈:书评页排版很乱,上下划不动,只有中间一个“评论列表区域”能自己滚动。我一看布局就是经典的ScrollView套RecyclerView问题。

当时的代码是这样的:

<ScrollView android:id="@+id/scroll_root" 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="书名、作者信息等头部内容" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/comment_list" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="底部分享按钮区域" /> </LinearLayout> </ScrollView>

现象是:外层ScrollView根本滚不动,但是手指在评论列表区域上下滑时,列表自己能滚,只是滚到底之后就卡住,既不能继续触发外层滚动,也无法看到底部分享按钮。这个现象非常典型:RecyclerView把滚动事件全吃了。

我的处理方案:外层改成androidx.core.widget.NestedScrollView,RecyclerView保持原样。但注意,只改NestedScrollView还不够,为了让两个控件协调得更好,我还给RecyclerView挂了setNestedScrollingEnabled(false)。你可能觉得这个和之前说的矛盾,其实不是。NestedScrollView作为父布局,RecyclerView设置nestedScrollingEnabled=false之后,RecyclerView内部不再自行滚动了,所有滚动事件直接交给NestedScrollView统一调度。在内容不多、列表不长的场景下,这个组合性能是可以接受的。

真正的根治方式是把RecyclerView去掉,改成一个简单的LinearLayout来承载评论数据。因为那个页面底下的评论列表最多十条,根本没有必要上RecyclerView,用LinearLayout铺开反而更快,滚动也更流畅。我后来在代码里删掉了RecyclerView,直接用addView往LinearLayout里添加评论item,所有问题迎刃而解,还省了一大截内存。

3.2 场景二:ScrollView子布局高度match_parent,内容撑不出去

第二个场景是在一个表单页面。页面布局:

<ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText ... /> <Spinner ... /> <!-- 后面还跟了二十多个表单项 --> </LinearLayout> </ScrollView>

看起来平平无奇,但LinearLayout的高度写成了match_parent。在ScrollView的测量机制里,它的直接子View会收到一个MeasureSpec.UNSPECIFIED的约束,理论上高度不受限制。但如果子View自身宽度和高度都写的是match_parent,并且它内部的子项又是按权重分配高度的话,它的实际测量高度会趋近于父View的高度,也就是屏高。那些超出屏幕的EditText和Spinner虽然还在视图树里,但ScrollView认为它们的总高度没有超过自己,所以不会给滚动口子。

我当时在这个表单页里加了个“照片上传”功能,照片一多,表单内容明显超过屏幕,但页面依然纹丝不动。检查了所有父容器高度属性之后,把LinearLayout的layout_height从match_parent改成wrap_content,再配合android:paddingBottom="100dp"留出底部空间,问题立刻解决。

这里有个细节值得说:如果你用的是Android Studio的Layout Editor,拖拽的时候它会默认给你加match_parent,特别是从别的页面复制布局过来的时候,很多人根本没注意这个高度属性。我后面给自己定了个规矩:ScrollView的直接子View,除了特殊情况,一律wrap_content,如果想让内容不满屏时也占满高度,用android:minHeight="match_parent"而不是直接写match_parent。

3.3 场景三:setOnTouchListener把滑动事件消费干净

这个案例来自一个天气App的主页。页面上方是天气主体信息,下面是一个ScrollView,里面放了一周天气预报的卡片。用户反馈:温度曲线区域外的地方都能滑,但一碰到那张“温度折线图”,ScrollView就死掉了,手往上一推,页面纹丝不动。

折线图是一个自定义View,它自己处理了触摸手势,支持点击某个点弹出温度详情。我看源码,发现自定义View里是这么写的:

@Override public boolean onTouchEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { startTracking(event); } return true; }

return true把ACTION_DOWN之后的ACTION_MOVE事件当成自己的手势消费掉了,即使它内部对ACTION_MOVE什么都不做。结果就是:手指按在温度折线图上时,ScrollView收到ACTION_DOWN但紧接着的ACTION_MOVE全被这个View截走,ScrollView的滚动逻辑完全被跳过。

修复方式有两个方向。一个是把return true改成return super.onTouchEvent(event),让不需要处理的事件自动向上传递;另一个是在自定义View的onTouchEvent里先判断自己是否真的需要这次触摸:

@Override public boolean onTouchEvent(MotionEvent event) { if (needHandleGesture(event)) { handleGesture(event); return true; } return false; }

只有确实需要处理的手势才返回true,其他情况一律放行。修改之后,温度折线图点击、ScrollView滑动两不误。

这个坑也提醒我:任何时候都不要无脑return true。在Android事件分发体系中,return true就是一个“令牌”,你把令牌拿了,别人就没资格碰这个事件。点按和滚动的边界感要非常清楚。

3.4 场景四:EditText抢焦点导致滑一半卡住

最后一个场景很隐蔽。页面是一个评论编辑页,顶部就是ScrollView,里面放了一个EditText用于输入评论,页面下方有一个提交按钮。用户说:评论框打字的时候一切正常,但打完字收起键盘后,ScrollView就“卡住了”,往上滑到编辑框附近就滑不动,还会乱跳。

这个问题的核心不是触摸事件,而是焦点和光标导致ScrollView的滚动跳变。EditText获得焦点时,系统会保证它完整可见,自动计算滚动偏移量把EditText滚到屏幕内。如果EditText下面还有很长的内容,ScrollView的滚动位置会被EditText的焦点“钳制”住,你手动上滑,它又自动滚回焦点区域,表现得就像“不能滑动”。

处理方式有几种:

  • 在根布局上设置android:descendantFocusability="beforeDescendants",让父布局在子View之前获取焦点,避免EditText一进页面就抢焦点。
  • 在ScrollView不可见的地方放一个android:focusable="true"和android:focusableInTouchMode="true"的透明View,主动把焦点先吸走。
  • 在Activity的onPause里调用editText.clearFocus(),并在onResume里做一次scrollView.fullScroll(View.FOCUS_UP),重置滚动位置。

还有一个配套技巧:给ScrollView设置android:windowSoftInputMode="adjustResize",并配合android:fitsSystemWindows="true",能让软键盘弹出时页面自动压缩,滚动范围更可预期。反正我在评论区域外部加了一个焦点重置逻辑之后,这类“键盘导致无法滑动”的问题基本绝迹了。

5. 一年踩坑后的几个经验和工具箱

5.1 布局写法的检查清单

写了一年多的ScrollView问题排查,我给自己整理了一张“布局安全清单”,每次新建带滚动页面的时候都过一遍:

  • 直接子View高度写wrap_content,不要match_parent。
  • 尽量不要在ScrollView里再嵌ScrollView,更不要嵌RecyclerView。
  • 所有嵌套列表优先考虑NestedScrollView,但也要评估item数量,超过20条就另想方案。
  • 自定义View的onTouchEvent务必谨慎返回true,只有确实消费的事件才返回true。
  • 在ScrollView里放置EditText时,考虑焦点抢占问题。
  • 给ScrollView加上android:scrollbars="vertical",方便调试时观察滚动条。
  • 内层列表禁用横向滚动或者嵌套滑动时,明确测试纵向边界。

这几条大概能覆盖90%的“滚动失效”场景。剩下的10%属于一些比较偏门的情况,比如硬件加速问题、主题里强制设置了overScrollMode等等,一般都要靠Log逐步排查。

5.2 调试用的几个工具和技巧

如果你也想快速定位ScrollView问题,这几个工具和方法值得存一下:

工具/方法用途使用建议
Layout Inspector查看运行时布局层级和尺寸注意看“View是否超出屏幕”和“高度是否为0”
Developer Options -> Show layout bounds显示所有View的边框在真机上快速判断哪些View占了全屏
Log打印onTouchEvent看清事件分发路径在ScrollView和子View都打,对比谁没收到事件
getScrollY()和getMeasuredHeight()判断ScrollView是否处于可滚动状态计算child.getHeight() - scrollView.getHeight()是否大于0
打开或关闭fillViewport属性排查子View被拉伸导致的高度异常每次改完跑一遍真实数据再判断

特别要提醒的是getMeasuredHeight这个API。我在排查“为什么我不能滚”的时候,会先打印scrollView.getChildAt(0).getMeasuredHeight()和scrollView.getMeasuredHeight(),如果前者不大于后者,那ScrollView在数学上就不可能产生滚动。这个方法比看布局文件直观得多,因为很多问题是运行期才暴露的,XML里看着一切正常。

5.3 为什么这个错误能错一年

说回题目里那个“这个错误已经错了1年多了”。为什么一个看似简单的ScrollView问题,能在项目里潜伏一年?我自己的体会是:这类问题通常不是出现在主角页面,而是出现在那些“偶尔用一次”的二级、三级页面上。人员一换、流程一乱,问题就被归因成“机型问题”“数据异常”,没有人去追根因。

加上ScrollView“滑不动”本身是一个组合症状,它可能是布局问题、事件问题、焦点问题、测量问题、代码问题,五条线汇在一起。你只从一条线去查,查两天也查不出结果。所以我觉得真正值钱的不是某一个修复代码,而是排查和归类的思路。你把一次问题当成五类问题去分析,养成上面说的那几个习惯,ScrollView这种“大哥级别”的老问题,基本就绝迹了。

最后说一个小技巧吧。后来我每次提交代码前,会特意在真机上跑一遍“快速上下用力滑动”的测试,专测滚动类页面,这个习惯帮我揪出了好几次回归。ScrollView问题就是典型的“它在XML里看不出毛病,但一上手就有问题”,所以别偷懒,滚一滚,比看十遍页面都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 3:34:42

让 Claude Code 越写越像你:用 Hook 自动积累编码规范的实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:33:39

ADB从入门到进阶:安装连接、常用命令与实战排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 3:30:40

虚函数与虚表:多态的成本到底花在哪

① 钩子&#xff1a;同样一行 b->f()&#xff0c;三种命运 同样一行 b->f()&#xff0c;编译器有时直接跳转&#xff0c;有时绕两个弯&#xff0c;有时还会"自作聪明"地在调用前先检查一遍虚表。 多态的代价到底花在哪&#xff1f;答案全在这几行汇编里。 这一…

作者头像 李华
网站建设 2026/9/29 3:29:06

【数据结构】图与树 · 算法手记与练习

#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <math.h>#define MAXN 1010int iMaxLength 0;//最长路径长度 int iCurrentLength 0;//当前路径长度 typedef int ElemType; ElemType stMax_Path[MAXN];//最长路径元素 ElemT…

作者头像 李华
网站建设 2026/9/29 3:28:27

Go gRPC 生产级部署:连接池 + 重试 + 超时 + 熔断全攻略

Go gRPC 生产级部署&#xff1a;连接池 重试 超时 熔断全攻略微服务架构离不开 gRPC&#xff0c;但默认 client-server 配置远不能满足生产需要。本文详解 gRPC 的连接管理、错误恢复与可观测性。一、连接池&#xff1a;gRPC 单连接复用 不同于 HTTP 池化&#xff0c;gRPC 默…

作者头像 李华
网站建设 2026/9/29 3:26:51

FanControl 上手指南:3步用温度曲线压住风扇噪音

FanControl 上手指南&#xff1a;3步用温度曲线压住风扇噪音 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanC…

作者头像 李华