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

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

1.1 简单显示和隐藏

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

1.2 渐变效果

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

1.3 通过设置背景颜色实现

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

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"><ScrollViewandroid:id="@+id/sv_scroll_title_outer"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><RelativeLayoutandroid:id="@+id/rl_scroll_title_title"android:layout_width="match_parent"android:layout_height="45dp"android:background="@color/red_F7E6ED"android:visibility="gone"><ImageViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:src="@mipmap/ic_navigation_back_white"android:tint="@color/red"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="标题部分"android:layout_centerInParent="true"/></RelativeLayout><TextViewandroid:id="@+id/tv_scroll_title_one"android:layout_width="match_parent"android:layout_height="250dp"android:background="@color/blue_74D3FF"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/black_999999"/></LinearLayout></ScrollView><!--    标题部分--><RelativeLayoutandroid: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"><ImageViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:src="@mipmap/ic_navigation_back_white"android:tint="@color/red"/><TextViewandroid: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(范围0~1),实现布局渐变scrollListener2()
3,监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变scrollListener3()

class ScrollTitleActivity : BaseActivity(R.layout.activity_scroll_title) {override fun initData() {}private var hasMeasured = falseoverride 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.heightLogUtils.e("height=$height")//height=750hasMeasured = true}true//返回true为可用状态}}override fun onWindowFocusChanged(hasFocus: Boolean) {super.onWindowFocusChanged(hasFocus)//方法三,会重复调用,当Activity的窗口得到焦点和失去焦点时均会被调用一次,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用,不太适合处理一些复杂的业务逻辑val height = tv_scroll_title_one.heightLogUtils.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/colorrl_scroll_title_titleWhite.visibility = View.GONErl_scroll_title_title.visibility = View.VISIBLEsv_scroll_title_outer.setOnScrollChangeListener { view, i, i2, i3, i4 ->val height = rl_scroll_title_title.heightLogUtils.e("i2 = $i2 ----------- height = $height")if (i2 <= 0){LogUtils.e("gone")rl_scroll_title_titleWhite.visibility = View.GONErl_scroll_title_titleWhite.setBackgroundColor(Color.argb(0, 255, 255, 255))}else if (i2 <= height){rl_scroll_title_titleWhite.visibility = View.VISIBLEval scale = i2.toFloat() / heightval 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.VISIBLErl_scroll_title_titleWhite.setBackgroundColor(ContextCompat.getColor(this,R.color.white))}}}/*** 监听滚动,通过设置alpha,实现布局渐变*/@RequiresApi(Build.VERSION_CODES.M)private fun scrollListener2() {rl_scroll_title_titleWhite.alpha = 0frl_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.GONEsv_scroll_title_outer.setOnScrollChangeListener { p0, p1, p2, p3, p4 ->//获取rl_scroll_title_title控件的高度val height = rl_scroll_title_titleWhite.heightLogUtils.e("p2=$p2---height=$height")if (p2 <= height) {//1.监听滚动,直接设置控件的透明度来实现标题渐变rl_scroll_title_titleWhite.visibility = View.GONELogUtils.e("gone")} else {//初始进入 height 为 0if (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 控制显示和隐藏实现

  • 布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="match_parent"><ScrollViewandroid:id="@+id/sv_scroll_stick_scroll"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid: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"/><TextViewandroid: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"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/black_999999"/></LinearLayout></ScrollView><TextViewandroid: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等),* 我们需要额外处理滑动冲突,并且这种包裹方式,会使得它们的缓存模式失效。

  • 布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayoutxmlns: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.ObservableScrollViewandroid:id="@+id/sv_scroll_stick2_scroll"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><!--                头部view--><TextViewandroid: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"/>
<!--            悬浮标题--><LinearLayoutandroid:id="@+id/ll_scroll_stick2_stick"android:layout_width="match_parent"android:layout_height="wrap_content"><RelativeLayoutandroid:id="@+id/rl_scroll_stick2_stick"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid: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><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/black_999999"/></LinearLayout></com.kiwilss.xview.ui.view.scrollview.widget.ObservableScrollView><LinearLayoutandroid: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.heightval height = ll_scroll_stick2_stick.topLogUtils.e("h = $h --- top = $height")if (y > 0 && y >= height){//addviewif (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 viewif (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 折叠布局实现

