这个“StickyScrollView”实现如何将视图粘贴到ScrollView的顶部?

时间:2016-12-11 04:02:43

标签: android

通常粘性标头的工作原理是,某些可滚动数据被划分为多个部分,每个部分都有自己的标题,当您向下滚动时,后续部分的标题会替换{{1}顶部的标题。 }}

我需要的是在每个相应的部分中添加其他粘性标头。例如,如果ScrollView停留在顶部,则其第一个部分的标题 - header1 - 会卡在其下方,但是当我们到达header1a部分时,1b标头会替换1b's,但会将1a's卡在同一个地方;当我们最终到达header1部分时,2将替换上一部分中当前卡住的标题 - header2header1

这是一个header1b实现,以一维方式实现粘性标头:
https://github.com/emilsjolander/StickyScrollViewItems

ScrollView

我正在尝试做的是调整它以满足我的需求,但我已尝试在这个实现中探讨它是如何做它的功能而我无法弄清楚它是如何让视图卡住的到import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import java.util.ArrayList; /** * * @author Emil Sj�lander - sjolander.emil@gmail.com * */ public class StickyScrollView extends ScrollView { /** * 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> mStickyViews; private View mCurrentlyStickingView; private float mStickyViewTopOffset; private int mStickyViewLeftOffset; private boolean mRedirectTouchesToStickyView; private boolean mClippingToPadding; private boolean mClipToPaddingHasBeenSet; private int mShadowHeight; private Drawable mShadowDrawable; private final Runnable mInvalidateRunnable = new Runnable() { @Override public void run() { if(mCurrentlyStickingView !=null){ int l = getLeftForViewRelativeOnlyChild(mCurrentlyStickingView); int t = getBottomForViewRelativeOnlyChild(mCurrentlyStickingView); int r = getRightForViewRelativeOnlyChild(mCurrentlyStickingView); int b = (int) (getScrollY() + (mCurrentlyStickingView.getHeight() + mStickyViewTopOffset)); 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(){ mStickyViews = 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(!mClipToPaddingHasBeenSet){ mClippingToPadding = true; } notifyHierarchyChanged(); } @Override public void setClipToPadding(boolean clipToPadding) { super.setClipToPadding(clipToPadding); mClippingToPadding = clipToPadding; mClipToPaddingHasBeenSet = 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(mCurrentlyStickingView != null){ canvas.save(); canvas.translate(getPaddingLeft() + mStickyViewLeftOffset, getScrollY() + mStickyViewTopOffset + (mClippingToPadding ? getPaddingTop() : 0)); canvas.clipRect(0, (mClippingToPadding ? -mStickyViewTopOffset : 0), getWidth() - mStickyViewLeftOffset,mCurrentlyStickingView.getHeight() + mShadowHeight + 1); if (mShadowDrawable != null) { int left = 0; int right = mCurrentlyStickingView.getWidth(); int top = mCurrentlyStickingView.getHeight(); int bottom = mCurrentlyStickingView.getHeight() + mShadowHeight; mShadowDrawable.setBounds(left, top, right, bottom); mShadowDrawable.draw(canvas); } canvas.clipRect(0, (mClippingToPadding ? -mStickyViewTopOffset : 0), getWidth(), mCurrentlyStickingView.getHeight()); if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){ showView(mCurrentlyStickingView); mCurrentlyStickingView.draw(canvas); hideView(mCurrentlyStickingView); }else{ mCurrentlyStickingView.draw(canvas); } canvas.restore(); } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if(ev.getAction()==MotionEvent.ACTION_DOWN){ mRedirectTouchesToStickyView = true; } if(mRedirectTouchesToStickyView){ mRedirectTouchesToStickyView = mCurrentlyStickingView != null; if(mRedirectTouchesToStickyView){ mRedirectTouchesToStickyView = ev.getY()<=(mCurrentlyStickingView.getHeight()+ mStickyViewTopOffset) && ev.getX() >= getLeftForViewRelativeOnlyChild(mCurrentlyStickingView) && ev.getX() <= getRightForViewRelativeOnlyChild(mCurrentlyStickingView); } }else if(mCurrentlyStickingView == null){ mRedirectTouchesToStickyView = false; } if(mRedirectTouchesToStickyView){ ev.offsetLocation(0, -1*((getScrollY() + mStickyViewTopOffset) - getTopForViewRelativeOnlyChild(mCurrentlyStickingView))); } return super.dispatchTouchEvent(ev); } private boolean hasNotDoneActionDown = true; @Override public boolean onTouchEvent(MotionEvent ev) { if(mRedirectTouchesToStickyView){ ev.offsetLocation(0, ((getScrollY() + mStickyViewTopOffset) - getTopForViewRelativeOnlyChild(mCurrentlyStickingView))); } 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 approachingStickyView = null; for(View v : mStickyViews){ int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()); if(viewTop<=0){ if(viewThatShouldStick==null || viewTop>(getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()))){ viewThatShouldStick = v; } }else{ if(approachingStickyView == null || viewTop<(getTopForViewRelativeOnlyChild(approachingStickyView) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()))){ approachingStickyView = v; } } } if(viewThatShouldStick!=null){ mStickyViewTopOffset = approachingStickyView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingStickyView) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight()); if(viewThatShouldStick != mCurrentlyStickingView){ if(mCurrentlyStickingView !=null){ stopStickingCurrentlyStickingView(); } // only compute the left offset when we start sticking. mStickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick); startStickingView(viewThatShouldStick); } }else if(mCurrentlyStickingView !=null){ stopStickingCurrentlyStickingView(); } } private void startStickingView(View viewThatShouldStick) { mCurrentlyStickingView = viewThatShouldStick; if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){ hideView(mCurrentlyStickingView); } if(((String) mCurrentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)){ post(mInvalidateRunnable); } } private void stopStickingCurrentlyStickingView() { if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){ showView(mCurrentlyStickingView); } mCurrentlyStickingView = null; removeCallbacks(mInvalidateRunnable); } /** * 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(mCurrentlyStickingView !=null){ stopStickingCurrentlyStickingView(); } mStickyViews.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)){ mStickyViews.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)){ mStickyViews.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); } } } 的顶部。有谁知道这是如何工作的?

编辑:

这也是我想要应用这个概念的布局:
*请注意,ScrollView(标题1和2)是包含Headers的自定义ViewGroups(标题1a,1b,2a);这些也是自定义Sub-Headers,其中包含ViewGroups的自定义视图。

enter image description here

3 个答案:

答案 0 :(得分:5)

你正在使用的StickyScrollView只是保存一个标签,看它是否应该是粘性的,如果没有滚动视图的哪个孩子是它的标题,并根据它保持它作为一个第一个孩子观点。 如果您只想使用此StickyScrollView,则必须对其进行修改并再保留一个标记作为子标题。 我建议您使用此ScrollView,然后使用this ListView。它很容易实现,并且有效。

答案 1 :(得分:5)

这不是火箭科学。理解这一点有两个关键部分。

首先是方法doTheStickyThing。这可以找出原因。

最初的步骤是确定要坚持哪个标题。向下滚动后,您可以在滚动视图顶部的上方和下方看到视图。您希望粘贴仍位于滚动视图顶部上方的最底部标题。所以你看到很多像这样的表达式:

         getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))

结果值只是视图顶部从滚动视图顶部的偏移量。如果标题位于滚动视图的顶部上方,则该值为负数。因此,事实证明您希望具有最大偏移值的标头仍然小于或等于零。获胜视图将分配到viewThatShouldStick

现在你有一个坚持标题,你想知道哪一个标题可能会在滚动时开始推动那个标题。这会被分配到approachingView

如果接近视图正在推动标题,则必须偏移标题的顶部。该值已分配给stickyViewTopOffset

第二个关键部分是绘制标题。这是在dispatchDraw中完成的。

这是使视图看起来“卡住”的技巧:普通的渲染逻辑希望根据其当前边界将该标头放在某个位置。我们可以在该标题下方移动画布(translate),使其在滚动视图的顶部绘制,而不是通常绘制的位置。然后我们告诉视图绘制自己。在已经绘制了所有列表项视图之后会发生这种情况,因此标题似乎浮动在列表项的顶部。

当我们移动画布时,我们还必须考虑另一个接近标题开始推动这个画面的情况。裁剪处理一些关于涉及填充时应该如何看待的角落案例。

我开始修改代码以执行您想要的操作,但事情变得很复杂。

您需要跟踪三个标题:标题,子标题和接近标题,而不是跟踪两个标题。现在,您必须处理子标题的顶部偏移以及标题的顶部偏移。然后你有两个场景:首先是接近的标题是一个主标题。这将修改两个顶部偏移。但是当接近的标题是 sub 标题时,只有固定子标题的顶部偏移量受到影响,主标题偏移量保持不变。

我能得到这个,但我现在时间很短。如果我能找到时间,我将完成代码并发布。

答案 2 :(得分:4)

您可以根据自己的要求使用header-decor。在内部使用RecyclerView,因此建议使用它。检查以下gif。

中的 Double Header 部分

enter image description here

希望这会对你有所帮助。