一、顶部标题显示和隐藏渐变效果

在吸顶效果前,先记录一个简单的标题渐变效果。

1.1 简单显示和隐藏

监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度。
iShot2020-09-1323.53.28.gif

1.2 渐变效果

监听滚动,通过设置alpha(范围0~1),实现布局渐变。
iShot2020-09-1323.56.38.gif

1.3 通过设置背景颜色实现

监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变。
iShot2020-09-1323.59.58.gif

1.4 实现方式如下

    1. xml 布局 ``` <?xml version=”1.0” encoding=”utf-8”?>

  1. <RelativeLayout
  2. android:id="@+id/rl_scroll_title_titleWhite"
  3. android:layout_width="match_parent"
  4. android:layout_height="45dp"
  5. app:layout_constraintTop_toTopOf="parent"
  6. android:background="@color/white"
  7. android:visibility="gone"
  8. >
  9. <ImageView
  10. android:layout_width="wrap_content"
  11. android:layout_height="match_parent"
  12. android:src="@mipmap/ic_navigation_back_white"
  13. android:tint="@color/red"/>
  14. <TextView
  15. android:layout_width="wrap_content"
  16. android:layout_height="wrap_content"
  17. android:text="标题部分"
  18. android:layout_centerInParent="true"/>
  19. </RelativeLayout>

  1. - 2. activity 部分,所有实现都在这里面
  2. 重点关注这三个方法就可以了。
  3. > 1.监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度scrollListener()
  4. > 2.监听滚动,通过设置alpha(范围0~1),实现布局渐变scrollListener2()
  5. > 3,监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变scrollListener3()

class ScrollTitleActivity : BaseActivity(R.layout.activity_scroll_title) { override fun initData() {

  1. }
  2. private var hasMeasured = false
  3. override fun initEvent() {
  4. //onCreate中获取控件的高度,参考: https://blog.csdn.net/wangzhongshun/article/details/105196366
  5. //方法一

// tv_scroll_title_one.post { // val height = tv_scroll_title_one.height // LogUtils.e(“height=$height”)//height=750 // } //方法二 tv_scroll_title_one.viewTreeObserver.addOnPreDrawListener { //不做处理会一直重复调用,调用一次就够了 if (!hasMeasured){ val height = tv_scroll_title_one.height LogUtils.e(“height=$height”)//height=750 hasMeasured = true } true//返回true为可用状态 } } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) //方法三,会重复调用,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用,不太适合处理一些复杂的业务逻辑 val height = tv_scroll_title_one.height LogUtils.e(“height=$height”) }

  1. @RequiresApi(Build.VERSION_CODES.M)
  2. override fun initInterface() {
  3. //1.监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度
  4. //scrollListener()
  5. //2.监听滚动,通过设置alpha(范围0~1),实现布局渐变
  6. //scrollListener2()
  7. //3,监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变
  8. //scrollListener3()
  9. }
  10. @RequiresApi(Build.VERSION_CODES.M)
  11. private fun scrollListener3() {
  12. //初始进入隐藏,Color.argb转换工具https://www.wanandroid.com/tools/color
  13. rl_scroll_title_titleWhite.visibility = View.GONE
  14. rl_scroll_title_title.visibility = View.VISIBLE
  15. sv_scroll_title_outer.setOnScrollChangeListener { view, i, i2, i3, i4 ->
  16. val height = rl_scroll_title_title.height
  17. LogUtils.e("i2 = $i2 ----------- height = $height")
  18. if (i2 <= 0){
  19. LogUtils.e("gone")
  20. rl_scroll_title_titleWhite.visibility = View.GONE
  21. rl_scroll_title_titleWhite.setBackgroundColor(Color.argb(0, 255, 255, 255))
  22. }else if (i2 <= height){
  23. rl_scroll_title_titleWhite.visibility = View.VISIBLE
  24. val scale = i2.toFloat() / height
  25. val alpha = (scale * 255).toInt()
  26. LogUtils.e("scale = $scale ---- alpha = $alpha")
  27. rl_scroll_title_titleWhite.setBackgroundColor(Color.argb(alpha, 255, 255, 255))
  28. }else{
  29. LogUtils.e("visible")
  30. rl_scroll_title_titleWhite.visibility = View.VISIBLE
  31. rl_scroll_title_titleWhite.setBackgroundColor(ContextCompat.getColor(this,R.color.white))
  32. }
  33. }
  34. }
  35. /**
  36. * 监听滚动,通过设置alpha,实现布局渐变
  37. */
  38. @RequiresApi(Build.VERSION_CODES.M)
  39. private fun scrollListener2() {
  40. rl_scroll_title_titleWhite.alpha = 0f
  41. rl_scroll_title_titleWhite.visibility = View.VISIBLE
  42. //这种情况,height不会为0,不需要处理
  43. sv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, p4 ->
  44. LogUtils.e("p2=$p2")
  45. if (p2 <= 0) {
  46. rl_scroll_title_titleWhite.alpha = 0f
  47. } else if (p2 < rl_scroll_title_titleWhite.height) {
  48. //1.监听滚动,直接设置控件的透明度来实现标题渐变
  49. //3,根据某个控件设置滚动到某个控件时,完全不透明
  50. val scale = p2.toFloat() / (rl_scroll_title_titleWhite.height)
  51. rl_scroll_title_titleWhite.alpha = scale
  52. } else {
  53. rl_scroll_title_titleWhite.alpha = 1f
  54. }
  55. }
  56. }
  57. /**
  58. * 监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度
  59. */
  60. @RequiresApi(Build.VERSION_CODES.M)
  61. private fun scrollListener() {
  62. //初始进入隐藏
  63. rl_scroll_title_titleWhite.visibility = View.GONE
  64. sv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, p4 ->
  65. //获取rl_scroll_title_title控件的高度
  66. val height = rl_scroll_title_titleWhite.height
  67. LogUtils.e("p2=$p2---height=$height")
  68. if (p2 <= height) {
  69. //1.监听滚动,直接设置控件的透明度来实现标题渐变
  70. rl_scroll_title_titleWhite.visibility = View.GONE
  71. LogUtils.e("gone")
  72. } else {
  73. //初始进入 height 为 0
  74. if (height == 0){
  75. rl_scroll_title_titleWhite.visibility = View.INVISIBLE
  76. }else{
  77. rl_scroll_title_titleWhite.visibility = View.VISIBLE
  78. }
  79. LogUtils.e("visible")
  80. }
  81. }
  82. }
  83. override fun initIsToolbar(): Boolean {
  84. return false
  85. }
  86. override fun onReload() {
  87. }

}

  1. <a name="dJkVl"></a>
  2. ## 二、吸顶,悬浮标题实现
  3. <a name="skegm"></a>
  4. ### 2.1 通过两个 View 控制显示和隐藏实现
  5. ![iShot2020-09-1400.05.07.gif](https://cdn.nlark.com/yuque/0/2020/gif/1624725/1600013139931-9b8d4839-1cf7-41ff-82a8-1e58cd1622aa.gif#align=left&display=inline&height=1016&margin=%5Bobject%20Object%5D&name=iShot2020-09-1400.05.07.gif&originHeight=1016&originWidth=614&size=2955058&status=done&style=none&width=614)
  6. - 布局文件

<?xml version=”1.0” encoding=”utf-8”?>

- activity class ScrollStickActivity : BaseActivity(R.layout.activityscroll_stick) { override fun initData() { } override fun initEvent() { } @RequiresApi(Build.VERSION_CODES.M) override fun initInterface() { //监听滚动 sv_scroll_stick_scroll.setOnScrollChangeListener { view, i, i2, i3, i4 -> if (i2 > tv_scroll_stick_one.height){ tv_scroll_stick_stick2.visibility = View.VISIBLE }else{ tv_scroll_stick_stick2.visibility = View.GONE } } } override fun onReload() { } } ``` ### 2.2 和上面的方法类似,通过 addView 和 removeView 实现 缺点是当包裹内容布局中带有滑动特性的View(ListView,RecyclerView等),* 我们需要额外处理滑动冲突,并且这种包裹方式,会使得它们的缓存模式失效。_
iShot2020-09-1400.07.54.gif - 布局 ``` <?xml version=”1.0” encoding=”utf-8”?>
  1. </LinearLayout>
  2. <TextView
  3. android:layout_width="match_parent"
  4. android:layout_height="250dp"
  5. android:background="@color/yellow_FF9B52"/>
  6. <TextView
  7. android:layout_width="match_parent"
  8. android:layout_height="250dp"
  9. android:background="@color/green_07C0C2"/>
  10. <TextView
  11. android:layout_width="match_parent"
  12. android:layout_height="250dp"
  13. android:background="@color/red_F7E6ED"/>
  14. <TextView
  15. android:layout_width="match_parent"
  16. android:layout_height="250dp"
  17. android:background="@color/black_999999"/>
  18. </LinearLayout>
  19. </com.kiwilss.xview.ui.view.scrollview.widget.ObservableScrollView>
- activity class ScrollStickActivity2 : BaseActivity(R.layout.activity_scroll_stick2) { override fun initData() { } override fun initEvent() { } override fun initInterface() { //监听滚动 sv_scroll_stick2_scroll.setScrollViewListener { scrollView, x, y, oldx, oldy -> val h = tv_scroll_stick2_header.height val height = ll_scroll_stick2_stick.top LogUtils.e(“h = $h —- top = $height”) if (y > 0 && y >= height){ //addview if (rl_scroll_stick2_stick.parent != ll_scroll_stick2_title) { ll_scroll_stick2_stick.removeView(rl_scroll_stick2_stick) ll_scroll_stick2_title.addView(rl_scroll_stick2_stick) } }else{ //remove view if (rl_scroll_stick2_stick.parent != ll_scroll_stick2_stick) { ll_scroll_stick2_title.removeView(rl_scroll_stick2_stick) ll_scroll_stick2_stick.addView(rl_scroll_stick2_stick) } } } } override fun onReload() { } } <a name="IVJxO"></a> ### 2.3 通过 MD 折叠布局实现 ![iShot2020-09-1400.10.50.gif](https://cdn.nlark.com/yuque/0/2020/gif/1624725/1600013469832-98d69ed0-7860-48fe-bbf3-1c36c918eccb.gif#align=left&display=inline&height=1048&margin=%5Bobject%20Object%5D&name=iShot2020-09-1400.10.50.gif&originHeight=1048&originWidth=622&size=6804567&status=done&style=none&width=622) - 布局 <?xml version=”1.0” encoding=”utf-8”?>
  1. </androidx.core.widget.NestedScrollView>

  1. - activity,可以什么都不用做就可以实现

class NestScrollStickActivity : BaseActivity(R.layout.activity_nestscroll_stick) {

  1. override fun initData() {
  2. }
  3. override fun initEvent() {
  4. }
  5. override fun initInterface() {
  6. //滚动监听,可以直接调用
  7. nsv_scroll_stick_outer.setOnScrollChangeListener { v: NestedScrollView?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
  8. LogUtils.e("x = $scrollX --- y = $scrollY")
  9. }
  10. }
  11. override fun initIsToolbar(): Boolean {
  12. return false
  13. }

}

  1. <a name="0Zkde"></a>
  2. ### 2.4 ObservableScrollView
  3. 上面用到了自定义 ScrollView 帮助实现滚动监听,可以直接使用 NestScrollView。下面是自定义 ScrollView:

public class ObservableScrollView extends ScrollView {

  1. private ScrollViewListener scrollViewListener = null;
  2. public ObservableScrollView(Context context) {
  3. super(context);
  4. }
  5. public ObservableScrollView(Context context, AttributeSet attrs,
  6. int defStyle) {
  7. super(context, attrs, defStyle);
  8. }
  9. public ObservableScrollView(Context context, AttributeSet attrs) {
  10. super(context, attrs);
  11. }
  12. public void setScrollViewListener(ScrollViewListener scrollViewListener) {
  13. this.scrollViewListener = scrollViewListener;
  14. }
  15. @Override
  16. protected void onScrollChanged(int x, int y, int oldx, int oldy) {
  17. super.onScrollChanged(x, y, oldx, oldy);
  18. if (scrollViewListener != null) {
  19. scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
  20. }
  21. }

}

  1. ```
  2. public interface ScrollViewListener {
  3. void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);
  4. }

