可用于商品展示的锚点定位加联动布局

演示

这个是网上找的一个项目,修复了一些bug

  1. 自定义的ScrollView会造成他子类里面包含的recycleview加载不全
  2. 从底部滑到顶部后再次跳转回混乱
  3. recycleview充当子类的时候占用父类的touch事件造成卡顿

使用方法

演示代码入口在CActivity里面,能看懂代码就不用看下面了

添加依赖
dependencies {
	api "com.android.support:design:${SUPPORT_VERSION}"
}

1.拷贝自定义widget/CustomScrollView
2.添加布局,在id为ll_top的LinearLayout里加头部
3.拷贝CActivity中四个方法,并在init中处理自己的数据
4.仿写一个AnchorView,在布局R.layout.view_anchor自定义自己的子模块界面

特别注意:
如果子模块中加了recycleview,一定要加这么一条代码
recyclerView.setNestedScrollingEnabled(false);

原理阐述(很简单)

1.自定义NestedScrollView

新建CustomScrollView继承NestedScrollView,通过回调讲两个方法暴露出去
onScrollChanged(滚动监听)
onInterceptTouchEvent(事件分发,监听它的原因是有时候CustomScrollView里面的子view会消费事件,导致CustomScrollView接受不到事件,所以在分发的时候传给CustomScrollView)
代码如下:

public class CustomScrollView extends NestedScrollView {

    public Callbacks mCallbacks;

    public CustomScrollView(Context context) {
        super(context);
    }

    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setCallbacks(Callbacks callbacks) {
        this.mCallbacks = callbacks;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mCallbacks != null) {
            mCallbacks.onScrollChanged(l, t, oldl, oldt);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mCallbacks != null) {
            mCallbacks.ontouch(ev);
        }
        return super.onInterceptTouchEvent(ev);
    }

    //定义接口用于回调
    public interface Callbacks {
        void onScrollChanged(int x, int y, int oldx, int oldy);
        void ontouch(MotionEvent ev);
    }

}
2.在布局中添加CustomScrollView

这个布局包含四部分:
头部LinearLayout
占位TabLayout
实际操作的TabLayout
底部内容的LinearLayout

这里最重要的就是两个tablayout,可以看一下他们在ScrollView滑动时的运动情况
在顶部时,两个tablayout叠加在一起


演示

滑到下面时,一个留着原地,一个跟着ScrollView移动位置,使自己保持在顶部


演示

具体布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/window_background"
    android:orientation="vertical">
    <cn.bittonet.wftpay.felonehelper.widget.CustomScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <LinearLayout
                    android:id="@+id/ll_top"
                    android:paddingBottom="10dp"
                    android:layout_width="match_parent"
                    android:layout_height="200dp"
                    android:focusable="true"
                    android:focusableInTouchMode="true"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="这里是顶部内容区域"
                        android:textSize="16sp" />

                </LinearLayout>

                <!--占位的tablayout-->
                <android.support.design.widget.TabLayout
                    android:id="@+id/tablayout_holder"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ffffff"
                    app:tabIndicatorColor="@color/colorPrimary"
                    app:tabMode="scrollable"
                    app:tabSelectedTextColor="@color/colorPrimary" />

                <LinearLayout
                    android:id="@+id/container"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    />

            </LinearLayout>


            <!--实际用户操作的tablayout-->
            <android.support.design.widget.TabLayout
                android:id="@+id/tablayout_real"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#ffffff"
                android:visibility="invisible"
                app:tabIndicatorColor="@color/colorPrimary"
                app:tabMode="scrollable"
                app:tabSelectedTextColor="@color/colorPrimary" />
        </FrameLayout>


    </cn.bittonet.wftpay.felonehelper.widget.CustomScrollView>

</LinearLayout>
3.处理滚动事件

根据滑动的距离改变tablayout的位置,这样就行实现了悬停的效果

