(三十八)CoordinatorLayout 源码分析及手写 CoordinatorLayout 以及 NestedScrolling 机制
来源:互联网 发布:不用手机注册淘宝账号 编辑:程序博客网 时间:2024/06/03 17:52
版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、CoordinatorLayout
这是上一篇文章对 CoordinatorLayout 的介绍。
1.demo
MainActivity :
public class MainActivity extends AppCompatActivity { private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.btn); } @Override public boolean onTouchEvent(MotionEvent event) { button.setX(event.getX()); button.setY(event.getY()); return super.onTouchEvent(event); }}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?><android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.xiaoyue.floatingactionbutton.MainActivity"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="我是观察者 " android:gravity="center" android:minHeight="200dp" android:background="@color/colorAccent" app:layout_behavior="com.xiaoyue.floatingactionbutton.MyBehavior" /> <Button android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="被观察者" android:gravity="center" /></android.support.design.widget.CoordinatorLayout>
CoordinatorLayout 使用了监听这模式,在这里根据 app:layout_behavior 属性设置一个 Behavior ,在 Behavior 里面进行监听的处理。
MyBehavior :
public class MyBehavior extends CoordinatorLayout.Behavior { /** * 写构造方法,不写奔溃 * @param context * @param set */ public MyBehavior(Context context, AttributeSet set) { super(context,set); } /** * CoordinatorLayout 会遍历子控件,多次调用此方法 * @param parent * @param child 监听者 * @param dependency 被监听者 * @return 是否要进行监听 */ @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof Button; } /** * 进行监听事件的处理 * @param parent * @param child 监听者 * @param dependency 被监听者 * @return */ @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { child.setX(dependency.getX()); child.setY(dependency.getY()+dependency.getHeight()); return true; }}
注:MyBehavior 必须要写构造函数,否者程序崩溃。
效果:
在这里只设置了 Button 的位置为手指触摸的位置,但是 TextView 也会随着 Button 的移动而进行移动。
2.其他
通过对 CoordinatorLayout 子视图进行 Behaviors 的声明,可以向这些子视图提供许多不同的和其父视图以及和其他子视图的交互方法。View 类可以通过注释一个 DefaultBehavior 来声明一个 behavior 作为 CoordinatorLayout 子视图的系统默认behavior。
Behaviors 可以用于实现各种交互和附加布局的变化,包括图片和面板的滑动,点击会消失元素和按钮等等的行为都可以依赖于其他子视图的变化而变化。
一个 CoordinatorLayout 的子视图可能有一个 achor,该子视图的 id 必须对应任意一个 CoordinatorLayout 的后代,但是这个子视图不一定是 CoordinatorLayout 固定的子视图或者这个子视图的后代。
子视图可以通过重写 insetEdge 来描述布局如何协调该子视图。 任何子视图都将通过 dodgeInsetEdges 被适当地移动,保证相互不重叠。
二、源码分析
app:layout_behavior
我们首先从 app:layout_behavior 这个属性开始分析,这个是作用于 CoordinatorLayout 下的子类,但是是属于 CoordinatorLayout 的自定义属性。查看 CoordinatorLayout 的 generateLayoutParams 方法。
CoordinatorLayout 的 generateLayoutParams:
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); }
generateLayoutParams 方法这里返回的 LayoutParams 是 CoordinatorLayout 重写过的。查看
LayoutParams 类的构造函数。
LayoutParams 构造函数:
LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); ... //判断是否有 layout_behavior 这个属性 mBehaviorResolved = a.hasValue( R.styleable.CoordinatorLayout_Layout_layout_behavior); //存在则根据反射进行初始化一个 Behavior if (mBehaviorResolved) { mBehavior = parseBehavior(context, attrs, a.getString( R.styleable.CoordinatorLayout_Layout_layout_behavior)); } a.recycle(); if (mBehavior != null) { // If we have a Behavior, dispatch that it has been attached mBehavior.onAttachedToLayoutParams(this); } }
LayoutParams 的构造函数先判断是否有 layout_behavior 这个属性,有的话则进行 Behavior 的初始化。
接着查看 Behavior 的初始化方法 parseBehavior。
parseBehavior:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { if (TextUtils.isEmpty(name)) { return null; } final String fullName; if (name.startsWith(".")) { //是 . 开头的,默认添加上包名 // Relative to the app package. Prepend the app package name. fullName = context.getPackageName() + name; } else if (name.indexOf('.') >= 0) { //有包含 . 的话,直接取这个 name // Fully qualified package name. fullName = name; } else { //取 CoordinatorLayout 所在包下的 behavior // Assume stock behavior in this package (if we have one) fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME) ? (WIDGET_PACKAGE_NAME + '.' + name) : name; } try { Map<String, Constructor<Behavior>> constructors = sConstructors.get(); if (constructors == null) { constructors = new HashMap<>(); sConstructors.set(constructors); } Constructor<Behavior> c = constructors.get(fullName); if (c == null) { //通过反射创建 Behavior final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true, context.getClassLoader()); c = clazz.getConstructor(CONSTRUCTOR_PARAMS); c.setAccessible(true); constructors.put(fullName, c); } return c.newInstance(context, attrs); } catch (Exception e) { throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e); } }
把 Behavior 保存在各个子控件的 LayoutParams 中,在需要调用的时候,对子控件进行遍历,获取 LayoutParams 下的 Behavior,进行调用对应的方法。
三、手写 CoordinatorLayout
根据上面简单的源码分析,按照源码设置 Behavior 的原理,我们可以自己实现 CoordinatorLayout。
CoordinatorLayout:
public class CoordinatorLayout extends RelativeLayout implements ViewTreeObserver.OnGlobalLayoutListener, NestedScrollingParent { //记录最后事件处理前的坐标 public float lastX; public float lastY; public CoordinatorLayout(Context context) { super(context); } public CoordinatorLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 当布局结束的时候 */ @Override protected void onFinishInflate() { super.onFinishInflate(); //必须当前绘制完成 onFinishInflate 后设置监听 getViewTreeObserver().addOnGlobalLayoutListener(this); } /** * 当布局被改变的时候,调用这个方法 */ @Override public void onGlobalLayout() { //循环遍历子控件,调用设置了的 Behavior for(int i = 0; i < getChildCount(); i ++) { View child = getChildAt(i); //真实类型是重写了的 LayoutParams LayoutParams layoutParams= (LayoutParams) child.getLayoutParams(); if (layoutParams.mBehavior != null) { layoutParams.mBehavior.onLayoutFinish(this,child); } } } /** * 触摸事件 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //按下的时候记录坐标 lastX = event.getRawX(); lastY = event.getRawY(); break; case MotionEvent.ACTION_MOVE: onTouchMove(event); break; } return super.onTouchEvent(event); } /** * 处理 move 事件 * @param event */ private void onTouchMove(MotionEvent event) { //获取当前坐标 float moveX = event.getRawX(); float moveY = event.getRawY(); //遍历调用有 Behavior 的子控件的 mBehavior 的 onTouchMove 事件 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); LayoutParams param = (LayoutParams) child.getLayoutParams(); if (param.mBehavior != null) { param.mBehavior.onTouchMove(this, child, event, moveX, moveY, lastX, lastY); } } //重新记录上一个坐标 lastY = moveY; lastX = moveX; } /** * 滚动事件 * 一定返回 true,不能接收子控件传递上来的滚动事件 * 实现了 NestedScrolling 机制的滚动控件 * * @param child 发生滚动的子控件 * @param target 触发嵌套滚动的 view * @param nestedScrollAxes 嵌套滚动的滚动方向 * @return */ @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return true; } /** * 子 View 停止滚动 * @param child 发生滚动的子控件 */ @Override public void onStopNestedScroll(View child) { } /** * 最重要的方法,滚动时候的传递 * @param target 触发嵌套滚动的 view * @param dxConsumed 表示 target 已经消费的 x 方向的距离 * @param dyConsumed 表示 target 已经消费的 y 方向的距离 * @param dxUnconsumed 表示 x 方向剩下的滑动距离 * @param dyUnconsumed 表示 y 方向剩下的滑动距离 */ @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); LayoutParams param = (LayoutParams) child.getLayoutParams(); if (param.mBehavior != null) { param.mBehavior.onNestedScroll(target ,child, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); } } } @Override public void onNestedScrollAccepted(View child, View target, int axes) { } /** * 准备发送滚动 * @param target 触发嵌套滚动的 view * @param dx 表示 target 本次滚动产生的 x 方向的滚动总距离 * @param dy 表示 target 本次滚动产生的 y 方向的滚动总距离 * @param consumed 表示父布局要消费的滚动距离,consumed[0] 和 consumed[1] 分别表示父布局在 x 和 y 方向上消费的距离. */ @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { } /** * 可以捕获对内部子 View 的 fling 事件,如果 return true 则表示拦截掉内部子 View 的事件 * @param target * @param velocityX * @param velocityY * @return */ @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(),attrs); } public static class LayoutParams extends RelativeLayout.LayoutParams{ //记录 Behavior Behavior mBehavior; static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] { Context.class, AttributeSet.class }; public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout); mBehavior = parseBehavior(context, attrs, a.getString( R.styleable.CoordinatorLayout_layout_behavior)); a.recycle(); } public LayoutParams(int w, int h) { super(w, h); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(RelativeLayout.LayoutParams source) { super(source); } static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { if (TextUtils.isEmpty(name)) { return null; } try { final Class<Behavior> clazz = (Class<Behavior>) Class.forName(name, true, context.getClassLoader()); Constructor<Behavior> c = clazz.getConstructor(CONSTRUCTOR_PARAMS); c.setAccessible(true); return c.newInstance(context, attrs); } catch (Exception e) { throw new RuntimeException("Could not inflate Behavior subclass " + name, e); } } }}
跟源码一样的处理,自定义一个 LayoutParams,保存 Behavior。还实现了 ViewTreeObserver.OnGlobalLayoutListener 这个接口,主要是为了处理子控件的变换时候进行 Behavior 的调用,设置监听必须在视图绘制完成之后。
Behavior :
public abstract class Behavior { public Behavior(Context context, AttributeSet set) { } public void onTouchMove(View parent, View child, MotionEvent event, float x, float y, float oldx, float oldy) { } /** * 布局绘制完成 * @param parent * @param child */ public void onLayoutFinish(View parent, View child) { } //将所有的事件 类型kaolv齐全 public void onSizeChanged(View parent, View child, int w, int h, int oldw, int oldh){ } public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) { return false; } public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { return false; } public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { Log.d("cici","onStartNestedScroll"); return true; } public void onStopNestedScroll(View child) { Log.d("cici","onStopNestedScroll"); } public void onNestedScrollAccepted(View child, View target, int axes) { Log.d("cici","onNestedScrollAccepted"); } public void onNestedScroll(View scrollView, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { } public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return false; } public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; }}
Behavior 拷贝源码的,没啥好修改。
MainActivity :
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); toolbar.setTitle("CoordinatorLayout"); setSupportActionBar(toolbar); }}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?><com.xiaoyue.coodrinlayoutzx.design.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.xiaoyue.coodrinlayoutzx.MainActivity"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="50dp" android:scaleType="fitXY" android:src="@drawable/a" app:layout_behavior="com.xiaoyue.coodrinlayoutzx.ImageBehavior"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="50dp" android:background="@color/colorPrimary" app:layout_behavior="com.xiaoyue.coodrinlayoutzx.ToolbarBehavior"> </android.support.v7.widget.Toolbar> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/image"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/large_text" /> </android.support.v4.widget.NestedScrollView></com.xiaoyue.coodrinlayoutzx.design.CoordinatorLayout>
**注:**Toolbar 在这里需要放置在 ImageView 下。 CoordinatorLayout 是继承 RelativeLayout,在布局后面的会显示在上层。
ToolbarBehavior:
public class ToolbarBehavior extends Behavior { private int maxHeight = 400; public ToolbarBehavior(Context context, AttributeSet set) { super(context, set); } /** * 进行透明度的变换 * @param scrollView * @param target * @param dxConsumed * @param dyConsumed * @param dxUnconsumed * @param dyUnconsumed */ @Override public void onNestedScroll(View scrollView, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { super.onNestedScroll(scrollView, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); if (scrollView.getScrollY() > 0) { //改变透明度 ViewCompat.setAlpha(target,scrollView.getScrollY() * 1.0f / maxHeight); } else if (scrollView.getScrollY() == 0) { ViewCompat.setAlpha(target,0); } }}
ImageBehavior:
public class ImageBehavior extends Behavior { private static final String TAG = "ImageBehavior"; //能够下拉的最大值 private int maxHeight = 400; //原始高度 private int originHeight; public ImageBehavior(Context context, AttributeSet set) { super(context, set); } @Override public void onLayoutFinish(View parent, View child) { super.onLayoutFinish(parent, child); //布局结束后,获取到 ImageView 的高度 if (originHeight == 0) { originHeight = child.getHeight(); } } /** * 滚动时候的处理,在这里进行缩放 * @param scrollView 发送滚动的控件 * @param target 目标控件 * @param dxConsumed 表示 target 已经消费的 x 方向的距离 * @param dyConsumed 表示 target 已经消费的 y 方向的距离 * @param dxUnconsumed 表示 x 方向剩下的滑动距离 * @param dyUnconsumed 表示 y 方向剩下的滑动距离 */ @Override public void onNestedScroll(View scrollView, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { Log.i(TAG, "onNestedScroll: " + scrollView.getScrollY() +" dyConsumed "+ dyConsumed); if (scrollView.getScrollY() >0) { ViewGroup.LayoutParams parmas=target.getLayoutParams(); Log.i(TAG, "onNestedScroll: parmas.height "+parmas.height+" originHeight "+originHeight); parmas.height = parmas.height - Math.abs(dyConsumed); if (parmas.height < originHeight) { parmas.height = originHeight; } target.setLayoutParams(parmas); } else if (scrollView.getScrollY()== 0){ ViewGroup.LayoutParams params = target.getLayoutParams(); params.height = params.height+ Math.abs(dyUnconsumed); if(params.height>= maxHeight){ params.height =maxHeight; } target.setLayoutParams(params); } }}
这是实现的两个 Behavior,图片的最大高度写死了,这个需要比原始图片设置的高度大(单位不一致),也可以写成自定义属性进行使用。同时,这里只进行 onNestedScroll 方法的调用,其他方法的调用是一样的。
效果:
四、NestedScrolling 机制
当实现类似这种滑动效果,最外层是一个 ViewGroup,里面包含一个可滚动的子控件,ViewGroup 跟 可滚动的子控件都需要进行滚动事件的处理,如果说通过 dispatchTouchEvent 、onInterceptTouchEvent 和 onTouchEvent 进行处理,需要 ViewGroup 获取到事件,然后在通过计算判断是否要去传递给子控件,较复杂。
NestedScrolling 机制处理这种情况就比较方便。
分析
这边从 RecyclerView 开始分析,RecyclerView 最终是实现了 NestedScrollingChild 这个接口。这依然是一个事件的处理,我们查看 onTouchEvent 方法。
RecyclerView 的 onTouchEvent:
@Override public boolean onTouchEvent(MotionEvent e) { ... switch (action) { case MotionEvent.ACTION_DOWN: { mScrollPointerId = e.getPointerId(0); mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } startNestedScroll(nestedScrollAxis, TYPE_TOUCH); } break; ... }
在 onTouchEvent 方法中,ACTION_DOWN 事件的最后调用 startNestedScroll 这个方法,这个方法是实现自 NestedScrollingChild 身上的。
RecyclerView 的 startNestedScroll:
@Override public boolean startNestedScroll(int axes, int type) { return getScrollingChildHelper().startNestedScroll(axes, type); } private NestedScrollingChildHelper getScrollingChildHelper() { if (mScrollingChildHelper == null) { mScrollingChildHelper = new NestedScrollingChildHelper(this); } return mScrollingChildHelper; }
getScrollingChildHelper 获取 NestedScrollingChildHelper,这是一个事件分发辅助类,RecyclerView 的 startNestedScroll 调用了 NestedScrollingChildHelper 的 startNestedScroll 方法。
NestedScrollingChildHelper 的 startNestedScroll :
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { if (hasNestedScrollingParent(type)) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; //循环遍历父节点,调用 ViewParentCompat 的 onStartNestedScroll 和 onNestedScrollAccepted 方法 while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { //记录响应的 NestedScrollingParent2 setNestedScrollingParentForType(type, p); ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
循环遍历父节点,调用 ViewParentCompat 的 onStartNestedScroll 和 onNestedScrollAccepted 方法。
ViewParentCompat 的 onStartNestedScroll :
public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes, int type) { if (parent instanceof NestedScrollingParent2) { // First try the NestedScrollingParent2 API return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target, nestedScrollAxes, type); } else if (type == ViewCompat.TYPE_TOUCH) { // Else if the type is the default (touch), try the NestedScrollingParent API return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); } return false; }
判断父节点是否是 NestedScrollingParent2(NestedScrollingParent2 继承 NestedScrollingParent),是的话调用 NestedScrollingParent2 的 onStartNestedScroll 方法并返回返回值。这里如果返回 false 的话,NestedScrollingChildHelper 的 startNestedScroll 就不会继续往下调用,所以在上面例子中我们是直接返回 true。
接着分析 move 事件的触发。
RecyclerView 的 onTouchEvent:
@Override public boolean onTouchEvent(MotionEvent e) { ... switch (action) { case MotionEvent.ACTION_MOVE: { ... if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ... if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker != null && (dx != 0 || dy != 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break; ... }
在 ACTION_MOVE 事件的处理有两个重要的方法 dispatchNestedPreScroll 和 scrollByInternal 方法,与 startNestedScroll 方法调用是一样的套路,通过 NestedScrollingChildHelper 去调用 ViewParentCompat 身上的方法,在调用到父控件身上的方法。
dispatchNestedPreScroll----》dispatchNestedPreScroll----》ViewParentCompat.onNestedPreScrollonTouchEvent----》scrollByInternal--》dispatchNestedScroll---》ViewParentCompat.onNestedScroll
五、附
代码链接:http://download.csdn.net/download/qq_18983205/10160861
- (三十八)CoordinatorLayout 源码分析及手写 CoordinatorLayout 以及 NestedScrolling 机制
- CoordinatorLayout源码解析之从NestedScrolling说起
- CoordinatorLayout自定义Behavior&源码分析
- CoordinatorLayout与Behavior源码分析
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- [Android Design Lib]CoordinatorLayout源码分析
- uva 11997 K Smallest Sums
- 数据结构-迷宫问题(回溯法)
- Volley源码解析
- 如何在VS2010中连接MySQL和Access数据库
- struts2中struts.xml按照一定顺序排列
- (三十八)CoordinatorLayout 源码分析及手写 CoordinatorLayout 以及 NestedScrolling 机制
- 【操作系统】页式储存方式,页,页表,页表项
- selenium在指定元素上方进行鼠标悬浮
- Java反射机制(源码反射优势解析)
- 数据结构实验之查找五:平方之哈希表
- BeanUtils.copyProperties使用
- AI的伦理与道德
- 实现上移下移 置顶置底效果
- 浅谈数组