Android NestedScrolling嵌套滑动机制

Android在发布5.0之后加入了嵌套滑动机制NestedScrolling,为嵌套滑动提供了更方便的处理方案。在此对嵌套滑动机制进行详细的分析。

嵌套滑动的常见用法比如在滑动列表的时候隐藏相关的TopBar和BottomBar,增加列表的信息展示范围,让用户聚焦于App想展示的内容上等。官方出的Design包里也有很多支持该机制的炫酷控件,比如CoordinatorLayout,AppBarLayout等,在用户体验上有很大的进步。

说道嵌套滑动,离不开以下几个内容:

  • NestedScrollingChild
  • NestedScrollingParent
  • NestedScrollingChildHelper
  • NestedScrollingParentHelper

在具体说明之前,先来看看我们的Sample,这是一个仿携程机票首页的Demo

这里用到了一个实现了NestedScrollingParent的CollaspingLayout作为父View和一个实现了NestedScrollingChild的NestedScrollView作为子View进行嵌套滑动,布局可以简单的描述成:

具体的布局结构大致如下:

<com.lycc.flight.fastproject.widget.search.CollaspingLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:orientation="vertical"android:id="@+id/pl_container"android:layout_width="match_parent"android:layout_height="match_parent"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="160dp"><com.yyydjk.library.BannerLayoutandroid:id="@+id/banner"android:layout_width="match_parent"android:layout_height="160dp"app:autoPlayDuration="5000"app:indicatorMargin="50dp"app:indicatorPosition="centerBottom"app:indicatorShape="oval"app:indicatorSpace="3dp"app:scrollDuration="1100"app:defaultImage="@mipmap/ic_launcher"app:selectedIndicatorColor="?attr/colorPrimary"app:selectedIndicatorHeight="6dp"app:selectedIndicatorWidth="6dp"app:unSelectedIndicatorColor="#99ffffff"app:unSelectedIndicatorHeight="6dp"app:unSelectedIndicatorWidth="6dp"app:layout_collapseMode="parallax"app:layout_collapseParallaxMultiplier="0.7"/><Viewandroid:id="@+id/view"android:layout_width="match_parent"android:layout_height="40dp"android:background="@drawable/gradient" /><FrameLayoutandroid:id="@+id/search_tab_container"android:layout_width="match_parent"android:layout_height="43dp"android:layout_marginBottom="-4dp"android:layout_alignParentBottom="true"><Viewandroid:layout_width="match_parent"android:layout_height="40dp"android:background="#5a000000"android:layout_marginLeft="5dp"android:layout_marginTop="3dp"android:layout_marginBottom="-4dp"android:layout_marginRight="5dp"/><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="bottom"android:layout_marginBottom="-4dp"android:layout_marginLeft="5dp"android:layout_marginRight="5dp"android:orientation="horizontal"><Viewandroid:id="@+id/slide_bg"android:layout_width="120dp"android:layout_height="43dp"android:background="@drawable/ctrip_slide_tab"/></LinearLayout><RadioGroupandroid:id="@+id/rg_slide"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"android:gravity="center"android:layout_centerInParent="true"><RadioButtonandroid:id="@+id/rb_left"android:background="@null"android:textColor="@color/top_layout_sliide_text_color_selector"android:gravity="center"android:button="@null"android:textSize="16dp"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:checked="false"android:text="单程" /><RadioButtonandroid:id="@+id/rb_center"android:background="@null"android:textColor="@color/top_layout_sliide_text_color_selector"android:gravity="center"android:textSize="16dp"android:button="@null"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="往返" /><RadioButtonandroid:id="@+id/rb_right"android:background="@null"android:button="@null"android:textColor="@color/top_layout_sliide_text_color_selector"android:gravity="center"android:textSize="16dp"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:singleLine="true"android:text="多程"android:visibility="visible" /></RadioGroup></FrameLayout><LinearLayoutandroid:id="@+id/top_container"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"android:minHeight="?attr/actionBarSize"android:orientation="horizontal"android:visibility="gone"android:gravity="center"android:layout_alignParentTop="true"app:layout_collapseMode="pin"android:background="@color/ctirp_color_primary"><RadioGroupandroid:layout_width="261dp"android:layout_height="wrap_content"android:orientation="horizontal"android:gravity="center"android:layout_centerInParent="true"><RadioButtonandroid:background="@drawable/title_left_shape"android:padding="6dp"android:textColor="@color/top_layout_text_color_selector"android:gravity="center"android:button="@null"android:textSize="16dp"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:checked="true"android:text="单程" /><RadioButtonandroid:background="@drawable/title_center_shape"android:padding="6dp"android:textColor="@color/top_layout_text_color_selector"android:gravity="center"android:textSize="16dp"android:button="@null"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_marginLeft="-1dp"android:layout_marginRight="-1dp"android:text="往返" /><RadioButtonandroid:background="@drawable/title_right_shape"android:padding="6dp"android:button="@null"android:textColor="@color/top_layout_text_color_selector"android:gravity="center"android:textSize="16dp"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:singleLine="true"android:text="多程"android:visibility="visible" /></RadioGroup></LinearLayout></RelativeLayout><android.support.v4.widget.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="wrap_content"app:layout_behavior="@string/appbar_scrolling_view_behavior"><ImageViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:src="@drawable/search_bg"android:scaleType="fitStart"/></android.support.v4.widget.NestedScrollView></com.lycc.flight.fastproject.widget.search.CollaspingLayout>复制代码

