一、顶部标题显示和隐藏渐变效果
1.1 简单显示和隐藏
1.2 渐变效果
1.3 通过设置背景颜色实现
监听滚动,通过设置背景颜色alpha(范围0~255),实现布局渐变。
1.4 实现方式如下
xml 布局 ``` <?xml version=”1.0” encoding=”utf-8”?>
<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>
- 2. 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.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/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() {}
}
<a name="dJkVl"></a>## 二、吸顶,悬浮标题实现<a name="skegm"></a>### 2.1 通过两个 View 控制显示和隐藏实现- 布局文件
<?xml version=”1.0” encoding=”utf-8”?>
- activity
class ScrollStickActivity : BaseActivity(R.layout.activityscroll_stick) {
override fun initData() {
}
override fun initEvent() {
}
@RequiresApi(Build.VERSION_CODES.M)
override fun initInterface() {
//监听滚动
sv_scroll_stick_scroll.setOnScrollChangeListener { view, i, i2, i3, i4 ->
if (i2 > tv_scroll_stick_one.height){
tv_scroll_stick_stick2.visibility = View.VISIBLE
}else{
tv_scroll_stick_stick2.visibility = View.GONE
}
}
}
override fun onReload() {
}
}
```
### 2.2 和上面的方法类似,通过 addView 和 removeView 实现
缺点是当包裹内容布局中带有滑动特性的View(ListView,RecyclerView等),* 我们需要额外处理滑动冲突,并且这种包裹方式,会使得它们的缓存模式失效。_
- 布局
```
<?xml version=”1.0” encoding=”utf-8”?>
</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>
- activity
class ScrollStickActivity2 : BaseActivity(R.layout.activity_scroll_stick2) {
override fun initData() {
}
override fun initEvent() {
}
override fun initInterface() {
//监听滚动
sv_scroll_stick2_scroll.setScrollViewListener { scrollView, x, y, oldx, oldy ->
val h = tv_scroll_stick2_header.height
val height = ll_scroll_stick2_stick.top
LogUtils.e(“h = $h —- top = $height”)
if (y > 0 && y >= height){
//addview
if (rl_scroll_stick2_stick.parent != ll_scroll_stick2_title) {
ll_scroll_stick2_stick.removeView(rl_scroll_stick2_stick)
ll_scroll_stick2_title.addView(rl_scroll_stick2_stick)
}
}else{
//remove view
if (rl_scroll_stick2_stick.parent != ll_scroll_stick2_stick) {
ll_scroll_stick2_title.removeView(rl_scroll_stick2_stick)
ll_scroll_stick2_stick.addView(rl_scroll_stick2_stick)
}
}
}
}
override fun onReload() {
}
}
<a name="IVJxO"></a>
### 2.3 通过 MD 折叠布局实现

- 布局
<?xml version=”1.0” encoding=”utf-8”?>
</androidx.core.widget.NestedScrollView>
- 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}
}
<a name="0Zkde"></a>### 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;}@Overrideprotected 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”?>
<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>
- 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
三、参考
Android Scrollview上滑停靠—悬浮框停靠在标题栏下方(防微博详情页)
android ScrollView 吸顶效果
Android NestedScrollView滚动到顶部固定子View悬停
