NestedScrolling机制解析(二)——NestedScrollView源码

2023-05-16

上一篇文章我们介绍了NestedScrollingParent和NestedScrollingChild接口,了解了两个接口里的方法和相互之间的调用关系。这篇我们以NestedScrollView类为例,看先嵌套滚动Parent和Child之前具体是怎么实现的。为啥用NestedScrollView呢,因为这既是一个NestedScrollingParent又是一个NestedScrollingChild,了解了整个类后就了解了整个机制了。

这里就不全部贴出了源码,后面我们一边讲解一边贴出源码。

一、类简介

还是老规矩,我们先看下Google对这个类的介绍

NestedScrollView is just like ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.

就是说NestedScrollViewScrollView类似,是一个支持滚动的控件。此外,它还同时支持作为NestedScrollingParent或者NestedScrollingChild进行嵌套滚动操作。默认是启用嵌套滚动的。

再看下继承关系

 public class NestedScrollView extends FrameLayout implements NestedScrollingParent,
          NestedScrollingChild2, ScrollingView {}

可以看到该类继承自FrameLayout,实现了NestedScrollingParentNestedScrollingChild

ScrollingView接口。所以才具有上诉的特性咯。

另外这里有个一个NestedScrollingChild2,在上篇文章已经提到了,这个其实核心和NestedScrollingChild是一样的,只是在部分方法上面多了一个type字段用于判断而已。基本上就可以直接看成NestedScrollingChild接口。

另外这里说明一下,因为这里重点是研究嵌套机制的,所以并不是所有的源码都有涉及,只介绍与嵌套相关的

二、嵌套滚动流程分析

1、总流程介绍

总的来说Parent和Child之间的相互调用遵循下面的调用关系:

 

2、具体分析

NestedScrollView是一个FrameLayout也就是一个ViewGroup,根据Android的触摸事件分发机制,一般会进入到onInterceptTouchEvent(MotionEvent ev)进行拦截判断。所以我们也就从这里作为分析的入口。

@Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          /*
           * This method JUST determines whether we want to intercept the motion.
           * If we return true, onMotionEvent will be called and we do the actual
           * scrolling there.
           */
  ​
          /*
          * Shortcut the most recurring case: the user is in the dragging
          * state and he is moving his finger.  We want to intercept this
          * motion.
          */
          final int action = ev.getAction();
          //mIsBeingDragged标识当前View是否在移动 这里的意思在移动或者移动事件都进行拦截
          if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
              return true;
          }
  ​
          switch (action & MotionEvent.ACTION_MASK) {
              case MotionEvent.ACTION_MOVE: {
                  /*
                   * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                   * whether the user has moved far enough from his original down touch.
                   */
  ​
                  /*
                  * Locally do absolute value. mLastMotionY is set to the y value
                  * of the down event.
                  */
                  final int activePointerId = mActivePointerId;
                  if (activePointerId == INVALID_POINTER) {
                      // If we don't have a valid id, the touch down wasn't on content.
                      break;
                  }
  ​
                  final int pointerIndex = ev.findPointerIndex(activePointerId);
                  if (pointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + activePointerId
                              + " in onInterceptTouchEvent");
                      break;
                  }
  ​
                  final int y = (int) ev.getY(pointerIndex);
                  final int yDiff = Math.abs(y - mLastMotionY);
                  // 是滑动事件 并且是垂直方向
                  if (yDiff > mTouchSlop
                          && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                      // 进入这里说明是自己想处理的情况了 所以设置mIsBeingDragged 用于拦截事件
                      mIsBeingDragged = true;
                      mLastMotionY = y;
                      initVelocityTrackerIfNotExists();
                      mVelocityTracker.addMovement(ev);
                      mNestedYOffset = 0;
                      final ViewParent parent = getParent();
                      // 因为自己要处理 所以叫Parent不要拦截
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                  }
                  break;
              }
  ​
              case MotionEvent.ACTION_DOWN: {
                  final int y = (int) ev.getY();
                  // 判断是否是在子控件区域
                  if (!inChild((int) ev.getX(), y)) {
                      mIsBeingDragged = false;
                      recycleVelocityTracker();
                      break;
                  }
  ​
                  /*
                   * Remember location of down touch.
                   * ACTION_DOWN always refers to pointer index 0.
                   */
                  // 记录按下的位置
                  mLastMotionY = y;
                  mActivePointerId = ev.getPointerId(0);
  ​
                  initOrResetVelocityTracker();
                  mVelocityTracker.addMovement(ev);
                  /*
                   * If being flinged and user touches the screen, initiate drag;
                   * otherwise don't. mScroller.isFinished should be false when
                   * being flinged. We need to call computeScrollOffset() first so that
                   * isFinished() is correct.
                  */
                  // 如果是在惯性滑动中的点击 交给自己处理
                  mScroller.computeScrollOffset();
                  mIsBeingDragged = !mScroller.isFinished();
                  // 这里就是开始嵌套滑动的地方了
                  startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                  break;
              }
  ​
              case MotionEvent.ACTION_CANCEL:
              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);
                  }
                  // 停止嵌套滑动((前提是要作为NestedScrollingChild))
                  stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  break;
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  break;
          }
  ​
          /*
          * The only time we want to intercept motion events is if we are in the
          * drag mode.
          */
          return mIsBeingDragged;
      }

