ScrollView 吸顶效果

5,806 阅读4分钟

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

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

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"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ScrollView
        android:id="@+id/sv_scroll_title_outer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <RelativeLayout
                android:id="@+id/rl_scroll_title_title"
                android:layout_width="match_parent"
                android:layout_height="45dp"
                android:background="@color/red_F7E6ED"
                android:visibility="gone"
                >

                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:src="@mipmap/ic_navigation_back_white"
                    android:tint="@color/red"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="标题部分"
                    android:layout_centerInParent="true"/>

            </RelativeLayout>

            <TextView
                android:id="@+id/tv_scroll_title_one"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/blue_74D3FF"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/green_07C0C2"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/red_F7E6ED"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/black_999999"/>
        </LinearLayout>

    </ScrollView>

<!--    标题部分-->
    <RelativeLayout
        android:id="@+id/rl_scroll_title_titleWhite"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        app:layout_constraintTop_toTopOf="parent"
        android:background="@color/white"
        android:visibility="gone"
        >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:src="@mipmap/ic_navigation_back_white"
            android:tint="@color/red"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="标题部分"
            android:layout_centerInParent="true"/>

    </RelativeLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
    1. activity 部分,所有实现都在这里面

重点关注这三个方法就可以了。

1.监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度scrollListener() 2.监听滚动,通过设置alpha(范围01),实现布局渐变scrollListener2() 3,监听滚动,通过设置背景颜色alpha(范围0255),实现布局渐变scrollListener3()

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

    }
    private var hasMeasured = false
    override fun initEvent() {
        //onCreate中获取控件的高度,参考: https://blog.csdn.net/wangzhongshun/article/details/105196366
        //方法一
//        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")
    }



    @RequiresApi(Build.VERSION_CODES.M)
    override fun initInterface() {
        //1.监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度
        //scrollListener()
        //2.监听滚动,通过设置alpha(范围0~1),实现布局渐变
        //scrollListener2()
        //3,监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变
        //scrollListener3()
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun scrollListener3() {
        //初始进入隐藏,Color.argb转换工具https://www.wanandroid.com/tools/color
        rl_scroll_title_titleWhite.visibility = View.GONE
        rl_scroll_title_title.visibility = View.VISIBLE
        sv_scroll_title_outer.setOnScrollChangeListener { view, i, i2, i3, i4 ->
            val height = rl_scroll_title_title.height
            LogUtils.e("i2 = $i2 ----------- height = $height")
            if (i2 <= 0){
                LogUtils.e("gone")
                rl_scroll_title_titleWhite.visibility = View.GONE
                rl_scroll_title_titleWhite.setBackgroundColor(Color.argb(0, 255, 255, 255))
            }else if (i2 <= height){
                rl_scroll_title_titleWhite.visibility = View.VISIBLE
                val scale = i2.toFloat() / height
                val alpha = (scale * 255).toInt()
                LogUtils.e("scale = $scale ---- alpha = $alpha")
                rl_scroll_title_titleWhite.setBackgroundColor(Color.argb(alpha, 255, 255, 255))
            }else{
                LogUtils.e("visible")
                rl_scroll_title_titleWhite.visibility = View.VISIBLE
                rl_scroll_title_titleWhite.setBackgroundColor(ContextCompat.getColor(this,R.color.white))
            }
        }
    }

    /**
     * 监听滚动,通过设置alpha,实现布局渐变
     */
    @RequiresApi(Build.VERSION_CODES.M)
    private fun scrollListener2() {
        rl_scroll_title_titleWhite.alpha = 0f
        rl_scroll_title_titleWhite.visibility = View.VISIBLE
        //这种情况,height不会为0,不需要处理
        sv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, p4 ->
            LogUtils.e("p2=$p2")
            if (p2 <= 0) {
                rl_scroll_title_titleWhite.alpha = 0f
            } else if (p2 < rl_scroll_title_titleWhite.height) {
                //1.监听滚动,直接设置控件的透明度来实现标题渐变
                //3,根据某个控件设置滚动到某个控件时,完全不透明
                val scale = p2.toFloat() / (rl_scroll_title_titleWhite.height)
                rl_scroll_title_titleWhite.alpha = scale
            } else {
                rl_scroll_title_titleWhite.alpha = 1f
            }
        }
    }


    /**
     * 监听滚动,只控制显示和隐藏,布局初始隐藏,不用设置渐变度
     */
    @RequiresApi(Build.VERSION_CODES.M)
    private fun scrollListener() {
        //初始进入隐藏
        rl_scroll_title_titleWhite.visibility = View.GONE
        sv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, p4 ->
            //获取rl_scroll_title_title控件的高度
            val height = rl_scroll_title_titleWhite.height
            LogUtils.e("p2=$p2---height=$height")
            if (p2 <= height) {
                //1.监听滚动,直接设置控件的透明度来实现标题渐变
                rl_scroll_title_titleWhite.visibility = View.GONE
                LogUtils.e("gone")
            } else {
                //初始进入 height 为 0
                if (height == 0){
                    rl_scroll_title_titleWhite.visibility = View.INVISIBLE
                }else{
                    rl_scroll_title_titleWhite.visibility = View.VISIBLE
                }
                LogUtils.e("visible")
            }
        }
    }

    override fun initIsToolbar(): Boolean {
        return false
    }

    override fun onReload() {
    }
}