scrollView.setCallbacks(new CustomScrollView.Callbacks() {
            @Override
            public void onScrollChanged(int x, int y, int oldx, int oldy) {
                //根据滑动的距离y(不断变化的) 和 holderTabLayout距离父布局顶部的距离(这个距离是固定的)对比,
                //当y < holderTabLayout.getTop()时,holderTabLayout 仍在屏幕内,realTabLayout不断移动holderTabLayout.getTop()距离,覆盖holderTabLayout
                //当y > holderTabLayout.getTop()时,holderTabLayout 移出,realTabLayout不断移动y,相对的停留在顶部,看上去是静止的
                int translation = Math.max(y, holderTabLayout.getTop());
                realTabLayout.setTranslationY(translation);

                if (isScroll) {
                    for (int i = tabTxt.length - 1; i >= 0; i--) {
                        //需要y减去顶部内容区域的高度
                        if (y - mLlTop.getMeasuredHeight() > anchorList.get(i).getTop() - 10) {
                            setScrollPos(i);
                            break;
                        }
                    }
                }

            }

完整代码如下:

public class CActivity extends AppCompatActivity {
    /**
     * 占位tablayout,用于滑动过程中去确定实际的tablayout的位置
     */
    private TabLayout        holderTabLayout;
    /**
     * 实际操作的tablayout,
     */
    private TabLayout        realTabLayout;
    private CustomScrollView scrollView;
    private LinearLayout     container;
    private LinearLayout     mLlTop;
    private String[] tabTxt = {"客厅", "卧室", "餐厅", "书房", "阳台", "儿童房"};

    private List<AnchorView> anchorList = new ArrayList<>();

    //判读是否是scrollview主动引起的滑动,true-是,false-否,由tablayout引起的
    private boolean isScroll;
    //记录上一次位置,防止在同一内容块里滑动 重复定位到tablayout
    private int lastPos = 0;
    //监听判断最后一个模块的高度,不满一屏时让最后一个模块撑满屏幕
    private ViewTreeObserver.OnGlobalLayoutListener listener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_c);
        holderTabLayout = findViewById(R.id.tablayout_holder);
        realTabLayout = findViewById(R.id.tablayout_real);
        scrollView = findViewById(R.id.scrollView);
        container = findViewById(R.id.container);
        mLlTop = findViewById(R.id.ll_top);
        init();
    }

    private void init() {
        for (int i = 0; i < tabTxt.length; i++) {
            AnchorView anchorView = new AnchorView(this);
            anchorView.setAnchorTxt(tabTxt[i]);
            anchorView.setContentTxt(tabTxt[i]);
            anchorList.add(anchorView);
            container.addView(anchorView);
        }
        for (int i = 0; i < tabTxt.length; i++) {
            holderTabLayout.addTab(holderTabLayout.newTab().setText(tabTxt[i]));
            realTabLayout.addTab(realTabLayout.newTab().setText(tabTxt[i]));
        }


        listener = new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //计算让最后一个view高度撑满屏幕
                int        screenH    = getScreenHeight();
                int        statusBarH = getStatusBarHeight(CActivity.this);
                int        tabH       = holderTabLayout.getHeight();
                int        lastH      = screenH - statusBarH - tabH - 16 * 3;
                AnchorView anchorView = anchorList.get(anchorList.size() - 1);
                if (anchorView.getHeight() < lastH) {
                    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                            LinearLayout.LayoutParams.MATCH_PARENT,
                            LinearLayout.LayoutParams.WRAP_CONTENT);
                    params.height = lastH;
                    anchorView.setLayoutParams(params);
                }

                //一开始让实际的tablayout 移动到 占位的tablayout处,覆盖占位的tablayout
                realTabLayout.setTranslationY(holderTabLayout.getTop());
                realTabLayout.setVisibility(View.VISIBLE);
                container.getViewTreeObserver().removeOnGlobalLayoutListener(listener);

            }
        };
        container.getViewTreeObserver().addOnGlobalLayoutListener(listener);

        //监听scrollview滑动
        scrollView.setCallbacks(new CustomScrollView.Callbacks() {
            @Override
            public void onScrollChanged(int x, int y, int oldx, int oldy) {
                //根据滑动的距离y(不断变化的) 和 holderTabLayout距离父布局顶部的距离(这个距离是固定的)对比,
                //当y < holderTabLayout.getTop()时,holderTabLayout 仍在屏幕内,realTabLayout不断移动holderTabLayout.getTop()距离,覆盖holderTabLayout
                //当y > holderTabLayout.getTop()时,holderTabLayout 移出,realTabLayout不断移动y,相对的停留在顶部,看上去是静止的
                int translation = Math.max(y, holderTabLayout.getTop());
                realTabLayout.setTranslationY(translation);

                if (isScroll) {
                    for (int i = tabTxt.length - 1; i >= 0; i--) {
                        //需要y减去顶部内容区域的高度
                        if (y - mLlTop.getMeasuredHeight() > anchorList.get(i).getTop() - 10) {
                            setScrollPos(i);
                            break;
                        }
                    }
                }

            }

            @Override
            public void ontouch(MotionEvent ev) {
                if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                    isScroll = true;
                }
            }
        });

        //实际的tablayout的点击切换
        realTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                isScroll = false;
                int pos = tab.getPosition();
                Log.d("=======pos", "" + pos);
                int top = anchorList.get(pos).getTop();
                //同样这里滑动要加上顶部内容区域的高度(这里写死的高度)
                scrollView.smoothScrollTo(0, top + mLlTop.getMeasuredHeight());
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {
                isScroll = false;
                int pos = tab.getPosition();
                int top = anchorList.get(pos).getTop();
                //同样这里滑动要加上顶部内容区域的高度
                scrollView.smoothScrollTo(0, top +mLlTop.getMeasuredHeight());
            }
        });
    }

    private void setScrollPos(int newPos) {
        if (lastPos != newPos) {
            realTabLayout.setScrollPosition(newPos, 0, true);
        }
        lastPos = newPos;
    }

    private int getScreenHeight() {
        return getResources().getDisplayMetrics().heightPixels;
    }

    public int getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources()
                                .getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }
}