  • 布局
<?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.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><com.google.android.material.appbar.CollapsingToolbarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"app:contentScrim="@color/blue_74D3FF"app:layout_scrollFlags="scroll|exitUntilCollapsed"app:titleEnabled="false"><ImageViewandroid: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><!--        悬浮标题--><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="45dp"android:background="@color/white"><TextViewandroid: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.NestedScrollViewandroid: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"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/tv_scroll_title_one"android:layout_width="match_parent"android:layout_height="250dp"android:background="@color/blue_74D3FF"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid: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”,只要加上这个就可以实现吸顶效果。

  • xml
<?xml version="1.0" encoding="utf-8"?>
<com.kiwilss.xview.ui.view.scrollview.widget.StickyScrollViewxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/tv_scroll_title_one"android:layout_width="match_parent"android:layout_height="250dp"android:background="@color/colorAccent"/><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="45dp"android:background="@color/white"android:visibility="visible"android:tag="sticky"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="第一个悬停部分"android:layout_centerInParent="true"/></RelativeLayout><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="45dp"android:background="@color/white"android:visibility="visible"android:tag="sticky"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="第二个悬停部分"android:layout_centerInParent="true"/></RelativeLayout><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/green_07C0C2"/><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="45dp"android:background="@color/white"android:visibility="visible"android:tag="sticky"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="第三个悬停部分"android:layout_centerInParent="true"/></RelativeLayout><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/red_F7E6ED"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/black_999999"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/blue_74D3FF"/><TextViewandroid:layout_width="match_parent"android:layout_height="250dp"android:background="@color/yellow_FF9B52"/><TextViewandroid: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() {@Overridepublic 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;}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);if (!clipToPaddingHasBeenSet) {clippingToPadding = true;}notifyHierarchyChanged();}@Overridepublic void setClipToPadding(boolean clipToPadding) {super.setClipToPadding(clipToPadding);clippingToPadding = clipToPadding;clipToPaddingHasBeenSet = true;}@Overridepublic void addView(View child) {super.addView(child);findStickyViews(child);}@Overridepublic void addView(View child, int index) {super.addView(child, index);findStickyViews(child);}@Overridepublic void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {super.addView(child, index, params);findStickyViews(child);}@Overridepublic void addView(View child, int width, int height) {super.addView(child, width, height);findStickyViews(child);}@Overridepublic void addView(View child, android.view.ViewGroup.LayoutParams params) {super.addView(child, params);findStickyViews(child);}@Overrideprotected 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();}}@Overridepublic 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;@Overridepublic 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);}@Overrideprotected 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悬停

ScrollView 吸顶效果相关推荐

  1. Unity实现ScrollView并包含吸顶效果

    1 前言 想在Unity做一个类似android/ios的界面,可上下滑动,可吸顶,该咋做呢? 本文就来做个demo. 先上效果: Scroll View是一个2D UI控件,新建好了,会自动包含子节 ...

  2. vue音乐项目歌手页面滚动、吸顶效果

    总结singer页面: 1.api中去获取 ['热',A-Z] 以及根据['热',A-Z]获取的所有歌手的数据 2.渲染数据 2.1 渲染左边 字母title ['热',A-Z] + 该字母开头的歌手 ...

  3. 微信小程序第六篇:元素吸顶效果实现

    系列文章传送门: 微信小程序第一篇:自定义组件详解 微信小程序第二篇:七种主流通信方法详解 微信小程序第三篇:获取页面节点信息 微信小程序第四篇:生成图片并保存到手机相册 微信小程序第五篇:页面弹出效 ...

  4. 自定义tab吸顶效果一

    PS:问题:什么是吸顶,吸顶有什么作用,吸顶怎么使用? 在很多app商城中,介绍软件的时候就会使用吸顶效果, 吸顶有很多作用,一个最简单粗暴的作用就是,让用户知道此刻在浏览哪个模块,并可以选择另外的模 ...