2.5 多个标题悬浮

使用自定义 View 实现,这个方法可以满足一个标题悬浮和多个标题悬浮,使用的关键点在于在想要悬浮的控件上加上 tag 属性,android:tag=”sticky”,只要加上这个就可以实现吸顶效果。

iShot2020-09-1400.16.16.gif

  • xml ``` <?xml version=”1.0” encoding=”utf-8”?>

    1. <LinearLayout
    2. android:layout_width="match_parent"
    3. android:layout_height="wrap_content"
    4. android:orientation="vertical">
    5. <TextView
    6. android:id="@+id/tv_scroll_title_one"
    7. android:layout_width="match_parent"
    8. android:layout_height="250dp"
    9. android:background="@color/colorAccent"/>
    10. <RelativeLayout
    11. android:layout_width="match_parent"
    12. android:layout_height="45dp"
    13. android:background="@color/white"
    14. android:visibility="visible"
    15. android:tag="sticky"
    16. >
    17. <TextView
    18. android:layout_width="wrap_content"
    19. android:layout_height="wrap_content"
    20. android:text="第一个悬停部分"
    21. android:layout_centerInParent="true"/>
    22. </RelativeLayout>
    23. <TextView
    24. android:layout_width="match_parent"
    25. android:layout_height="250dp"
    26. android:background="@color/yellow_FF9B52"/>
    27. <RelativeLayout
    28. android:layout_width="match_parent"
    29. android:layout_height="45dp"
    30. android:background="@color/white"
    31. android:visibility="visible"
    32. android:tag="sticky"
    33. >
    34. <TextView
    35. android:layout_width="wrap_content"
    36. android:layout_height="wrap_content"
    37. android:text="第二个悬停部分"
    38. android:layout_centerInParent="true"/>
    39. </RelativeLayout>
    40. <TextView
    41. android:layout_width="match_parent"
    42. android:layout_height="250dp"
    43. android:background="@color/green_07C0C2"/>
    44. <RelativeLayout
    45. android:layout_width="match_parent"
    46. android:layout_height="45dp"
    47. android:background="@color/white"
    48. android:visibility="visible"
    49. android:tag="sticky"
    50. >
    51. <TextView
    52. android:layout_width="wrap_content"
    53. android:layout_height="wrap_content"
    54. android:text="第三个悬停部分"
    55. android:layout_centerInParent="true"/>
    56. </RelativeLayout>
    57. <TextView
    58. android:layout_width="match_parent"
    59. android:layout_height="250dp"
    60. android:background="@color/red_F7E6ED"/>
    61. <TextView
    62. android:layout_width="match_parent"
    63. android:layout_height="250dp"
    64. android:background="@color/black_999999"/>
  1. <TextView
  2. android:layout_width="match_parent"
  3. android:layout_height="250dp"
  4. android:background="@color/blue_74D3FF"/>
  5. <TextView
  6. android:layout_width="match_parent"
  7. android:layout_height="250dp"
  8. android:background="@color/yellow_FF9B52"/>
  9. <TextView
  10. android:layout_width="match_parent"
  11. android:layout_height="250dp"
  12. android:background="@color/colorPrimary"/>
  13. </LinearLayout>
  14. </LinearLayout>

  1. - StickyScrollView

public class StickyScrollView extends NestedScrollView {

  1. /**
  2. * Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc
  3. */
  4. public static final String STICKY_TAG = "sticky";
  5. /**
  6. * Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc
  7. */
  8. public static final String FLAG_NONCONSTANT = "-nonconstant";
  9. /**
  10. * Flag for views that have aren't fully opaque
  11. */
  12. public static final String FLAG_HASTRANSPARANCY = "-hastransparancy";
  13. /**
  14. * Default height of the shadow peeking out below the stuck view.
  15. */
  16. private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;
  17. private ArrayList<View> stickyViews;
  18. private View currentlyStickingView;
  19. private float stickyViewTopOffset;
  20. private int stickyViewLeftOffset;
  21. private boolean redirectTouchesToStickyView;
  22. private boolean clippingToPadding;
  23. private boolean clipToPaddingHasBeenSet;
  24. private int mShadowHeight;
  25. private Drawable mShadowDrawable;
  26. private final Runnable invalidateRunnable = new Runnable() {
  27. @Override
  28. public void run() {
  29. if (currentlyStickingView != null) {
  30. int l = getLeftForViewRelativeOnlyChild(currentlyStickingView);
  31. int t = getBottomForViewRelativeOnlyChild(currentlyStickingView);
  32. int r = getRightForViewRelativeOnlyChild(currentlyStickingView);
  33. int b = (int) (getScrollY() + (currentlyStickingView.getHeight() + stickyViewTopOffset));
  34. invalidate(l, t, r, b);
  35. }
  36. postDelayed(this, 16);
  37. }
  38. };
  39. public StickyScrollView(Context context) {
  40. this(context, null);
  41. }
  42. public StickyScrollView(Context context, AttributeSet attrs) {
  43. this(context, attrs, android.R.attr.scrollViewStyle);
  44. }
  45. public StickyScrollView(Context context, AttributeSet attrs, int defStyle) {
  46. super(context, attrs, defStyle);
  47. setup();
  48. TypedArray a = context.obtainStyledAttributes(attrs,
  49. R.styleable.StickyScrollView, defStyle, 0);
  50. final float density = context.getResources().getDisplayMetrics().density;
  51. int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
  52. mShadowHeight = a.getDimensionPixelSize(
  53. R.styleable.StickyScrollView_stuckShadowHeight,
  54. defaultShadowHeightInPix);
  55. int shadowDrawableRes = a.getResourceId(
  56. R.styleable.StickyScrollView_stuckShadowDrawable, -1);
  57. if (shadowDrawableRes != -1) {
  58. mShadowDrawable = context.getResources().getDrawable(
  59. shadowDrawableRes);
  60. }
  61. a.recycle();
  62. }
  63. /**
  64. * Sets the height of the shadow drawable in pixels.
  65. *
  66. * @param height
  67. */
  68. public void setShadowHeight(int height) {
  69. mShadowHeight = height;
  70. }
  71. public void setup() {
  72. stickyViews = new ArrayList<View>();
  73. }
  74. private int getLeftForViewRelativeOnlyChild(View v) {
  75. int left = v.getLeft();
  76. while (v.getParent() != getChildAt(0)) {
  77. v = (View) v.getParent();
  78. left += v.getLeft();
  79. }
  80. return left;
  81. }
  82. private int getTopForViewRelativeOnlyChild(View v) {
  83. int top = v.getTop();
  84. while (v.getParent() != getChildAt(0)) {
  85. v = (View) v.getParent();
  86. top += v.getTop();
  87. }
  88. return top;
  89. }
  90. private int getRightForViewRelativeOnlyChild(View v) {
  91. int right = v.getRight();
  92. while (v.getParent() != getChildAt(0)) {
  93. v = (View) v.getParent();
  94. right += v.getRight();
  95. }
  96. return right;
  97. }
  98. private int getBottomForViewRelativeOnlyChild(View v) {
  99. int bottom = v.getBottom();
  100. while (v.getParent() != getChildAt(0)) {
  101. v = (View) v.getParent();
  102. bottom += v.getBottom();
  103. }
  104. return bottom;
  105. }
  106. @Override
  107. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  108. super.onLayout(changed, l, t, r, b);
  109. if (!clipToPaddingHasBeenSet) {
  110. clippingToPadding = true;
  111. }
  112. notifyHierarchyChanged();
  113. }
  114. @Override
  115. public void setClipToPadding(boolean clipToPadding) {
  116. super.setClipToPadding(clipToPadding);
  117. clippingToPadding = clipToPadding;
  118. clipToPaddingHasBeenSet = true;
  119. }
  120. @Override
  121. public void addView(View child) {
  122. super.addView(child);
  123. findStickyViews(child);
  124. }
  125. @Override
  126. public void addView(View child, int index) {
  127. super.addView(child, index);
  128. findStickyViews(child);
  129. }
  130. @Override
  131. public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
  132. super.addView(child, index, params);
  133. findStickyViews(child);
  134. }
  135. @Override
  136. public void addView(View child, int width, int height) {
  137. super.addView(child, width, height);
  138. findStickyViews(child);
  139. }
  140. @Override
  141. public void addView(View child, android.view.ViewGroup.LayoutParams params) {
  142. super.addView(child, params);
  143. findStickyViews(child);
  144. }
  145. @Override
  146. protected void dispatchDraw(Canvas canvas) {
  147. super.dispatchDraw(canvas);
  148. if (currentlyStickingView != null) {
  149. canvas.save();
  150. canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() + stickyViewTopOffset + (clippingToPadding ? getPaddingTop() : 0));
  151. canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0),
  152. getWidth() - stickyViewLeftOffset,
  153. currentlyStickingView.getHeight() + mShadowHeight + 1);
  154. if (mShadowDrawable != null) {
  155. int left = 0;
  156. int right = currentlyStickingView.getWidth();
  157. int top = currentlyStickingView.getHeight();
  158. int bottom = currentlyStickingView.getHeight() + mShadowHeight;
  159. mShadowDrawable.setBounds(left, top, right, bottom);
  160. mShadowDrawable.draw(canvas);
  161. }
  162. canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(), currentlyStickingView.getHeight());
  163. if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
  164. showView(currentlyStickingView);
  165. currentlyStickingView.draw(canvas);
  166. hideView(currentlyStickingView);
  167. } else {
  168. currentlyStickingView.draw(canvas);
  169. }
  170. canvas.restore();
  171. }
  172. }
  173. @Override
  174. public boolean dispatchTouchEvent(MotionEvent ev) {
  175. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
  176. redirectTouchesToStickyView = true;
  177. }
  178. if (redirectTouchesToStickyView) {
  179. redirectTouchesToStickyView = currentlyStickingView != null;
  180. if (redirectTouchesToStickyView) {
  181. redirectTouchesToStickyView =
  182. ev.getY() <= (currentlyStickingView.getHeight() + stickyViewTopOffset) &&
  183. ev.getX() >= getLeftForViewRelativeOnlyChild(currentlyStickingView) &&
  184. ev.getX() <= getRightForViewRelativeOnlyChild(currentlyStickingView);
  185. }
  186. } else if (currentlyStickingView == null) {
  187. redirectTouchesToStickyView = false;
  188. }
  189. if (redirectTouchesToStickyView) {
  190. ev.offsetLocation(0, -1 * ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
  191. }
  192. return super.dispatchTouchEvent(ev);
  193. }
  194. private boolean hasNotDoneActionDown = true;
  195. @Override
  196. public boolean onTouchEvent(MotionEvent ev) {
  197. if (redirectTouchesToStickyView) {
  198. ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
  199. }
  200. if (ev.getAction() == MotionEvent.ACTION_DOWN) {
  201. hasNotDoneActionDown = false;
  202. }
  203. if (hasNotDoneActionDown) {
  204. MotionEvent down = MotionEvent.obtain(ev);
  205. down.setAction(MotionEvent.ACTION_DOWN);
  206. super.onTouchEvent(down);
  207. hasNotDoneActionDown = false;
  208. }
  209. if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
  210. hasNotDoneActionDown = true;
  211. }
  212. return super.onTouchEvent(ev);
  213. }
  214. @Override
  215. protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  216. super.onScrollChanged(l, t, oldl, oldt);
  217. doTheStickyThing();
  218. }
  219. private void doTheStickyThing() {
  220. View viewThatShouldStick = null;
  221. View approachingView = null;
  222. for (View v : stickyViews) {
  223. int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop());
  224. if (viewTop <= 0) {
  225. if (viewThatShouldStick == null || viewTop > (getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
  226. viewThatShouldStick = v;
  227. }
  228. } else {
  229. if (approachingView == null || viewTop < (getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
  230. approachingView = v;
  231. }
  232. }
  233. }
  234. if (viewThatShouldStick != null) {
  235. stickyViewTopOffset = approachingView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());
  236. if (viewThatShouldStick != currentlyStickingView) {
  237. if (currentlyStickingView != null) {
  238. stopStickingCurrentlyStickingView();
  239. }
  240. // only compute the left offset when we start sticking.
  241. stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
  242. startStickingView(viewThatShouldStick);
  243. }
  244. } else if (currentlyStickingView != null) {
  245. stopStickingCurrentlyStickingView();
  246. }
  247. }
  248. private void startStickingView(View viewThatShouldStick) {
  249. currentlyStickingView = viewThatShouldStick;
  250. if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
  251. hideView(currentlyStickingView);
  252. }
  253. if (((String) currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)) {
  254. post(invalidateRunnable);
  255. }
  256. }
  257. private void stopStickingCurrentlyStickingView() {
  258. if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
  259. showView(currentlyStickingView);
  260. }
  261. currentlyStickingView = null;
  262. removeCallbacks(invalidateRunnable);
  263. }
  264. /**
  265. * Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy
  266. */
  267. public void notifyStickyAttributeChanged() {
  268. notifyHierarchyChanged();
  269. }
  270. private void notifyHierarchyChanged() {
  271. if (currentlyStickingView != null) {
  272. stopStickingCurrentlyStickingView();
  273. }
  274. stickyViews.clear();
  275. findStickyViews(getChildAt(0));
  276. doTheStickyThing();
  277. invalidate();
  278. }
  279. private void findStickyViews(View v) {
  280. if (v instanceof ViewGroup) {
  281. ViewGroup vg = (ViewGroup) v;
  282. for (int i = 0; i < vg.getChildCount(); i++) {
  283. String tag = getStringTagForView(vg.getChildAt(i));
  284. if (tag != null && tag.contains(STICKY_TAG)) {
  285. stickyViews.add(vg.getChildAt(i));
  286. } else if (vg.getChildAt(i) instanceof ViewGroup) {
  287. findStickyViews(vg.getChildAt(i));
  288. }
  289. }
  290. } else {
  291. String tag = (String) v.getTag();
  292. if (tag != null && tag.contains(STICKY_TAG)) {
  293. stickyViews.add(v);
  294. }
  295. }
  296. }
  297. private String getStringTagForView(View v) {
  298. Object tagObject = v.getTag();
  299. return String.valueOf(tagObject);
  300. }
  301. private void hideView(View v) {
  302. if (Build.VERSION.SDK_INT >= 11) {
  303. v.setAlpha(0);
  304. } else {
  305. AlphaAnimation anim = new AlphaAnimation(1, 0);
  306. anim.setDuration(0);
  307. anim.setFillAfter(true);
  308. v.startAnimation(anim);
  309. }
  310. }
  311. private void showView(View v) {
  312. if (Build.VERSION.SDK_INT >= 11) {
  313. v.setAlpha(1);
  314. } else {
  315. AlphaAnimation anim = new AlphaAnimation(0, 1);
  316. anim.setDuration(0);
  317. anim.setFillAfter(true);
  318. v.startAnimation(anim);
  319. }
  320. }

}

  1. - attr

```

三、参考

Android Scrollview上滑停靠—悬浮框停靠在标题栏下方(防微博详情页)
android ScrollView 吸顶效果
Android NestedScrollView滚动到顶部固定子View悬停