二、吸顶,悬浮标题实现

2.1 通过两个 View 控制显示和隐藏实现

iShot2020-09-1400.05.07.gif

  • 布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:id="@+id/sv_scroll_stick_scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_scroll_stick_one"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:text="上半部分内容"
                android:textColor="@color/white"
                android:gravity="center"
                android:background="@color/blue_74D3FF"/>

            <TextView
                android:id="@+id/tv_scroll_stick_stick"
                android:layout_width="match_parent"
                android:layout_height="45dp"
                android:background="@color/red"
                android:text="悬浮部分"
                android:textColor="@color/white"
                android:gravity="center"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/green_07C0C2"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/red_F7E6ED"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/black_999999"/>
        </LinearLayout>
    </ScrollView>



    <TextView
        android:id="@+id/tv_scroll_stick_stick2"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@color/red"
        android:text="悬浮部分2"
        android:textColor="@color/white"
        android:gravity="center"
        android:visibility="gone"/>


</RelativeLayout>
  • activity
class ScrollStickActivity : BaseActivity(R.layout.activity_scroll_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"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">


    <com.kiwilss.xview.ui.view.scrollview.widget.ObservableScrollView
        android:id="@+id/sv_scroll_stick2_scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

<!--                头部view-->

            <TextView
                android:id="@+id/tv_scroll_stick2_header"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:text="上半部分内容"
                android:textColor="@color/white"
                android:gravity="center"
                android:background="@color/blue_74D3FF"/>
<!--            悬浮标题-->

            <LinearLayout
                android:id="@+id/ll_scroll_stick2_stick"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <RelativeLayout
                    android:id="@+id/rl_scroll_stick2_stick"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <TextView
                        android:id="@+id/tv_scroll_stick2_stick"
                        android:layout_width="match_parent"
                        android:layout_height="45dp"
                        android:background="@color/red"
                        android:text="悬浮部分"
                        android:textColor="@color/white"
                        android:gravity="center"/>
                </RelativeLayout>

            </LinearLayout>




            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/green_07C0C2"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/red_F7E6ED"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/black_999999"/>
        </LinearLayout>

    </com.kiwilss.xview.ui.view.scrollview.widget.ObservableScrollView>

   <LinearLayout
       android:id="@+id/ll_scroll_stick2_title"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical"/>

</FrameLayout>
  • 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() {
    }
}

2.3 通过 MD 折叠布局实现

iShot2020-09-1400.10.50.gif

  • 布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:contentScrim="@color/blue_74D3FF"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:titleEnabled="false">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:src="@mipmap/wuhuang"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"/>

        </com.google.android.material.appbar.CollapsingToolbarLayout>

<!--        悬浮标题-->

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="45dp"
            android:background="@color/white">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="要悬浮的标题"
                android:layout_centerInParent="true"/>
        </RelativeLayout>
    </com.google.android.material.appbar.AppBarLayout>


    <androidx.core.widget.NestedScrollView
        android:id="@+id/nsv_scroll_stick_outer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_scroll_title_one"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/blue_74D3FF"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/green_07C0C2"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/red_F7E6ED"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/black_999999"/>
        </LinearLayout>


    </androidx.core.widget.NestedScrollView>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
  • activity,可以什么都不用做就可以实现
