您现在的位置是:首页 > 文章详情

Android NestedScrollView滚动到顶部固定子View悬停挂靠粘在顶端

日期:2018-06-14点击:345
Android NestedScrollView滚动到顶部固定子View悬停挂靠粘在顶端

网上有一个StickyScrollView,称之为粘性ScrollView,比如一个垂直方向的布局,依次摆放几个子View,当某一个子View滚到到顶端时候要停靠在顶部,悬停在顶部的位置不动。这很像联系人的pinned,假设联系人按照Z分组后,若滚动到顶部,Z就悬停靠在顶端不动,下面到的联系人继续滚到。当用户下拉时候,又恢复正常。如:
初始化阶段:

滚到使blog到顶端,然后此时姓名被顶走了:

StickyScrollView原先是继承自ScrollView,然而现在ScrollView已经被新型的NestedScrollView逐渐取代了,现在我把它改写成继承自NestedScrollView后,整理出来:
package zhangphil.test; import java.util.ArrayList; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.widget.NestedScrollView; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; 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); } } }

同时需要在res/values/attr.xml加入定义的属性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="StickyScrollView"> <attr name="stuckShadowHeight" format="dimension" /> <attr name="stuckShadowDrawable" format="reference" /> </declare-styleable> </resources>

使用起来很简单,在xml布局中把某一个子View增加一个属性:

android:tag="sticky"
即可实现该子View在NestedScrollView中,当滚动到顶部后悬停挂靠在顶部不动,直到下一个粘性子View滚动上来把它顶飞,或者用户下拉,拉出上面的View。如xml代码:
<?xml version="1.0" encoding="utf-8"?> <zhangphil.test.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="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_red_light" android:tag="sticky" android:text="姓名" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="280dp" android:background="@android:color/holo_blue_bright" android:gravity="center" android:text="zhang phil" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_red_light" android:tag="sticky" android:text="blog" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="280dp" android:background="@android:color/holo_orange_light" android:gravity="center" android:text="zhang phil csdn blog" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_red_light" android:tag="sticky" android:text="链接" android:textColor="@android:color/white" /> <TextView android:layout_width="match_parent" android:layout_height="280dp" android:background="@android:color/holo_purple" android:gravity="center" android:text="http://blog.csdn.net/zhangphil" android:textColor="@android:color/white" /> </LinearLayout> </zhangphil.test.StickyScrollView>
原文链接:https://yq.aliyun.com/articles/615639
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章