从布局可以看到其实在实现了NestedScrollingParent之后就能很方便的完成子View和父View的嵌套滑动,下面就来简单看看上面的四个类是如何使用的,在系统为我们提供的控件中,NestedScrollView是实现了这个机制的控件,以它的实现为例,首先看作为嵌套滑动的子View:

        // NestedScrollingChild@Overridepublic void setNestedScrollingEnabled(boolean enabled) {mChildHelper.setNestedScrollingEnabled(enabled);}@Overridepublic boolean isNestedScrollingEnabled() {return mChildHelper.isNestedScrollingEnabled();}@Overridepublic boolean startNestedScroll(int axes) {return mChildHelper.startNestedScroll(axes);}@Overridepublic void stopNestedScroll() {mChildHelper.stopNestedScroll();}@Overridepublic boolean hasNestedScrollingParent() {return mChildHelper.hasNestedScrollingParent();}@Overridepublic boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow) {return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,offsetInWindow);}@Overridepublic boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);}@Overridepublic boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);}@Overridepublic boolean dispatchNestedPreFling(float velocityX, float velocityY) {return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);}复制代码

再来看看同样作为嵌套滑动父View的CollaspingLayout的实现

    // NestedScrollingParent@Overridepublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;}@Overridepublic void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);}@Overridepublic void onStopNestedScroll(View target) {if(mHeaderController.getScrollPercentage() == 1.0f){mHeaderState = STATE_IDLE_TOP;}else if(mHeaderController.getScrollPercentage() == 0.0f){mHeaderState = STATE_IDLE_BOTTOM;}computeScroll();}@Overridepublic void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed) {final int myConsumed = moveBy(dyUnconsumed);final int myUnconsumed = dyUnconsumed - myConsumed;}@Overridepublic void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {if (dy > 0 && mHeaderController.canScrollUp()) {final int delta = moveBy(dy);consumed[0] = 0;consumed[1] = delta;}}@Overridepublic boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {if (!consumed) {flingWithNestedDispatch((int) velocityY);return true;}return false;}复制代码

从上面的实现可以看出,基本上都是通过mParentHelper和mChildHelper来完成滑动的,没接触过这方面的同学看着肯定觉得很难理解,的确有些跳跃性,在说清楚这个问题之前必须先把这几个类之间的交互逻辑理清楚才能不至于不知所云。
先来梳理一下子View和父View的接中都有哪些方法。这种套路一般都是子View发起的然后父View进行回调从而完成配合。

子View 父View
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

这里的子View指的是实现了NestedScrollingChild的View,例如我们的NestedScrollView,父View指的是实现了NestedScrollingParent的View,比如我们上面写的CollaspingLayout。

首先在子View滑动还未开始之前将调用startNestedScroll,对应NestedScrollView中的ACTION_DOWN:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {case MotionEvent.ACTION_DOWN: {......startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//在接到点击事件之初调用break;                           }
}复制代码

那么调用 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)寓意何在?跟进去看到其实是调用mChildHelper.startNestedScroll(axes)的实现

