1. 效果


ScrollView头部悬停_自定义view

2.分析和实现


2.1效果分析:

  1.RelativeLayout根布局中放置一个ScrollView,隐藏一个需要悬浮的一模一样的View,对ScrollView滚动进行监听,当滚动位置大于悬浮View距顶部的距离时候将其显示,否则将其隐藏;
  2.布局中放置一个固定在头部的容器,对ScrollView滚动进行监听,当滚动位置大于悬浮View距顶部的距离时候将悬浮的View从原来的布局中移除并添加到头部容器中,否则将其移回到原来的布局中

无论用哪种方式其实实现起来都挺简单的,原理也都是一样的,第一种可能在性能方面相对要好一点,只不过在每一个地方可能要多写几行代码,这里实现第二种自定义TopFloatScrollView

2.2效果实现:

  1.自定义的View继承RelativeLayout在构造函数中添加两个布局,一个是滚动的ScrollView,一个是隐藏在头部存放需要存放悬浮View的容器LinearLayout;
  2.在onFinishInflate()中首先判断一下在该自定义View中是否只包含一个孩子,将其取出来放置到ScrollView中,然后我们根据需要悬浮的ViewId找到悬浮的View,之后我们确定悬浮View在父容器中位置,在其上面再包上一层View,以免移除的时候界面会回缩;
  3.在onWindowFocusChanged()中获取悬浮View距顶部的距离,同时我们获取悬浮View的bitmap作为头部容器布局的背景,之所以这么搞是因为在快速滑动下界面会出现闪动的情况;
  4.监听自定义ScrollView的滚动,如果滚动的位置大于悬浮View到头部的距离将其添加到头部的容器布局中,如果小于了移到原来的布局中。

1.自定义TopFloatScrollView在其构造方法中添加一个ScrollView和隐藏在顶部需要临时存放需要悬浮的容器LinearLayout;

public class TopFloatScrollView extends FrameLayout implements OnScrollListener {
    private Context mContext;
    // 滚动的内容ScrollView,自定义可以监听滚动
    private MonitorScrollView mContainerSv;
    // ScrollView 的根内容
    private ViewGroup mRootView;
    // 一直在头部的容器
    private RelativeLayout mFloatTopContainer;

    public TopFloatScrollView(Context context) {
        this(context, null);
    }

    public TopFloatScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TopFloatScrollView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initView();
    }

    private void initView() {
        // 添加滚动的ScrollView
        mContainerSv = new MonitorScrollView(mContext);
        // 设置滚动监听
        mContainerSv.setOnScrollListener(this);
        FrameLayout.LayoutParams params = new LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        mContainerSv.setLayoutParams(params);
        super.addView(mContainerSv);

        // 添加一直隐藏在头部容器
        mFloatTopContainer = new RelativeLayout(mContext);
        super.addView(mFloatTopContainer);
        mFloatTopContainer.setVisibility(View.GONE);
    }

2.在onFinishInflate()中首先判断一下在该自定义View中是否只包含一个孩子,将其取出来放置到ScrollView中,然后我们根据需要悬浮的ViewId找到悬浮的View,之后我们确定悬浮View在父容器中位置,在其上面再包上一层View,以免到时候移除的时候界面会回缩;

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() > 3) {
            // 只容许在xml中配置一个ChildView
            throw new IllegalStateException(
                    "TopFloatScrollView can host only one direct child");
        }
        initInnerView();
    }

    /**
     * 初始化内部的View
     */
    private void initInnerView() {
        // 判断是不是只有一个ChildView
        int childCount = this.getChildCount();

        if (childCount == 2) {
            // 刚开始可能没有在layout中设置子View,需要在代码中动态添加
            return;
        }

        // 获取ScrollView的内容
        mRootView = (ViewGroup) this.getChildAt(2);

        if (mRootView == null) {
            return;
        }

        // 先从this里面移除
        this.removeView(mRootView);
        // 添加到ScrollView里面
        mContainerSv.addView(mRootView);

        // 获取悬浮的View
        mFloatView = mRootView.findViewById(R.id.top_float);

        if (mFloatView == null) {
            throw new RuntimeException(
                    "Can't find the float View for id is top_float");
        }

        // 找到FloatView在这个RootView中的位置
        findFloatViewPosition();
        // 把悬浮View放到新增容器中然后把新增容器放到主布局中
        initAddFloatContainerView();
    }

    /**
     * 把悬浮View放到新增容器中然后把新增容器放到主布局中
     */
    private void initAddFloatContainerView() {
        if (mFloatView != null) {
            GeneralUtil.measureView(mFloatView);

            mRootView.removeView(mFloatView);
            mAddFloatContainer.addView(mFloatView);
            mRootView.addView(mAddFloatContainer, mFloatViewPotion);

            // 设置临时高度
            mAddFloatContainer.getLayoutParams().height = mFloatView
                    .getMeasuredHeight();
        }
    }

    /**
     * 找到FloatView在这个RootView中的位置
     */
    private void findFloatViewPosition() {
        int count = mRootView.getChildCount();
        for (int position = 0; position < count; position++) {
            View childView = mRootView.getChildAt(position);
            if (childView == mFloatView) {
                mFloatViewPotion = position;
                break;
            }
        }
    }

3.在onWindowFocusChanged()中获取悬浮View距顶部的距离,同时我们获取悬浮View的bitmap作为头部容器布局的背景,之所以这么搞是因为在快速滑动下界面会出现闪动的情况;

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus && mAddFloatContainer != null && mFloatView != null) {
            // 获取需要悬浮的View到这个顶部的高度
            mFloatToTopHeight = mAddFloatContainer.getTop();

            mFloatTopContainer.getLayoutParams().height = mFloatView
                    .getMeasuredHeight();

            // 之所以要设置背景是因为快速向上滑动的时候会出现闪现的情况
            mFloatTopContainer.setBackground(BitmapUtil
                    .bitmap2Drawable(BitmapUtil.getBitmapFromView(mFloatView)));
        }
    }

4.监听自定义ScrollView的滚动,如果滚动的位置大于悬浮View到头部的距离将其添加到头部的容器布局中,如果小于了移到原来的布局中。

    // 滚动Y位置监听
    @Override
    public void onScroll(int y) {
        if (mFloatView != null) {
            if (y > mFloatToTopHeight) {
                addFloatToContainer();
            } else {
                addFloatToRootView();
            }
        }
    }

    /**
     * 添加到原来的RooView中
     */
    private void addFloatToRootView() {
        ViewParent currentParent = mFloatView.getParent();
        if (currentParent != mAddFloatContainer) {
            // 添加到原来的RooView中 我们之前有记录位置
            mFloatTopContainer.removeView(mFloatView);
            mAddFloatContainer.addView(mFloatView);
            mFloatTopContainer.setVisibility(View.GONE);
        }
    }

    /**
     * 添加悬浮到这个准备好的容器中
     */
    private void addFloatToContainer() {
        ViewParent currentParent = mFloatView.getParent();
        if (currentParent != mFloatTopContainer) {
            // 添加到容器中
            mFloatTopContainer.setVisibility(View.VISIBLE);
            mAddFloatContainer.removeAllViews();
            mFloatTopContainer.addView(mFloatView);
        }
    }

    /************************重载所有的addView()方法************************/
    ...
    ...

  使用的时候我们将需要悬浮View的id设置成我们约定的android:id=”@id/top_float”,也和ScrollView 添加布局一样只允许一个子View,否则都会抛运行时的异常