这个方法的代码量不大,部分说明已经写在注释里了,这里总结起来主要就是做了一下几件事情:

在ACTION_DOWN中:一个是判断是否需要拦截事件,二是在合适的时候调用startNestedScroll();方法

在ACTION_MOVE中:在需要处理的情况下,将mIsBeingDragged置为true,将事件传递给自己的onTouchEvent()方法进行处理。

在ACTION_UP或者ACTION_CANCEL中:将mIsBeingDragged重置为false,然后调用stopNestedScroll()停止嵌套滑动。

这里我们看下startNestedScroll();stopNestedScroll()的实现。

@Override
      public boolean startNestedScroll(int axes, int type) {
          return mChildHelper.startNestedScroll(axes, type);
      }
  ​
  @Override
      public void stopNestedScroll() {
          mChildHelper.stopNestedScroll();
      }

可以看到,就是简单代理给了ChildHelper进行处理,根据上篇文章的解析,我们知道这两个方法的调用,最终会进入到Parent对应的onStartNestedScroll(View child, View target, int nestedScrollAxes)onStopNestedScroll(View target)方法。那我们就继续看下这两个方法的处理吧

@Override
  public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
      // 只处理垂直滑动的情况
      return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
  }
  ​
  ​
  @Override
  public void onStopNestedScroll(View target) {
      mParentHelper.onStopNestedScroll(target);
      stopNestedScroll();
  }
  onStartNestedScroll()方法中,就是判断是否是垂直滑动,是的话,返回true,表示进行处理。

onStopNestedScroll(View target)方法,代理给ParentHelper,然后继续调用stopNestedScroll();继续通知它的NestedScrollingParent(如果有的情况)停止嵌套滑动。

PS:这里有个主意的地方哦,有些人看到这里可能会觉得这不是形成死循环了么stopNestedScroll()->mChildHelper.stopNestedScroll()->onStopNestedScroll()->stopNestedScroll(),形成一个闭环了。

有这个误解的朋友是因为没有区分NestedScrollingParent和NestedSrollingChild身份。当我们调用stopNestedScroll()方法的时候,当前的NestedScrollView是必须是具有Child的有效的身份,如果是Parent这个方法没有意义的,相当于是一个空方法。

对应的onStopNestedScroll(View target)方法,是作为Parent有效身份的时候,才会被回调。对于Child身份也是没有意义的。所以其实从stopNestedScroll()stopNestedScroll()已经从一个对象(Child)到另一个对象了(Parent)的传递,不是在同一个对象里的调用,所以是不会死循环的。

后面不会再做说明,记得所有重写的NestedScrollingChild接口的方法,只有在Child身份的对象上有效,所有重写的NestedScrollingParent接口的方法,只有在Parent身份的对象上有效。

到这里,startNestedScroll和stopNestedScroll这两组流程分析就完了。那中间真正的分发流程在哪儿呢?那就是onTouchEvent()方法啊,在上面onInterceptTouchEvent(MotionEvent ev)方法里面,适当的时候,不是做了拦截操作么,那就会进入onTouchEvent()方法咯,而且如果该类的子View没有消费掉触摸事件,正常情况也会再分发到该类的onTouchEvent()方法。