class NestScrollStickActivity : BaseActivity(R.layout.activity_nestscroll_stick) {

    override fun initData() {
    }

    override fun initEvent() {
    }

    override fun initInterface() {

        //滚动监听,可以直接调用
        nsv_scroll_stick_outer.setOnScrollChangeListener { v: NestedScrollView?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
            LogUtils.e("x = $scrollX --- y = $scrollY")

        }

    }


    override fun initIsToolbar(): Boolean {
        return false
    }

}

2.4 ObservableScrollView

上面用到了自定义 ScrollView 帮助实现滚动监听,可以直接使用 NestScrollView。下面是自定义 ScrollView:

public class ObservableScrollView extends ScrollView {
  
    private ScrollViewListener scrollViewListener = null;  
  
    public ObservableScrollView(Context context) {
        super(context);  
    }  
  
    public ObservableScrollView(Context context, AttributeSet attrs,
            int defStyle) {  
        super(context, attrs, defStyle);  
    }  
  
    public ObservableScrollView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    public void setScrollViewListener(ScrollViewListener scrollViewListener) {  
        this.scrollViewListener = scrollViewListener;  
    }  
  
    @Override  
    protected void onScrollChanged(int x, int y, int oldx, int oldy) {  
        super.onScrollChanged(x, y, oldx, oldy);  
        if (scrollViewListener != null) {  
            scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);  
        }  
    }  
  
} 
public interface ScrollViewListener {
  
    void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);  
  
}  

2.5 多个标题悬浮

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

iShot2020-09-1400.16.16.gif

  • xml
<?xml version="1.0" encoding="utf-8"?>
<com.kiwilss.xview.ui.view.scrollview.widget.StickyScrollView
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_scroll_title_one"
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/colorAccent"/>
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="45dp"
                android:background="@color/white"
                android:visibility="visible"
                android:tag="sticky"
                >
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="第一个悬停部分"
                    android:layout_centerInParent="true"/>

            </RelativeLayout>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="45dp"
                android:background="@color/white"
                android:visibility="visible"
                android:tag="sticky"
                >
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="第二个悬停部分"
                    android:layout_centerInParent="true"/>

            </RelativeLayout>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/green_07C0C2"/>
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="45dp"
                android:background="@color/white"
                android:visibility="visible"
                android:tag="sticky"
                >
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="第三个悬停部分"
                    android:layout_centerInParent="true"/>

            </RelativeLayout>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/red_F7E6ED"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/black_999999"/>


            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/blue_74D3FF"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/yellow_FF9B52"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:background="@color/colorPrimary"/>

        </LinearLayout>




    </LinearLayout>


</com.kiwilss.xview.ui.view.scrollview.widget.StickyScrollView>
  • StickyScrollView

public class StickyScrollView extends NestedScrollView {
 
    /**
     * Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc
     */
    public static final String STICKY_TAG = "sticky";
 
    /**
     * Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc
     */
    public static final String FLAG_NONCONSTANT = "-nonconstant";
 
    /**
     * Flag for views that have aren't fully opaque
     */
    public static final String FLAG_HASTRANSPARANCY = "-hastransparancy";
 
    /**
     * Default height of the shadow peeking out below the stuck view.
     */
    private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;
 
    private ArrayList<View> stickyViews;
    private View currentlyStickingView;
    private float stickyViewTopOffset;
    private int stickyViewLeftOffset;
    private boolean redirectTouchesToStickyView;
    private boolean clippingToPadding;
    private boolean clipToPaddingHasBeenSet;
 
    private int mShadowHeight;
    private Drawable mShadowDrawable;
 
    private final Runnable invalidateRunnable = new Runnable() {
 
        @Override
        public void run() {
            if (currentlyStickingView != null) {
                int l = getLeftForViewRelativeOnlyChild(currentlyStickingView);
                int t = getBottomForViewRelativeOnlyChild(currentlyStickingView);
                int r = getRightForViewRelativeOnlyChild(currentlyStickingView);
                int b = (int) (getScrollY() + (currentlyStickingView.getHeight() + stickyViewTopOffset));
                invalidate(l, t, r, b);
            }
            postDelayed(this, 16);
        }
    };
 
