Android 自定义带有粘性的Scrollview

  有时候我们会遇到这样的场景,一个TabLayout+Viewpager+Fragment,当向上滚动一个Listview或是Gridview的时候,TabLayout便“粘在顶端”不再移动。随后向下滚动的时候,TabLayout便会回到原来的位置,我们就称这个TabLayout是有粘性的。我们可以通过自定义Scrollview来实现。
  在布局文件中对要粘性的控件设置android:tag=”sticky”即可。原理就是先查找tag为“sticky”tag的控件,随后设置滑动监听获得控件的坐标,随后当到达顶端的时候,修改其高度为0,最后刷新重绘。其核心代码如下:
  

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 android.widget.ScrollView;

import com.ihavau.www.stickyscrollviewlistdemo.R;

import java.util.ArrayList;

public class StickyScrollViewList 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;
    /**
     * XKJ add for add 50dp offset of top
     */
    private static final int MIN_STICK_TOP = 0;// px
    //    private static final int MIN_STICK_TOP = 0;
    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 OnScrollChangedListener mOnScrollHandler = null;
    private IOnScrollToEnd mOnScrollToEnd = null;
    private IOnScroollToTop mOnScrollToTop = null;

    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 StickyScrollViewList(Context context) {
        this(context, null);
    }

    public StickyScrollViewList(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.scrollViewStyle);
    }

    public StickyScrollViewList(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)));

            // XKJ add TODO: remove this
            currentlyStickingView.invalidate();
        }
        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();
        if (mOnScrollHandler != null) {
            mOnScrollHandler.onScrollChanged(l, t, oldl, oldt);
        }
        int maxScroll = getChildAt(0).getHeight() - getHeight();
        if (getChildCount() > 0 && t == maxScroll) {
            if (mOnScrollToEnd != null) {
                mOnScrollToEnd.onScrollToEnd();
            }
        }
        if (getChildCount() <= 0 && t == getChildAt(0).getHeight()) {
            if (mOnScrollToTop != null) {
                mOnScrollToTop.onScroollToTop();
            }
        }
    }

    public void setOnScrollListener(OnScrollChangedListener handler) {
        mOnScrollHandler = handler;
    }

    public interface OnScrollChangedListener {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }

    public interface IOnScrollToEnd {
        public void onScrollToEnd();
    }

    public interface IOnScroollToTop {
        public void onScroollToTop();
    }

    public void setOnScrollToEndListener(IOnScrollToEnd handler) {
        mOnScrollToEnd = handler;
    }

    private void doTheStickyThing() {
        View viewThatShouldStick = null;
        View approachingView = null;
        for (View v : stickyViews) {
            int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ?
                    0 : getPaddingTop())
                    - MIN_STICK_TOP;// add 50dp

            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 ? MIN_STICK_TOP : Math.min(MIN_STICK_TOP,
                    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);
        }
    }

    /**
     * 解决viewpager在scrollview滑动冲突的问题
     */
    // 滑动距离及坐标
    private float xDistance, yDistance, xLast, yLast;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDistance = yDistance = 0f;
                xLast = ev.getX();
                yLast = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float curX = ev.getX();
                final float curY = ev.getY();

                xDistance += Math.abs(curX - xLast);
                yDistance += Math.abs(curY - yLast);
//                com.ihaveu.utils.Log.i("test", "curx:"+curX+",cury:"+curY+",xlast:"+xLast+",
// ylast:"+yLast);
//                xLast = curX;
//                yLast = curY;

                if (xDistance > yDistance) {
                    return false;
                }

        }
        return super.onInterceptTouchEvent(ev);
    }
}

代码下载地址:http://download.csdn.net/detail/zuozuoshenghen/9585463
  

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对你的问题,我可以提供一些思路和参考代码。 首先,实现 Android 中的自定义吸顶,可以采用两种方式: 1. 使用固定顶部控件 + ScrollView + ViewPager 的方式实现。 这种方式比较常见,具体实现思路如下: (1)在布局文件中定义一个固定在顶部的控件,例如 LinearLayout,将其设置为可见性为 gone,即不可见。 (2)在 ScrollView 中添加 ViewPager,将其填充满整个布局,用于滑动切换不同的子页面。 (3)监听 ScrollView 的滑动事件,在滑动到一定位置时,将顶部控件设置为可见,实现吸顶效果。 具体实现代码可以参考以下链接: - https://www.jianshu.com/p/4f28a4d0c3b1 - https://www.cnblogs.com/xiaohuafice/p/11050662.html 2. 使用 CoordinatorLayout + AppBarLayout 实现。 这种方式相对来说比较简单,具体实现思路如下: (1)在布局文件中使用 CoordinatorLayout 作为根布局,并添加一个 AppBarLayout 作为子布局。 (2)在 AppBarLayout 中添加一个 Toolbar 控件作为顶部的固定控件,将其设置为可见性为 gone。 (3)在子页面中,使用 NestedScrollView 作为滑动的容器,并将其放在 AppBarLayout 的下面。 (4)监听 NestedScrollView 的滑动事件,在滑动到一定位置时,将 Toolbar 设置为可见,实现吸顶效果。 具体实现代码可以参考以下链接: - https://www.jianshu.com/p/5d0f7e7e7c97 - https://www.jianshu.com/p/5d0f7e7e7c97 希望以上内容能够帮助到你,有什么问题可以再和我交流哦!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值