这里我们就继续这个方法的分析吧

@Override
      public boolean onTouchEvent(MotionEvent ev) {
          initVelocityTrackerIfNotExists();
  ​
          MotionEvent vtev = MotionEvent.obtain(ev);
  ​
          final int actionMasked = ev.getActionMasked();
  ​
          if (actionMasked == MotionEvent.ACTION_DOWN) {
              mNestedYOffset = 0;
          }
          vtev.offsetLocation(0, mNestedYOffset);
  ​
          switch (actionMasked) {
              case MotionEvent.ACTION_DOWN: {
                  if (getChildCount() == 0) {
                      // 没有Child,那还滑动个啥啊,都撑不开布局
                      return false;
                  }
                  if ((mIsBeingDragged = !mScroller.isFinished())) {
                      final ViewParent parent = getParent();
                      // 通知父View不要拦截触摸事件
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                  }
  ​
                  /*
                   * If being flinged and user touches, stop the fling. isFinished
                   * will be false if being flinged.
                   */
                  // 如果是在惯性滑动中 停止滑动
                  if (!mScroller.isFinished()) {
                      mScroller.abortAnimation();
                  }
  ​
                  // Remember where the motion event started
                  // 记录按下的位置
                  mLastMotionY = (int) ev.getY();
                  mActivePointerId = ev.getPointerId(0);
                  // 也是开启嵌套滑动
                  startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                  break;
              }
              case MotionEvent.ACTION_MOVE:
                  final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                  if (activePointerIndex == -1) {
                      Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                      break;
                  }
  ​
                  final int y = (int) ev.getY(activePointerIndex);
                  // 手指滑动距离
                  int deltaY = mLastMotionY - y;
                  // 先分发给Parent进行预处理
                  if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                          ViewCompat.TYPE_TOUCH)) {
                      // 如果Parent消费了滑动距离 需要减去
                      deltaY -= mScrollConsumed[1];
                      vtev.offsetLocation(0, mScrollOffset[1]);
                      mNestedYOffset += mScrollOffset[1];
                  }
                  if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                      final ViewParent parent = getParent();
                      // 也是告知父View不要拦截事件
                      if (parent != null) {
                          parent.requestDisallowInterceptTouchEvent(true);
                      }
                      mIsBeingDragged = true;
                      if (deltaY > 0) {
                          deltaY -= mTouchSlop;
                      } else {
                          deltaY += mTouchSlop;
                      }
                  }
                  if (mIsBeingDragged) {
                      // Scroll to follow the motion event
                      mLastMotionY = y - mScrollOffset[1];
                  
                      final int oldY = getScrollY();
                      final int range = getScrollRange();
                      final int overscrollMode = getOverScrollMode();
                      boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                              || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
  ​
                      // Calling overScrollByCompat will call onOverScrolled, which
                      // calls onScrollChanged if applicable.
                      // 滚动自己
                      if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                              0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                          // Break our velocity if we hit a scroll barrier.
                          mVelocityTracker.clear();
                      }
  ​
                      // 重新计算未消费的距离
                      final int scrolledDeltaY = getScrollY() - oldY;
                      final int unconsumedY = deltaY - scrolledDeltaY;
                      // 分发给Parent进行嵌套滚动
                      if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                              ViewCompat.TYPE_TOUCH)) {
                          mLastMotionY -= mScrollOffset[1];
                          vtev.offsetLocation(0, mScrollOffset[1]);
                          mNestedYOffset += mScrollOffset[1];
                      } else if (canOverscroll) {
                          // 如果Parent没有消费 并且可以滚动 继续处理
                          ensureGlows();
                          final int pulledToY = oldY + deltaY;
                          if (pulledToY < 0) {
                              EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
                                      ev.getX(activePointerIndex) / getWidth());
                              if (!mEdgeGlowBottom.isFinished()) {
                                  mEdgeGlowBottom.onRelease();
                              }
                          } else if (pulledToY > range) {
                              EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
                                      1.f - ev.getX(activePointerIndex)
                                              / getWidth());
                              if (!mEdgeGlowTop.isFinished()) {
                                  mEdgeGlowTop.onRelease();
                              }
                          }
                          if (mEdgeGlowTop != null
                                  && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                              ViewCompat.postInvalidateOnAnimation(this);
                          }
                      }
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  final VelocityTracker velocityTracker = mVelocityTracker;
                  velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                  int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                  if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                      // 如果达到了惯性的速度 分发惯性滑动事件
                      flingWithNestedDispatch(-initialVelocity);
                  } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                          getScrollRange())) {
                      ViewCompat.postInvalidateOnAnimation(this);
                  }
                  mActivePointerId = INVALID_POINTER;
                  // 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  endDrag();
                  break;
              case MotionEvent.ACTION_CANCEL:
                  if (mIsBeingDragged && getChildCount() > 0) {
                      if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                              getScrollRange())) {
                          ViewCompat.postInvalidateOnAnimation(this);
                      }
                  }
                  mActivePointerId = INVALID_POINTER;
                  // 这个方法里面会调用stopNestedScroll(ViewCompat.TYPE_TOUCH);
                  endDrag();
                  break;
              case MotionEvent.ACTION_POINTER_DOWN: {
                  final int index = ev.getActionIndex();
                  mLastMotionY = (int) ev.getY(index);
                  mActivePointerId = ev.getPointerId(index);
                  break;
              }
              case MotionEvent.ACTION_POINTER_UP:
                  onSecondaryPointerUp(ev);
                  mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                  break;
          }
  ​
          if (mVelocityTracker != null) {
              mVelocityTracker.addMovement(vtev);
          }
          vtev.recycle();
          // 这里始终返回true 所以Parent作为父View其实是进不了onTouchEvent()方法的。
          return true;
      }