public boolean startNestedScroll(int axes) {if (hasNestedScrollingParent()) {// Already in progressreturn true;}if (isNestedScrollingEnabled()) {ViewParent p = mView.getParent();View child = mView;while (p != null) {//重点在这-------> 在子View开始滑动前通知父View,回调到父View的onStartNestedScroll(),//父View需要滑动则返回true:if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {mNestedScrollingParent = p;//---------> 如果父View决定要和子View一块滑动,调用父ViewonNestedScrollAccepted()ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);return true;}if (p instanceof View) {child = (View) p;}p = p.getParent();}}return false;}复制代码

大家仔细看我在代码里加的注释,需要关心的就是父View在此时需要决定是否跟随子View滑动,看看父View的实现:

 @Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}复制代码

ViewCompat.SCROLL_AXIS_VERTICAL的值是2(10),所以当nestedScrollAxes 也为2的时候,返回true,回到上面可以看到只要是竖直方向的 滑动,父View就会和子View进行嵌套滑动。而在父View的
onNestedScrollAccepted中,则把滑动的方向给保存下来了。这样父View和子View的第一次合作关系就结束了,再看看接下来是如何配合的。
当子View在滑动的Move事件中,又开始了嵌套滑动

 @Override
public boolean onTouchEvent(MotionEvent ev) {case MotionEvent.ACTION_MOVE:final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);int deltaY = mLastMotionY - y;if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {deltaY -= mScrollConsumed[1];vtev.offsetLocation(0, mScrollOffset[1]);mNestedYOffset += mScrollOffset[1];}
}复制代码

在子View决定滑动的时候,再次在进行自己的滑动前调用dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)

     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {if (dx != 0 || dy != 0) {int startX = 0;int startY = 0;if (offsetInWindow != null) {mView.getLocationInWindow(offsetInWindow);startX = offsetInWindow[0];startY = offsetInWindow[1];}if (consumed == null) {if (mTempNestedScrollConsumed == null) {mTempNestedScrollConsumed = new int[2];}consumed = mTempNestedScrollConsumed;}//--------->重点在这,首先把consume封装好,consumed[0]表示X方向父View消耗的距离,// consumed[1]表示Y方向上父View消耗的距离,在父View处理前当然都是0consumed[0] = 0;consumed[1] = 0;//然后调用父View的onNestedPreScroll并把当前的dx,dy以及消耗距离的consumed传递过去ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);if (offsetInWindow != null) {mView.getLocationInWindow(offsetInWindow);offsetInWindow[0] -= startX;offsetInWindow[1] -= startY;}return consumed[0] != 0 || consumed[1] != 0;} else if (offsetInWindow != null) {offsetInWindow[0] = 0;offsetInWindow[1] = 0;}}return false;}复制代码

看看父View是怎么处理的,也是实现了这套机制的,看看他是怎么处理的:

    @Overridepublic void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {if (dy > 0 && mHeaderController.canScrollUp()) {final int delta = moveBy(dy);consumed[0] = 0;consumed[1] = delta;}}复制代码

通过moveby计算父View滑动的距离,并将父ViewY方向消耗的距离记录下来

继续来看子View,在通知了父View并且父View消耗了滑动距离之后,剩下的就是自己进行滑动了

@Override
public boolean onTouchEvent(MotionEvent ev) {case MotionEvent.ACTION_MOVE:final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);int deltaY = mLastMotionY - y;if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {deltaY -= mScrollConsumed[1];//重点在这:-------->父View滑动之后调整自己的Offset为父View滑动的距离vtev.offsetLocation(0, mScrollOffset[1]);mNestedYOffset += mScrollOffset[1];}.........if(mIsBeingDragged){mLastMotionY = y - mScrollOffset[1];final int oldY = getScrollY();final int range = getScrollRange();final int overscrollMode = ViewCompat.getOverScrollMode(this);boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||(overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&range > 0);// Calling overScrollByCompat will call onOverScrolled, which// calls onScrollChanged if applicable.//重点在这:-------->父View消耗了部分滑动距离后,子View自己开始滑动,通过overScrollByCompat//把滑动距离的参数传给mScroller进行弹性滑动if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,0, true) && !hasNestedScrollingParent()) {// Break our velocity if we hit a scroll barrier.mVelocityTracker.clear();}}......//重点在这:-------->自己滑动完之后再调用dispatchNestedScroll通知父View滑动结束if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {mLastMotionY -= mScrollOffset[1];vtev.offsetLocation(0, mScrollOffset[1]);mNestedYOffset += mScrollOffset[1];}break;
}复制代码

接下来又是父View的回调了,来看看父View的处理:

 @Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed) {final int myConsumed = moveBy(dyUnconsumed);final int myUnconsumed = dyUnconsumed - myConsumed;
}复制代码

父View在这里将最后子View滑动完后剩余的距离进行收尾处理,自我调整后第二轮的嵌套滑动也结束了。

那么再看看最后一轮滑动:

@Override
public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_UP:/* Release the drag */mIsBeingDragged = false;mActivePointerId = INVALID_POINTER;recycleVelocityTracker();if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {ViewCompat.postInvalidateOnAnimation(this);}stopNestedScroll();break;
}复制代码

