The top title shows and hides the gradient effect

Record a simple title gradient before the top effect.

1.1 Simple show and hide

Monitor scrolling, only control show and hide, layout is initially hidden, do not set gradient.

1.2 Gradient effect

Listen for scrolling and gradient the layout by setting alpha(range 0 to 1).

1.3 By setting the background color

Listen for scrolling and gradient the layout by setting the background color alpha(range 0 to 255).

1.4 The implementation is as follows

    1. XML layout
<? The 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 :layout_height="wrap_content" 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 :layout_width="match_parent" <RelativeLayout 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 :layout_centerInParent="true"/> </ relative > </androidx.constraintlayout.widget.ConstraintLayout>Copy the code
    1. The Activity section, all the implementation is in there

Just focus on these three methods.

1. ScrollListener () 2. ScrollListener () 2. Monitor rolling, by setting the alpha (range 0 1), layout change scrollListener2 () 3, listening to the rolling, by setting the background color alpha (range 0 255), the layout change scrollListener3 ()

class ScrollTitleActivity : BaseActivity(R.layout.activity_scroll_title) { override fun initData() { } private var hasMeasured = false override fun InitEvent () {//onCreate to get the height of the control. https://blog.csdn.net/wangzhongshun/article/details/105196366 a / / / / method tv_scroll_title_one. Post {/ / val height = // logutils. e("height=$height")//height=750 // Tv_scroll_title_one. ViewTreeObserver. AddOnPreDrawListener {/ / don't do processing have been repeated calls, call once is enough if (! hasMeasured){ val height = tv_scroll_title_one.height LogUtils.e("height=$height")//height=750 hasMeasured = true } }} Override Fun onWindowFocusChanged(hasFocus: Boolean) {super. OnWindowFocusChanged (hasFocus) / / method 3, is to be repeated calls, when the window of the Activity to get focus and loses focus will be called once, if frequently onPause and onResume, So onWindowFocusChanged will also be called frequently, Val height= tv_scroll_title_one.height logutils.e ("height=$height")} val height= tv_scroll_title_one.height logutils.e ("height=$height")} @RequiresApi(Build.VERSION_CODES.M) override fun initInterface() { //1. ScrollListener () //2 //scrollListener2() //3, scrollListener3() //scrollListener3()} @RequiresApi(Build.VERSION_CODES.M) private fun scrollListener3() { / / initial access to hidden, Color argb conversion tool https://www.wanandroid.com/tools/color rl_scroll_title_titleWhite. Visibility = the 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)) } } } /** * */ @requiresAPI (build.version_codes.m) private fun scrollListener2() { Rl_scroll_title_titlewhite. alpha = 0f rl_scroll_title_titlewhite. visibility = view. VISIBLE 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. 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() {// Start to hide rl_scroll_title_titleWhite.visibility = View.GONE sv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, P4 -> // Get 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 {// Set height to 0 initially 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() { } }Copy the code

Two, the top, suspension title realization

2.1 Through two View control display and hide implementation

  • Layout file
<? The 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 :textColor="@color/white" Android :gravity="center" 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=" @color/white" Android :background="@color/red" Android :text=" @color/white" android:visibility="gone"/> </RelativeLayout>Copy the code
  • activity
class ScrollStickActivity : BaseActivity(R.layout.activity_scroll_stick) { override fun initData() { } override fun initEvent() { } @requiresAPI (build.version_codes.m) Override fun initInterface() {// Listen for scrolling 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() { } }Copy the code

2.2 Similar to the above method, implemented through addView and removeView

The disadvantage is that * we need to deal with sliding conflicts when wrapping a View with sliding features (ListView, RecyclerView, etc.) in the content layout, and this way of wrapping makes their caching mode invalid.

  • layout
<? The 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"> <! <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=" @color/white" Android :background="@color/red" Android :text=" @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>Copy the code
  • activity
class ScrollStickActivity2 : BaseActivity(R.layout.activity_scroll_stick2) { override fun initData() { } override fun initEvent() { } override fun InitInterface () {/ / monitor rolling 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() { } }Copy the code

2.3 Realization through MD folding layout

  • layout
<? The 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 :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>Copy the code
  • Activity, can be implemented without doing anything
class NestScrollStickActivity : BaseActivity(R.layout.activity_nestscroll_stick) { override fun initData() { } override fun initEvent() { } override fun InitInterface () {/ / rolling listening, you can directly call 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 } }Copy the code

2.4 ObservableScrollView

So we’ve used a custom ScrollView to help us implement the ScrollView, so we can use NestScrollView directly. Here is a custom 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); }}}Copy the code
public interface ScrollViewListener {
  
    void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);  
  
}  
Copy the code

2.5 More than one title is suspended

Use custom View implementation, this method can satisfy a title hover and multiple title hover, the key point to use is to want to hover control on the tag attribute, Android :tag=”sticky”, as long as you add this can achieve the top effect.

  • xml
<? The 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 :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 :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 :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>Copy the code
  • 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);
        }
    }
}
Copy the code
  • attr
  <declare-styleable name="StickyScrollView">
        <attr name="stuckShadowHeight" format="dimension" />
        <attr name="stuckShadowDrawable" format="reference" />
    </declare-styleable>
Copy the code

Three, reference

Android NestedScrollView scroll to the top of the fixed child View hover