    public StickyScrollView(Context context) {
        this(context, null);
    }
 
    public StickyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.scrollViewStyle);
    }
 
    public StickyScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setup();
 
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.StickyScrollView, defStyle, 0);
 
        final float density = context.getResources().getDisplayMetrics().density;
        int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
 
        mShadowHeight = a.getDimensionPixelSize(
                R.styleable.StickyScrollView_stuckShadowHeight,
                defaultShadowHeightInPix);
 
        int shadowDrawableRes = a.getResourceId(
                R.styleable.StickyScrollView_stuckShadowDrawable, -1);
 
        if (shadowDrawableRes != -1) {
            mShadowDrawable = context.getResources().getDrawable(
                    shadowDrawableRes);
        }
 
        a.recycle();
    }
 
    /**
     * Sets the height of the shadow drawable in pixels.
     *
     * @param height
     */
    public void setShadowHeight(int height) {
        mShadowHeight = height;
    }
 
 
    public void setup() {
        stickyViews = new ArrayList<View>();
    }
 
    private int getLeftForViewRelativeOnlyChild(View v) {
        int left = v.getLeft();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            left += v.getLeft();
        }
        return left;
    }
 
    private int getTopForViewRelativeOnlyChild(View v) {
        int top = v.getTop();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            top += v.getTop();
        }
        return top;
    }
 
    private int getRightForViewRelativeOnlyChild(View v) {
        int right = v.getRight();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            right += v.getRight();
        }
        return right;
    }
 
    private int getBottomForViewRelativeOnlyChild(View v) {
        int bottom = v.getBottom();
        while (v.getParent() != getChildAt(0)) {
            v = (View) v.getParent();
            bottom += v.getBottom();
        }
        return bottom;
    }
 
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!clipToPaddingHasBeenSet) {
            clippingToPadding = true;
        }
        notifyHierarchyChanged();
    }
 
    @Override
    public void setClipToPadding(boolean clipToPadding) {
        super.setClipToPadding(clipToPadding);
        clippingToPadding = clipToPadding;
        clipToPaddingHasBeenSet = true;
    }
 
    @Override
    public void addView(View child) {
        super.addView(child);
        findStickyViews(child);
    }
 
    @Override
    public void addView(View child, int index) {
        super.addView(child, index);
        findStickyViews(child);
    }
 
    @Override
    public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        findStickyViews(child);
    }
 
    @Override
    public void addView(View child, int width, int height) {
        super.addView(child, width, height);
        findStickyViews(child);
    }
 
    @Override
    public void addView(View child, android.view.ViewGroup.LayoutParams params) {
        super.addView(child, params);
        findStickyViews(child);
    }
 
    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (currentlyStickingView != null) {
            canvas.save();
            canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() + stickyViewTopOffset + (clippingToPadding ? getPaddingTop() : 0));
 
            canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0),
                    getWidth() - stickyViewLeftOffset,
                    currentlyStickingView.getHeight() + mShadowHeight + 1);
 
            if (mShadowDrawable != null) {
                int left = 0;
                int right = currentlyStickingView.getWidth();
                int top = currentlyStickingView.getHeight();
                int bottom = currentlyStickingView.getHeight() + mShadowHeight;
                mShadowDrawable.setBounds(left, top, right, bottom);
                mShadowDrawable.draw(canvas);
            }
 
            canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(), currentlyStickingView.getHeight());
            if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
                showView(currentlyStickingView);
                currentlyStickingView.draw(canvas);
                hideView(currentlyStickingView);
            } else {
                currentlyStickingView.draw(canvas);
            }
            canvas.restore();
        }
    }
 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            redirectTouchesToStickyView = true;
        }
 
        if (redirectTouchesToStickyView) {
            redirectTouchesToStickyView = currentlyStickingView != null;
            if (redirectTouchesToStickyView) {
                redirectTouchesToStickyView =
                        ev.getY() <= (currentlyStickingView.getHeight() + stickyViewTopOffset) &&
                                ev.getX() >= getLeftForViewRelativeOnlyChild(currentlyStickingView) &&
                                ev.getX() <= getRightForViewRelativeOnlyChild(currentlyStickingView);
            }
        } else if (currentlyStickingView == null) {
            redirectTouchesToStickyView = false;
        }
        if (redirectTouchesToStickyView) {
            ev.offsetLocation(0, -1 * ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
        }
        return super.dispatchTouchEvent(ev);
    }
 
    private boolean hasNotDoneActionDown = true;
 
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (redirectTouchesToStickyView) {
            ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
        }
 
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            hasNotDoneActionDown = false;
        }
 
        if (hasNotDoneActionDown) {
            MotionEvent down = MotionEvent.obtain(ev);
            down.setAction(MotionEvent.ACTION_DOWN);
            super.onTouchEvent(down);
            hasNotDoneActionDown = false;
        }
 
        if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            hasNotDoneActionDown = true;
        }
 
        return super.onTouchEvent(ev);
    }
 
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        doTheStickyThing();
    }
 
    private void doTheStickyThing() {
        View viewThatShouldStick = null;
        View approachingView = null;
        for (View v : stickyViews) {
            int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop());
            if (viewTop <= 0) {
                if (viewThatShouldStick == null || viewTop > (getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
                    viewThatShouldStick = v;
                }
            } else {
                if (approachingView == null || viewTop < (getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))) {
                    approachingView = v;
                }
            }
        }
        if (viewThatShouldStick != null) {
            stickyViewTopOffset = approachingView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());
            if (viewThatShouldStick != currentlyStickingView) {
                if (currentlyStickingView != null) {
                    stopStickingCurrentlyStickingView();
                }
                // only compute the left offset when we start sticking.
                stickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
                startStickingView(viewThatShouldStick);
            }
        } else if (currentlyStickingView != null) {
            stopStickingCurrentlyStickingView();
        }
    }
 
    private void startStickingView(View viewThatShouldStick) {
        currentlyStickingView = viewThatShouldStick;
        if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
            hideView(currentlyStickingView);
        }
        if (((String) currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)) {
            post(invalidateRunnable);
        }
    }
 
    private void stopStickingCurrentlyStickingView() {
        if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) {
            showView(currentlyStickingView);
        }
        currentlyStickingView = null;
        removeCallbacks(invalidateRunnable);
    }
 
    /**
     * Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy
     */
    public void notifyStickyAttributeChanged() {
        notifyHierarchyChanged();
    }
 
    private void notifyHierarchyChanged() {
        if (currentlyStickingView != null) {
            stopStickingCurrentlyStickingView();
        }
        stickyViews.clear();
        findStickyViews(getChildAt(0));
        doTheStickyThing();
        invalidate();
    }
 
    private void findStickyViews(View v) {
        if (v instanceof ViewGroup) {
            ViewGroup vg = (ViewGroup) v;
            for (int i = 0; i < vg.getChildCount(); i++) {
                String tag = getStringTagForView(vg.getChildAt(i));
                if (tag != null && tag.contains(STICKY_TAG)) {
                    stickyViews.add(vg.getChildAt(i));
                } else if (vg.getChildAt(i) instanceof ViewGroup) {
                    findStickyViews(vg.getChildAt(i));
                }
            }
        } else {
            String tag = (String) v.getTag();
            if (tag != null && tag.contains(STICKY_TAG)) {
                stickyViews.add(v);
            }
        }
    }
 
    private String getStringTagForView(View v) {
        Object tagObject = v.getTag();
        return String.valueOf(tagObject);
    }
 
    private void hideView(View v) {
        if (Build.VERSION.SDK_INT >= 11) {
            v.setAlpha(0);
        } else {
            AlphaAnimation anim = new AlphaAnimation(1, 0);
            anim.setDuration(0);
            anim.setFillAfter(true);
            v.startAnimation(anim);
        }
    }
 
    private void showView(View v) {
        if (Build.VERSION.SDK_INT >= 11) {
            v.setAlpha(1);
        } else {
            AlphaAnimation anim = new AlphaAnimation(0, 1);
            anim.setDuration(0);
            anim.setFillAfter(true);
            v.startAnimation(anim);
        }
    }
}
  • attr
  <declare-styleable name="StickyScrollView">
        <attr name="stuckShadowHeight" format="dimension" />
        <attr name="stuckShadowDrawable" format="reference" />
    </declare-styleable>

三、参考

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