在触控事件的最后一个阶段,也就是ACTION_UP时,调用stopNestedScroll(),这时会通知父View的onStopNestedScroll()来对整个系列的滑动来收尾

    public void stopNestedScroll() {if (mNestedScrollingParent != null) {ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);mNestedScrollingParent = null;}}复制代码

父类最后在自己的onStopNestedScroll()实现相关的收尾处理,比如重置滑动状态标记,完成动画操作,通知滑动结束等。这样,整个滑动嵌套流程就完成了。

最后来总结一下整个流程,分为三个步骤:

  • 步骤一:子View的ACTION_DOWN调用startNestedScroll---->父View的onStartNestedScroll判断是否要一起滑动,父ViewonNestedScrollAccepted同意协同滑动
  • 步骤二:子View的ACTION_MOVE调用dispatchNestedPreScroll---->父View的onNestedPreScroll在子View滑动之前先进行滑动并消耗需要的距离---->父View完成该次滑动之后返回消耗的距离,子View在剩下的距离中再完成自己需要的滑动
  • 步骤三:子View滑动完成之后调用dispatchNestedScroll---->父View的onNestedScroll处理父View和子View之前滑动剩余的距离
  • 步骤四:子View的ACTION_UP调用stopNestedScroll---->父View的onStopNestedScroll完成滑动收尾工作

这样,子View和父View的一系列嵌套滑动就完成了,可以看出来整个嵌套滑动还是靠子View来推动父View进行滑动的,这也解决了在传统的滑动事件中一旦事件被子View处理了就很难再分享给父View共同处理的问题,这也是嵌套滑动的一个特点。

结语

嵌套滑动作为官方推出的一套更加方便的处理滑动的工具,可以说是很大程度上减少了我们在出来这方面问题上的复杂性,当然,上面提到的仅仅是原理,真正的实现大家可以仔细地去看Design包一些控件的源码来进一步深入了解。同时,下一次还将继续分享如何用三大利器:CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout来实现携程机票首页的交互,敬请期待。

附上仿携程机票首页交互:
三分钟带你仿携程机票首页炫酷交互