这个方法比较长,里面涉及到一些具体滚动操作,这不是我们本篇的重点,所以我们看关键地方就可以了。重要地方我都写了注释说明。

先看返回值,这里直接返回了true,从这里知道,Parent和Child嵌套的时候,Parent是肯定进不了该方法的。所以这里面的情况,我们只需要考虑Child身份即可。

ACTION_DOWN中

如果是惯性滑动的情况,停止滑动。同样是调用startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);开启嵌套滑动,所以Parent中的onStartNestedScroll()可能不止一次调用。但是多次调用有影响吗?没影响,在里面没有做具体滚动操作,只是做是否需要处理的判断而已。

ACTION_MOVE中

先通过调用dispatchNestedPreScroll()分发给Parent进行滚动处理。然后再通过overScrollByCompat()自己处理滚动事件,最后再计算一下未消费的距离,再通过dispatchNestedScroll()继续给Parent进行处理。同时根据返回值,判断Parent是否处理了,进行下一步操作。

这里的dispatchNestedPreScroll(),就会进入Parent的onNestedPreScroll()的方法,我们看下处理:

@Override
  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
      dispatchNestedPreScroll(dx, dy, consumed, null);
  }

可以看到其实啥也没做,就是继续给它自己的Parent(如果有的情况)分发事件

接下来再看下dispatchNestedScroll()对应的Parent的onNestedScroll()方法

@Override
      public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
              int dyUnconsumed) {
          final int oldScrollY = getScrollY();
          // 滚动自己 消费掉距离
          scrollBy(0, dyUnconsumed);
          final int myConsumed = getScrollY() - oldScrollY;
          final int myUnconsumed = dyUnconsumed - myConsumed;
          // 继续分发给上一级
          dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
      }

这里面也比较简单,就是使用scrollBy()方法,滚动自己。消费掉滚动距离。同样在自己还有Parent的情况下,继续向上分发。

到这里,ACTION_DOWN的情况,我们就介绍完了,继续看UP和CANCEL

ACTION_UP和ACTION_CANCEL中:

在这两个case中,最后都调用了endDrag()。我们看下这个方法

private void endDrag() {
          mIsBeingDragged = false;
  ​
          recycleVelocityTracker();
          // 停止嵌套滚动 
          stopNestedScroll(ViewCompat.TYPE_TOUCH);
  ​
          if (mEdgeGlowTop != null) {
              mEdgeGlowTop.onRelease();
              mEdgeGlowBottom.onRelease();
          }
      }

这里面就是调用stopNestedScroll(ViewCompat.TYPE_TOUCH);,这个方法前面已经分析了,这里不做多的说明。