  5. 【使用篇】WebView 实现嵌套滑动,丝滑般实现吸顶效果,完美兼容 X5 webview

    本文首发我的公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,5 年中大厂程序员,一起建立核心竞争力 背 ...

  6. 30秒实现Vue吸顶效果

    酱酱,好久不见鸭! 前言:吸顶效果图: 1.滚动前: image.png 2.滚动中: image.png 3.滚动超过后: image.png 直观效果可参pc端微博左侧的信息栏 第一步:html ...

  7. 微信小程序中实现吸顶效果(流畅、不卡顿)

    欢迎访问我的 个人博客 最开始的时候,在小程序中实现吸顶效果,开发工具看起来还挺好的,但是在真机上就会有问题了. 原因是我不停的去 setData 会导致操作反馈延迟严重,无法及时将操作处理结果及时传 ...

  8. 最贴近京东首页体验的嵌套滑动吸顶效果

    吸顶效果是各家 App 或多或少都会用的一个交互,这种交互也常见于 PC.H5,可以说是一种通用性很强的前端交互体验,相比较来说京东首页的嵌套滑动吸顶效果是各个类似效果中体验比较好的一个,因为在嵌套布 ...

  9. vue中怎么实现吸顶效果

    在 web 应用中,我们经常需要让页面中的一个或多个元素在页面滚动时保持固定位置.这种效果通常被称为吸顶效果,因为它使元素像粘在页面顶部一样固定不动. 在 Vue 中,实现吸顶效果有不同的方法.本文将 ...

  10. Android - 吸顶效果 布局篇

    调研了一下微博和豆瓣等大体量的APP,发现内容详情页的评论吸顶效果非常常见. 以截图自豆瓣的效果为例,当上划至内容部分消失时,滑动中的回复条会置顶,并保持在位置不动. 笔者通过实践,记录下目前发现的最 ...

最新文章

  1. 联机分析处理系统与联机事务处理系统的区别和联系_混合事务分析处理“HTAP”的技术要点分析...
  2. 定制Eclipse IDE之插件篇(一)
  3. android的支付宝sdk 提示系统繁忙 请稍后再试_《活动运营中防系统扑街指南》
  4. 黑马Go语言与区块链学习笔记
  5. 页面的div中有滚动条,js实现刷新页面后回到记录时滚动条的位置
  6. hive或mysql报错Too many connections
  7. 2021年,深度学习还有哪些有潜力且处于上升期的研究方向?
  8. 拓端tecdat|R语言高维数据的主成分pca、 t-SNE算法降维与可视化分析案例报告
  9. 网络测速工具iperf使用介绍
  10. HMI报表设计与打印,标签、账单、支票、条码数据打印与出版VC++源码解决方案2018!
  11. 快压下载|快压软件官方下载
  12. 学习笔记 —— 吴恩达《机器学习》课程
  13. 幼儿园观察记录的目的和目标_幼儿园游戏观察记录
  14. 利用Visual C++ 实现QQ消息群发 ,大神实际测试,已成功发送消息
  15. html 页面换皮肤,HTML中如何实现更换网页皮肤
  16. STM8S003做无刷电机控制需要配置的选项字节
  17. HTML和CSS中的图像与背景图像
  18. linux定时删除或者压缩日志文件
  19. 2021.02.17 GDKOI2021 好题记 第一记
  20. 大数据开发工程师都需要学什么?

热门文章

  1. python股票行情接口实时获取股市数据
  2. 第五节 B-S看涨看跌期权定价
  3. IntelliJ IDEA 设置代码提示或自动补全的快捷键 (附IntelliJ IDEA常用快捷键)
  4. LibLand摄相头驱动 for Linux
  5. The Operation couldn't be completed.(LaunchServicesError error 0.) 的解决方法
  6. python实现匿名发邮件_Python 实现邮件发送功能(初级)
  7. 【oracle】函数minus
  8. JSP统计网站访问人数
  9. 光纤跳线如何区分单模和多模
  10. 移动APP开发的三种常见模式