干货:五分钟带你看懂NestedScrolling嵌套滑动机制相关推荐

  1. 五分钟学会python函数_五分钟带你搞懂python 迭代器与生成器

    前言 大家周末好,今天给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉得这是一种新的获取数据的方法.对于获取数据的方法而言,我们会一种就 ...

  2. 三分钟带你看懂HDMI接口的PCB设计

    三分钟带你看懂HDMI接口的PCB设计 本文主要讲解的是HDMI的设计,包括作用和运用的总结,希望大家看了以后能轻松的应对各种HDMI方案的PCB设计. 一.什么是HDMI? 高清晰度多媒体接口(英文 ...

  3. 一分钟带你看懂UML图

    一分钟带你看懂UML图 小小demo package Test;/*** @Description:* @ProjectNmae: demo1* @PackageName: Test* @ClassN ...

  4. 三分钟带你看懂prototype原型——ES6进阶

    三分钟带你看懂prototype原型--ES6进阶 1. prototype 定义 2. new 构造函数 3. 存储 4. prototype 作用 1. prototype 定义 在JS中的类的实 ...

  5. android 嵌套分组拖动_Android NestedScrolling嵌套滑动机制

    Android NestedScrolling嵌套滑动机制 最近项目要用到官网的下拉刷新SwipeRefreshLayout,它是个容器,包裹各种控件实现下拉,不像以前自己要实现事件的拦截,都是通过对 ...

  6. 【 全干货 】5 分钟带你看懂 Docker !

    欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~ 作者丨唐文广:腾讯工程师,负责无线研发部地图测试. 导语:Docker,近两年才流行起来的超轻量级虚拟机,它可以让你轻松完成持续集成.自动交付 ...

  7. 【 全干货 】5 分钟带你看懂 Docker ! 1

    作者丨唐文广:腾讯工程师,负责无线研发部地图测试. 导语:Docker,近两年才流行起来的超轻量级虚拟机,它可以让你轻松完成持续集成.自动交付.自动部署,并且实现开发环境.测试环境.运维环境三方环境的 ...

  8. 【全干货】5分钟带你看懂 Docker!

    作者丨唐文广:腾讯工程师,负责无线研发部地图测试. 导语:Docker,近两年才流行起来的超轻量级虚拟机,它可以让你轻松完成持续集成.自动交付.自动部署,并且实现开发环境.测试环境.运维环境三方环境的 ...

  9. Neurons字幕组 | 2分钟带你看懂李飞飞论文:神经网络是怎样给一幅图增加文字描述,实现“看图说话”的?(附论文下载)

    Neurons字幕组出品 翻译|智博校对|龙牧雪 时间轴|虫2后期| Halo 项目管理|大力 Neurons字幕组 第四期作品震撼来袭! Neurons字幕组源自英文单词Neuron,一个个独立的神 ...

最新文章

  1. OpenCV录制视频
  2. 【干货】史上最全的Tensorflow学习资源汇总,速藏!
  3. 多线程程序中操作的原子性--转帖
  4. 识别中文_中文场景文字识别大赛官方baseline
  5. Linux shell 上机编程-----习题
  6. 单片机---HLK-W801并口驱动ST7789
  7. IDEA 之搭建spring-boot maven报错Project ‘org.springframework.boot:spring-boot-starter-parent:2.2.0.RELEAS
  8. PBOC规范研究之十 ---标准动态数据认证(转)
  9. puzzle(1036)数邻、多米诺骨牌
  10. [Err] 1813 - Tablespace ‘`XX`.`XX`‘ exists.
  11. SLAM本质剖析-Boost
  12. 打印机耗材的发展趋势
  13. 计算机网络实验:PPP配置与分析
  14. 智力竞赛抢答器 Verilog HDL 建模
  15. Python ACCESS学习(二) 创建文件链接ACCESS数据库
  16. matlab12个简答题,Matlab 期末考试题库(共12套卷)
  17. Mysql表单自增id自定义规则,用LPAD/RPAD就够了
  18. [动态规划] 以“合唱团”问题为例 [python]
  19. Win10怎么样修复edge浏览器?如何重置edge浏览器
  20. Python-Django毕业设计安卓勤工俭学APP(程序+LW)

热门文章

  1. JAVA实现包含main函数的栈问题(《剑指offer》)
  2. 2021CCF颁奖典礼首次多平台网络直播,致敬获得者!CCF杰出工程师
  3. 英特尔又做了一个违背祖宗的决定:布局RISC-V
  4. 马斯克“口无遮拦”发推特又挨批,被指无视法院命令
  5. 李飞飞、颜宁等8位华人学者入选美国艺术与科学院院士,其中7位女性
  6. 史上最全解读 | 飞桨模型库重大升级,主流算法模型全覆盖
  7. 禁掉人脸识别!一群音乐人正在号召,禁止在音乐节上动用人脸识别
  8. LSTM之父,现已加入鬼畜全明星,“他为啥没得图灵奖,太不公平了!”
  9. 谷歌大罢工组织者离职:自曝不得不走,“遭遇秋后算账”
  10. mac git命令按tab键自动补全