回到ACTION_UP中。这里面在调用endDrag()之前,还调用了flingWithNestedDispatch()方法,看下具体实现:

 private void flingWithNestedDispatch(int velocityY) {
          final int scrollY = getScrollY();
          final boolean canFling = (scrollY > 0 || velocityY > 0)
                  && (scrollY < getScrollRange() || velocityY < 0);
          // 先给Parent看是否需要处理
          if (!dispatchNestedPreFling(0, velocityY)) {
              // 再次回调Parent,其实主要目的通过canFling参数,是告诉Parent我自己处理了
              dispatchNestedFling(0, velocityY, canFling);
              // 没有处理 自己处理
              fling(velocityY);
          }
      }
 这里面就是继续惯性滑动事件的分发而已,注释说的很清楚了,就不解释了。那我们就继续看Parent中对应的两个方法:onNestedPreScroll()和onNestedFling()
@Override
      public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
          if (!consumed) {
              flingWithNestedDispatch((int) velocityY);
              return true;
          }
          return false;
      }
  ​
      @Override
      public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
          return dispatchNestedPreFling(velocityX, velocityY);
      }

处理很简单,onNestedPreFling()直接往上一级Parent分发,onNestedFling()直接,看Child是否消费了,没有消费往上一级Parent分发。并返回true,如果已经消费了,直接返回fasle即可。

到这里,整个流程就分析完了,还是做一个简单的总结吧。

3、总结

大家再通过这张流程图(画的不好,将就看了)自己回忆和梳理一下吧。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

NestedScrolling机制解析(二)——NestedScrollView源码 的相关文章

  • ubantu虚拟机无法联网

    在VMware中安装Ubuntu虚拟机 xff0c 总会发生无法上网的情况 xff0c 主要情况有以下几点 xff1a 宿主机可以上网 xff1b 虚拟机却无法访问网页虚拟机ping不通任何网站 xff0c 用浏览器显示error 一般情况
  • 半监督语义分割论文学习记录

    Semi Supervised Semantic Segmentation with Cross Consistency Training 1 1 motivation 一致性训练的目的是在应用于输入的小扰动上增强模型预测的不变性 因此 x

随机推荐

  • 使用Qt二次开发周立功CAN(一)

    使用Qt二次开发周立功CAN xff08 一 xff09 使用Qt二次开发周立功的CAN通信 xff0c 第一步需要完成动态链接库的加载 xff0c 成功加载之后才能调用其提供的接口函数 加载库需要注意的问题有两个 xff1a 一是Qt版本
  • 字节序基础知识

    在各种计算机体系结构中 xff0c 对于字节 字等的存储机制有所不同 xff0c 因而引发了计算机通信领 域中一个很重要的问题 xff0c 即通信双方交流的信息单元 xff08 比特 字节 字 双字等等 xff09 应该以什么样的顺序进行传
  • vlc命令行: 转码 流化 推流

    写在命令行之前的话 xff1a VLC不仅仅可以通过界面进行播放 xff0c 转码 xff0c 流化 xff0c 也可以通过命令行进行播放 xff0c 转码和流化 还可以利用里面的SDK进行二次开发 vlc命令行使用方法 xff1a 1 x
  • C++ 简单实现HTTP GET/POST 请求

    HTTP 超文本传输协议 是一种客户端与服务端的传输协议 xff0c 最早用于浏览器和服务器之间的通信 xff0c 后来因为其使用灵活 方便等特点 xff0c 广泛用于客户端与服务端的通信 文章将简单介绍HTTP协议 xff0c 同时以C
  • STM32单片机HAL库下串口接收不定长数据

    xff33 xff34 xff2d xff13 xff12 单片机 xff28 xff21 xff2c 库下串口接收不定长数据 xff28 xff21 xff2c 库下的串口接收不定长数据代码配置代码实现代码演示总结 xff28 xff21
  • C++将一个数据格式化为固定长度的字符串

    经常会遇到将数据解析为文本文件的现象 xff0c 通常因为数据长度的不同导致 xff0c 可视化效果不好 写一个输入数据获取固定长度字符串的函数 xff0c 来得到一个固定长度的数据 xff0c 让格式化看起来好看一些 include lt
  • Socket原理与编程基础

    一 Socket简介 Socket是进程通讯的一种方式 xff0c 即调用这个网络库的一些API函数实现分布在不同主机的相关进程之间的数据交换 几个定义 xff1a xff08 1 xff09 IP地址 xff1a 即依照TCP IP协议分
  • mac 安装brew

    起因是这样的 xff0c 我想在mac上安装htop 然后我了解到可以用brew安装htop 然后再执行命令 brew install htop 所以我就开始吭哧吭哧安装brew 过程xue wei 曲折了一些 先是看到一个文章 xff0c
  • 【项目学习】C++实现高并发服务器——代码学习(三)用户注册登录功能

    项目来源 xff1a WebServer 上一篇 xff1a 存储解析HTTP请求报文 xff0c 创建响应报文 本文介绍以下功能的代码实现 利用RAII机制实现了数据库连接池 xff0c 减少数据库连接建立与关闭的开销 xff0c 同时实
  • 用CSS3实现动画进度条

    CSS3的新特性为我们实现漂亮的进度条扫清了障碍 xff0c 我们可以完全不需要任何图片和简单的Javascript代码就可以构建 一 第一个例子 效果图 xff1a Demo地址 xff1a http namepk sinaapp com
  • tcpdump命令使用详解

    tcpdump命令使用详解 疯狂的小企鹅的博客 CSDN博客 tcpdump命令详解全网最详细的 tcpdump 使用指南 王一白 博客园Tcpdump抓包工具实战教程 xff0c 让你知道一个抓包走天下 xff01 哔哩哔哩 bilibi
  • Chrome浏览器Postman插件安装包及安装教程

    最近电脑装了新环境 xff0c 以前本地的postman安装包竟然找不到了 xff0c 网上费尽心力找了很多资源 xff0c 终于找到纯净的安装包 xff0c 绕开套路 xff0c 现将Postman安装包及安装教程分享给各位 xff0c
  • LayoutInflater的错误用法(Avoid passing null as the view root )

    今天在练习使用Fragment的时候 xff0c 注意到在使用LayoutInflater的时候有黄色报警 xff08 Avoid passing null as the view root needed to resolve layout
  • Android M(6.0)运行时权限申请及遇到的坑

    一 概述 在对动态权限申请进行详细说明时 xff0c 还是先大致介绍下6 0后 xff0c google对权限的一个归类和划分 在Android M之前 xff0c 再开发应用的时候 xff0c 程序员只需要在AndroidManifest
  • Android DataBinding介绍(一)——简介、数据及方法事件绑定

    简介 Data binding 是Google在2015年7月发布的Android Studio v1 3 0 版本上引入的 xff0c 在2016年4月Android Studio v2 0 0 上正式支持 引入之初 xff0c 不支持双
  • CoordinatorLayout的使用(一)——简单使用

    简介 CoordinatorLayout是Android support design推出的新布局 xff0c 主要用于作为视图根布局以及协调子控件的行为 xff08 根据用户的触摸行为产生一定的动画效果 xff09 主要是通过设置子Vie
  • BottomSheetDialog的使用及注意事项

    一 BottomSheetDialog简介 用途 xff1a 底部弹起的view或dialog 实现 xff1a 其关键也是CoordinatorLayout与Behavior 要求 xff1a 采用View的形式展示的话 xff0c 用于
  • mac设置mysql的环境变量

    1 终端输入 xff1a PATH 61 34 PATH 34 usr local mysql bin 这种每次重新进入终端都得写一次 xff0c 比较麻烦 xff1b 2 改变 zshrc文件 终端输入 xff1a vim zshrc 按
  • 关于解决自定义FloatingActionButton滑动行为(Behavior)只隐藏不出现的问题

    最近在使用FloatingActionButton的时候 xff0c 自定义了其Behavior xff0c 然后发现在SDK在25及以上的时候 xff0c 出现了只能隐藏不能重新出现的问题 xff08 24及以下没有出现此问题 xff09
  • NestedScrolling机制解析(二)——NestedScrollView源码

    上一篇文章我们介绍了NestedScrollingParent和NestedScrollingChild接口 xff0c 了解了两个接口里的方法和相互之间的调用关系 这篇我们以NestedScrollView类为例 xff0c 看先嵌套滚动