最近项目里有这种需求,滑动控件里嵌套Recyclerview,最后项目上线的运行效果:
关键代码已经抽离成一个库:
dependencies { //your dependencies ... compile 'com.linklink.views:ScrollviewWithinRecyclerviewAndFloatView:1.0.2' }
demo效果:
界面结构:
整个界面是Fragment,里面填充子Fragment:
使用示例:
1,添加依赖:
dependencies { // your dependencies ... compile 'com.linklink.views:ScrollviewWithinRecyclerviewAndFloatView:1.0.2' }
2,MainActivity.java:
package linklink.com.demo; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Fragment fragment=new MyFragment(); if(fragment!=null){ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commitAllowingStateLoss(); } } }
3,MyFragment.java,实现CustomMainFragment里的抽象方法:
package linklink.com.demo; import android.view.LayoutInflater; import android.view.View; import java.util.ArrayList; import linklink.com.scrollview_within_recyclerview.base.CustomBaseFragment2; import linklink.com.scrollview_within_recyclerview.ui.*; import linklink.com.scrollview_within_recyclerview.utils.DensityUtil; /** * MyFragment * 责任人: Chuck * 修改人: Chuck * 创建/修改时间: 2018/6/25 16:43 * Copyright : 2017-2018 深圳令令科技有限公司-版权所有 **/ public class MyFragment extends CustomMainFragment { @Override public int getTitleBackgroundRes(){ //设置透明控件的背景资源. return R.drawable.shape_blue_rect; } @Override public View getTitleView(){ //设置顶部的title布局 return LayoutInflater.from(getActivity()).inflate(R.layout.title, null); } @Override public int getTitleViewParentHeight(){ //设置getTitleView()的父容器的高度值.返回title的实际高度 加上 getTitleViewMarginTop()就行 return DensityUtil.dp2px(getActivity(),50) + getTitleViewMarginTop();//50是title布局里写死的50dp } @Override public int getTitleViewMarginTop() { //设置TitleView的MarginTop,单位:像素.因为有些界面可能包含了顶部的状态栏. // 包含了状态栏,这个方法返回你获取的状态栏高度 // 不包含状态栏,这个方法直接返回0 return DensityUtil.dp2px(getActivity(),20); } @Override public View getHeadView(){ //这里可以返回一个左右滑动的banner.滑动事件的分发逻辑已经处理过 return LayoutInflater.from(getActivity()).inflate(R.layout.banner, null); } @Override public View getFloatView(){ //设置悬浮控件,如果需要与viewpager绑定,可以定义一个成员变量,然后重写onActivityCreated,添加绑定逻辑 return LayoutInflater.from(getActivity()).inflate(R.layout.float_view, null); } @Override public ArrayList<CustomBaseFragment2> getSubFragments(){ //在viewpager里添加子碎片.CustomBaseFragment2 ArrayList<CustomBaseFragment2> list =new ArrayList<>(); SubFragment1 subFragment1=new SubFragment1(); list.add(subFragment1); SubFragment2 subFragment2=new SubFragment2(); list.add(subFragment2); return list; } }
3,SubFragment.java,实现CustomBaseFragment2的方法:
package linklink.com.demo; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.ArrayList; import linklink.com.scrollview_within_recyclerview.R; import linklink.com.scrollview_within_recyclerview.base.CustomBaseFragment2; import linklink.com.scrollview_within_recyclerview.utils.LogUtil; /** * SubFragment1 * 责任人: Chuck * 修改人: Chuck * 创建/修改时间: 2018/6/25 14:39 * Copyright : 2017-2018 深圳令令科技有限公司-版权所有 **/ public class SubFragment1 extends CustomBaseFragment2 { private static String TAG = "SubFragment1"; protected RecyclerView mRecyclerView;//可以改为自己的带刷新的控件.也可以是Listview private ArrayList<String> mDatas; //滑到顶部, 这个方法在控件还原初始状态时会调用,可以不重写 public void setSelection(int position){ try { if(position==0){ mRecyclerView.smoothScrollToPosition(position); } } catch (Exception e) { e.printStackTrace(); } } //第一个item(包含headview)是否在顶部 public boolean isHeadviewAtTopNow(){ LogUtil.i(TAG,"mRecyclerView:"+mRecyclerView); View view = mRecyclerView.getChildAt(0); LogUtil.i(TAG,"view:"+view); if(mRecyclerView!=null&&view!=null){ int[] outLocation=new int[2]; view.getLocationOnScreen(outLocation); int[] outLocation2=new int[2]; mRecyclerView.getLocationOnScreen(outLocation2); LogUtil.i(TAG,"第一个view在屏幕上的绝对位置,outLocation[1]:"+outLocation[1]); LogUtil.i(TAG,"recyclerview在屏幕上的绝对位置,outLocation2[1]:"+outLocation2[1]); //return mRecyclerViewHeadView.getTop()==0; return outLocation[1]==outLocation2[1]; } else{ LogUtil.i(TAG,"mRecyclerViewHeadView==null,return false"); return false; } } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { TAG=this.getClass().getSimpleName(); return inflater.inflate(R.layout.fragment_sub, container, false); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initData(); mRecyclerView = (RecyclerView) getView().findViewById(R.id.id_recyclerview); //设置布局管理器 mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); //设置adapter HomeAdapter adapter = new HomeAdapter(); mRecyclerView.setAdapter(adapter); //设置Item增加、移除动画 mRecyclerView.setItemAnimator(new DefaultItemAnimator()); //添加分割线 mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(),DividerItemDecoration.HORIZONTAL)); } private class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> { //生成holder @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { MyViewHolder holder = new MyViewHolder(LayoutInflater.from( getActivity()).inflate(R.layout.item, parent, false)); return holder; } //绑定holder @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.tv.setText(mDatas.get(position)); } @Override public int getItemCount() { return mDatas.size(); } class MyViewHolder extends RecyclerView.ViewHolder { TextView tv; public MyViewHolder(View view) { super(view); tv = (TextView) view.findViewById(R.id.id_num); } } } protected void initData() { mDatas = new ArrayList<String>(); for (int i = 'A'; i <= 'z'; i++) { mDatas.add(getFragmentMark() + (char) i); } } //为了界面上区分子页,加个前缀 protected String getFragmentMark(){ return "碎片1_ "; } }
实现方式:
1,顶部的事件分发控件MyDispatchRelativeLayout2,重写了onInterceptTouchEvent,上下滑动,则拦截,自己处理掉事件:
package linklink.com.scrollview_within_recyclerview.custom_view; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.RelativeLayout; import linklink.com.scrollview_within_recyclerview.utils.LogUtil; /** * MyDispatchRelativeLayout2 * 重写事件拦截 * 责任人: Chuck * 修改人: Chuck * 创建/修改时间: 2018/5/23 16:45 * Copyright : 2014-2017 深圳令令科技有限公司-版权所有 **/ public class MyDispatchRelativeLayout2 extends RelativeLayout { private static final String TAG="MyDispatchLinearLayout"; private static final int SCROLL_THRESHOLD = 50; public MyDispatchRelativeLayout2(Context context) { super(context); } public MyDispatchRelativeLayout2(Context context, AttributeSet attrs) { super(context, attrs); } public MyDispatchRelativeLayout2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } private int mActionDownY,mLastY,mLastRawY; //上下滑动,则拦截,自己处理掉事件 @Override public boolean onInterceptTouchEvent(MotionEvent event) { //return super.onInterceptTouchEvent(ev); switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下时记录纵坐标 mLastY = (int) event.getY();//最后一个action时Y值 LogUtil.e(TAG, "mLastY:" + mLastY); mActionDownY = (int) event.getY();//按下的瞬间Y LogUtil.e(TAG, "mActionDownY:" + mActionDownY); mLastRawY= (int) event.getRawY();//记录y值返回给其子控件使用 LogUtil.e(TAG, "=============================ACTION_DOWN,mLastY" + mLastY); if(mActionDownCallBack!=null){ mActionDownCallBack.recordActionDownY(mLastRawY); } break; case MotionEvent.ACTION_MOVE: LogUtil.e(TAG, "=============================ACTION_MOVE"); LogUtil.e(TAG, "event.getY()=============================" + event.getY()); int dY = (int) event.getY() - mActionDownY; LogUtil.e(TAG, "dY=============================" + dY); if(Math.abs(dY)>=SCROLL_THRESHOLD){//是上下滑动,则拦截,此view自己来处理事件 return true; } break; case MotionEvent.ACTION_UP://注意,全部是getRawX和getRawY LogUtil.e(TAG, "MotionEvent.ACTION_UP"); break; case MotionEvent.ACTION_CANCEL: break; } return false; } public ActionDownCallBack getmActionDownCallBack() { return mActionDownCallBack; } public void setmActionDownCallBack(ActionDownCallBack mActionDownCallBack) { this.mActionDownCallBack = mActionDownCallBack; } private ActionDownCallBack mActionDownCallBack;//按下一瞬间的rawY的处理 public interface ActionDownCallBack { void recordActionDownY(int mLastRawY); } }
2,ViewPager的父容器也是一个自定义控件MyDispatchRelativeLayout,也重写了onInterceptTouchEvent,与上面不同的是,是否拦截,通过一个接口来实现:
package linklink.com.scrollview_within_recyclerview.custom_view; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.RelativeLayout; import linklink.com.scrollview_within_recyclerview.utils.LogUtil; /** * MyDispatchRelativeLayout * 重写事件拦截 * 责任人: Chuck * 修改人: Chuck * 创建/修改时间: 2018/5/23 16:45 * Copyright : 2014-2017 深圳令令科技有限公司-版权所有 **/ public class MyDispatchRelativeLayout extends RelativeLayout { private static final String TAG="MyDispatchRelativeLayout"; public static final int SCROLL_THRESHOLD = 10;//横向滑动超过两个像素,就算是横向滑动 public MyDispatchRelativeLayout(Context context) { super(context); } public MyDispatchRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public MyDispatchRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } private float mActionDownX, mLastX; private float mActionDownY, mLastY; private int mActionDownRowY; /** * @method name:onInterceptTouchEvent * @des:左右滑动不拦截 上下滑动,根据接口的返回值来决定是否拦截 * @param :[event] * @return type:boolean * @date 创建时间:2018/5/23 * @author Chuck **/ @Override public boolean onInterceptTouchEvent(MotionEvent event) { if(1==0){//测试,全部拦截.自己处理事件,如果需要分发,则手动分发 return true; } //return super.onInterceptTouchEvent(ev); switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下时记录纵坐标 LogUtil.i(TAG, "=============================ACTION_DOWN,getX()=" + event.getX()); LogUtil.i(TAG, "=============================ACTION_DOWN,getY()=" + event.getY()); LogUtil.i(TAG, "=============================ACTION_DOWN,getRowX()=" + event.getRawX()); LogUtil.i(TAG, "=============================ACTION_DOWN,getRowY()=" + event.getRawY()); mLastX = event.getX();//最后一个action时X值 //LogUtil.i(TAG, "=============================ACTION_DOWN,mLastX" + mLastX); mActionDownX = event.getX();//按下的瞬间Y //LogUtil.i(TAG, "mActionDownX:" + mActionDownX); mLastY = event.getY();//最后一个action时Y值 //LogUtil.i(TAG, "mLastY:" + mLastY); mActionDownY = event.getY();//按下的瞬间Y //LogUtil.i(TAG, "mActionDownY:" + mActionDownY); mActionDownRowY = (int) event.getRawY();//按下的瞬间mActionDownRowY //LogUtil.i(TAG, "mActionDownRowY:" +mActionDownRowY); //LogUtil.i(TAG, "=============================ACTION_DOWN,mLastY" + mLastY); if(mInterceptProvider!=null){ mInterceptProvider.onActionDown(mActionDownRowY); } break; case MotionEvent.ACTION_MOVE://左右滑动,不拦截.上下滑动,由接口来决定是否拦截 by:Chuck 2018/05/23 LogUtil.i(TAG, "=============================ACTION_MOVE,getX()=" + event.getX()); LogUtil.i(TAG, "=============================ACTION_MOVE,getY()=" + event.getY()); LogUtil.i(TAG, "=============================ACTION_MOVE,getRowX()=" + event.getRawX()); LogUtil.i(TAG, "=============================ACTION_MOVE,getRowY()=" + event.getRawY()); float dX = event.getX() - mActionDownX; LogUtil.i(TAG, "dX=============================" + dX); float dY = event.getY() - mActionDownY; LogUtil.i(TAG, "dY=============================" + dY); LogUtil.i(TAG, "Math.abs(dX)=============================" + Math.abs(dX)); LogUtil.i(TAG, "Math.abs(dY)=============================" + Math.abs(dY)); if(Math.abs(dX)==0&& Math.abs(dY)==0){//实测的时候,发现有这种情况出现:手指上滑,但是坐标没变 LogUtil.i(TAG, "Math.abs(dX)==0&&Math.abs(dY)==0,不拦截" ); return false;//不拦截 } else if(Math.abs(dX)> Math.abs(dY)){//横向滑动的距离大于纵向的,则不拦截 LogUtil.i(TAG, "Math.abs(dX)>Math.abs(dY),不拦截" ); return false;//不拦截 } else{//横向滑动的距离小于纵向 if(Math.abs(dX)>SCROLL_THRESHOLD){//左右滑动的距离超过了阈值,则不拦截,子控件处理 LogUtil.i(TAG, "不拦截:Math.abs(dX)" + Math.abs(dX)); return false; } else{//判定为 上下滑动事件 if(mInterceptProvider!=null){//接口来处理是否拦截. "醉美大连"业务里的是切换的tab已经浮动到了顶部,则里面的viewPager里的recyclerView可以滑动 //现在记录的是getY,也就是距离view边界的距离,上滑的话,新的y会比旧的y小 boolean isScrollUp=event.getY()<=mActionDownY; if(event.getY()==mActionDownY){//y值没变默认为上滑.经实测,下滑不会出问题.但是有时候上滑,y值拿不到 isScrollUp=true; } LogUtil.i(TAG, "上滑? :" + isScrollUp); boolean intercept=mInterceptProvider.onInterceptTouchEvent(isScrollUp); LogUtil.i(TAG, "接口的返回值:" + intercept+(intercept?",拦截":",不拦截")); return intercept; } } } break; case MotionEvent.ACTION_UP: LogUtil.i(TAG, "MotionEvent.ACTION_UP"); break; case MotionEvent.ACTION_CANCEL: break; } return false; } /* */ /** * @method name:onInterceptTouchEvent * @des:左右滑动不拦截 上下滑动,根据接口的返回值来决定是否拦截 * @param :[event] * @return type:boolean * @date 创建时间:2018/5/23 * @author Chuck **//* @Override public boolean onInterceptTouchEvent(MotionEvent event) { //return super.onInterceptTouchEvent(ev); switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下时记录纵坐标 mLastX = (int) event.getRawX();//最后一个action时X值 LogUtil.i(TAG, "mLastX:" + mLastX); mActionDownX = (int) event.getRawX();//按下的瞬间Y LogUtil.i(TAG, "mActionDownX:" + mActionDownX); LogUtil.i(TAG, "=============================ACTION_DOWN,mLastX" + mLastX); mLastY = (int) event.getRawY();//最后一个action时Y值 LogUtil.i(TAG, "mLastY:" + mLastY); mActionDownY = (int) event.getRawY();//按下的瞬间Y LogUtil.i(TAG, "mActionDownY:" + mActionDownY); LogUtil.i(TAG, "=============================ACTION_DOWN,mLastY" + mLastY); if(mInterceptProvider!=null){ mInterceptProvider.onActionDown(mActionDownY); } break; case MotionEvent.ACTION_MOVE://左右滑动,不拦截.上下滑动,由接口来决定是否拦截 by:Chuck 2018/05/23 LogUtil.i(TAG, "=============================ACTION_MOVE"); LogUtil.i(TAG, "event.getRawX()=============================" + event.getRawX()); int dX = (int) event.getRawX() - mActionDownX; LogUtil.i(TAG, "event.getRawX()===================y==========" + event.getRawX()); LogUtil.i(TAG, "dX===================y==========" + dX); int dY = (int) event.getRawY() - mActionDownY; LogUtil.i(TAG, "event.getRawY()=============================" + event.getRawY()); LogUtil.i(TAG, "dY=============================" + dY); if(Math.abs(dX)>SCROLL_THRESHOLD){//左右滑动的距离超过了阈值,则不拦截,子控件处理 LogUtil.i(TAG, "不拦截:Math.abs(dX)" + Math.abs(dX)); return false; } else{ if(mInterceptProvider!=null){//接口来处理是否拦截. "醉美大连"业务里的是切换的tab已经浮动到了顶部,则里面的viewPager里的recyclerView可以滑动 boolean isScrollUp=(mActionDownY-event.getRawY())>0; LogUtil.i(TAG, "上滑? :" + isScrollUp); boolean intercept=mInterceptProvider.onInterceptTouchEvent(isScrollUp); LogUtil.i(TAG, "接口的返回值:" + intercept+(intercept?",拦截":",不拦截")); return intercept; } } break; case MotionEvent.ACTION_UP: LogUtil.i(TAG, "MotionEvent.ACTION_UP"); break; case MotionEvent.ACTION_CANCEL: break; } return false; } */ //实现是否拦截当前的事件 public interface InterceptProvider{ boolean onInterceptTouchEvent(boolean isScrollUp); void onActionDown(int actionDownY); } private InterceptProvider mInterceptProvider; public InterceptProvider getmInterceptProvider() { return mInterceptProvider; } public void setmInterceptProvider(InterceptProvider mInterceptProvider) { this.mInterceptProvider = mInterceptProvider; } }
3,核心的事件分发,处理类:CustomMainFragment,它主要做的是:当上下滑动顶部的广告控件或悬浮控件时,通过设置marginTop来制造界面的"滑动"(滑动时,顶部的透明度随之改变);当viewpager"滑动"到顶部时(这个值是广告控件高度减去title总高度),此时再滑动viewpager(它父容器是一个自定义控件,会有事件拦截逻辑)时,会走分支:
A:如果此时viewpager里的recyclerView或者Listview的第一个item(含headview)正在顶部(判断是否正在顶部,是子类fragment必须实现的一个方法),如果是上滑,则不拦截事件,事件会被viewpager里面的控件消费,也就是recyclerView或者Listview消费;反之,如果此时下滑,则拦截事件,则整个界面会"滑动"下来;
B:如果此时viewpager里的recyclerView或者Listview的第一个item(含headview)不在顶部,则不管上滑或下滑,都不拦截,事件会被viewpager里面的控件消费:
package linklink.com.scrollview_within_recyclerview.ui; import android.animation.ValueAnimator; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import java.util.ArrayList; import java.util.List; import linklink.com.scrollview_within_recyclerview.R; import linklink.com.scrollview_within_recyclerview.base.CustomBaseFragment2; import linklink.com.scrollview_within_recyclerview.custom_view.MyDispatchRelativeLayout; import linklink.com.scrollview_within_recyclerview.custom_view.MyDispatchRelativeLayout2; import linklink.com.scrollview_within_recyclerview.utils.DensityUtil; import linklink.com.scrollview_within_recyclerview.utils.LogUtil; /** * MainFragment * 责任人: Chuck * 修改人: Chuck * 创建/修改时间: 2018/6/25 10:11 * Copyright : 2014-2018 深圳令令科技有限公司-版权所有 **/ public abstract class CustomMainFragment extends Fragment implements ViewPager.OnPageChangeListener { private static final String TAG = "MainFragment"; protected static final int MARGIN_THRESHOLD = 10;//浮动tab的允许误差值 private static final int FLOAT_THRESHOLD_Y = 150;//悬浮临界容差 private static final int DOWN_BACK_THRESHOLD = 1;//是否可以下滑,下滑的上限.有则滑到极限松手,回弹 protected MyDispatchRelativeLayout2 llHead; protected LinearLayout adList; protected ImageView ivBackground; protected MyDispatchRelativeLayout rlVpContainner; protected RelativeLayout rlTitleFilled; protected ViewPager vp; private RelativeLayout rl_content_root; protected List<CustomBaseFragment2> mPagerList = new ArrayList<CustomBaseFragment2>();// 碎片集合 protected int initIndex = 0;//子页索引 protected LinearLayout mTitleViewRoot; private LinearLayout mTabContainer; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_main, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); llHead= (MyDispatchRelativeLayout2) getView().findViewById(R.id.ll_head); adList= (LinearLayout) getView().findViewById(R.id.ad_list); ivBackground= (ImageView) getView().findViewById(R.id.iv_background); rlVpContainner= (MyDispatchRelativeLayout) getView().findViewById(R.id.rl_vp_containner); rlTitleFilled= (RelativeLayout) getView().findViewById(R.id.rl_title); vp= (ViewPager) getView().findViewById(R.id.vp); rl_content_root= (RelativeLayout) getView().findViewById(R.id.rl_content_root); //重置title高度(子类的title可能需要设置不同的高度) ViewGroup.LayoutParams pa=rlTitleFilled.getLayoutParams(); pa.height= getTitleViewParentHeight(); LogUtil.i(TAG,"重置title父容器的高度值:"+ getTitleViewParentHeight()); rlTitleFilled.setLayoutParams(pa); ivBackground.setImageResource(getTitleBackgroundRes()); mTitleViewRoot= (LinearLayout) getView().findViewById(R.id.rl_titlt_root); View titleView=getTitleView(); if(titleView!=null){ mTitleViewRoot.addView(titleView); } //重置title的margigTop,以适应不同的状态栏高度 ViewGroup.MarginLayoutParams titlePa= (ViewGroup.MarginLayoutParams) titleView.getLayoutParams(); titlePa.setMargins(0, getTitleViewMarginTop(),0,0); View headView=getHeadView(); if(headView!=null){ adList.addView(headView); } mTabContainer= (LinearLayout) getView().findViewById(R.id.ll_tab_container); View floatView=getFloatView(); if(floatView!=null){ mTabContainer.addView(floatView); } getFragmnets(); initHeadScrollListener(); initViewPagerContainerScrollListener(); getScroolMax(); } protected int mHeadActionDownX, mHeadActionDownY, mHeadLastY, mHeadSlidedDistance, mScroolMax; /** * @param :[] * @return type:void * @method name:initHeadScrollListener * @des:给列表顶部的控件加滑动监听: 上滑时, 改变控件的位置, 并带动底下的viewpage一起滑动 * 下滑时类似 * 手指抬起时,如果是下滑的,则做一个回弹效果 * @date 创建时间:2018/5/23 * @author Chuck **/ private void initHeadScrollListener() { llHead.setmActionDownCallBack(new MyDispatchRelativeLayout2.ActionDownCallBack() { @Override public void recordActionDownY(int mLastRawY) { LogUtil.e(TAG, "顶部滑动控件,记录父容器按下的y的rawY值:" +mLastRawY); mHeadLastY = (int) mLastRawY;//最后一个action时Y值 mHeadActionDownX = (int) mLastRawY;//按下的瞬间X } }); llHead.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(final View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下时记录纵坐标 if (mScroolMax == 0) {//是个正值,head在上滑的上限值 //悬浮于顶部,距离值 mScroolMax = adList.getHeight()-rlTitleFilled.getHeight(); //mScroolMax = mViewBind.llHead.getHeight() - mViewBind.llTabContainer.getHeight()-mViewBind.rlTitleFilled.getHeight(); LogUtil.e(TAG, "能上滑的最大距离:" + mScroolMax); } mHeadLastY = (int) event.getRawY();//最后一个action时Y值 mHeadActionDownX = (int) event.getRawX();//按下的瞬间X LogUtil.e(TAG, "mActionDownX:" + mHeadActionDownX); mHeadActionDownY = (int) event.getRawY();//按下的瞬间Y LogUtil.e(TAG, "mActionDownY:" + mHeadActionDownY); LogUtil.e(TAG, "=============================ACTION_DOWN,mLastY" + mHeadLastY); break; case MotionEvent.ACTION_MOVE: LogUtil.e(TAG, "=============================ACTION_MOVE"); LogUtil.e(TAG, "event.getRawY()=============================" + event.getRawY()); int dY = (int) event.getRawY() - mHeadLastY; LogUtil.e(TAG, "dY=============================" + dY); mHeadSlidedDistance = (int) event.getRawY() - mHeadActionDownY; LogUtil.e(TAG, "mSlidedDistance=============================" + mHeadSlidedDistance); final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); int left = params.leftMargin; int top = params.topMargin; int right = params.rightMargin; int bottom = params.bottomMargin; final ViewGroup.MarginLayoutParams vpParams = (ViewGroup.MarginLayoutParams) rlVpContainner.getLayoutParams(); //int left = vpParams.leftMargin; //int top = vpParams.topMargin; //int right = vpParams.rightMargin; //int bottom =vpParams.bottomMargin; LogUtil.e(TAG, "left:" + left + ",top:" + top + ",right:" + right + ",bottom" + bottom); int topNew = top + dY; int bottomNew = bottom - dY; int stetchDistance = DOWN_BACK_THRESHOLD;//下滑的回弹距离上限 //上滑极限是tab的位置,下滑极限是回弹的距离 topNew小于0是上滑 topNew大于0是下滑 by:Chuck 2018/05/23 if ((topNew <= 0 && Math.abs(topNew) <= mScroolMax) || (topNew > 0 && topNew < stetchDistance)) { if(topNew <= 0 && Math.abs(topNew) >= mScroolMax){//滑动超标了.因为回调并不是一个像素一个像素的 topNew=-1 * mScroolMax;//赋值为阈值 } LogUtil.e(TAG, topNew + "=============================MOVE"); params.setMargins(left, topNew, right, bottomNew); v.setLayoutParams(params); vpParams.setMargins(left, topNew, right, bottomNew); rlVpContainner.setLayoutParams(vpParams); mHeadLastY = (int) event.getRawY(); //重置顶部title的透明度 resetTitleAlpha(bottomNew); } break; case MotionEvent.ACTION_UP://注意,全部是getRawX和getRawY LogUtil.e(TAG, "MotionEvent.ACTION_UP"); final ViewGroup.MarginLayoutParams paramsNew = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); int topUp = paramsNew.topMargin; final ViewGroup.MarginLayoutParams paramsNew2 = (ViewGroup.MarginLayoutParams) rlVpContainner.getLayoutParams(); LogUtil.e(TAG, "MotionEvent.ACTION_UP,topUp" + topUp); if(topUp<=0){//上滑,几乎快要悬浮,则抬手时让它悬浮 if(Math.abs(Math.abs(topUp)-mScroolMax)<=FLOAT_THRESHOLD_Y ){ paramsNew.setMargins(0, -1 * mScroolMax, 0,mScroolMax); v.setLayoutParams(paramsNew); paramsNew2.setMargins(0, -1 *mScroolMax, 0,mScroolMax); rlVpContainner.setLayoutParams(paramsNew2); //重置顶部title的透明度 resetTitleAlpha(mScroolMax); } } if (topUp > 0) {//topUp>0表示是下滑后抬起手指,此时回弹head和底下的viewPager ValueAnimator anim = ValueAnimator.ofInt(topUp, 0); anim.setDuration(400); // 设置动画运行的时长 anim.setStartDelay(500); // 设置动画延迟播放时间 anim.setRepeatCount(0); // 设置动画重复播放次数 = 重放次数+1 // 动画播放次数 = infinite时,动画无限重复 // anim.setRepeatMode(ValueAnimator.RESTART); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int currentValue = (Integer) animation.getAnimatedValue(); paramsNew.setMargins(0, currentValue, 0, -1 * currentValue); v.setLayoutParams(paramsNew); paramsNew2.setMargins(0, currentValue, 0, -1 * currentValue); rlVpContainner.setLayoutParams(paramsNew2); } }); anim.start(); //重置顶部title的透明度 resetTitleAlpha(0); } break; case MotionEvent.ACTION_CANCEL: break; } return true; } }); } private int mContainerActionDownX, mContainerActionDownY, mContainerLastY, mContainerSlidedDistance; /** * @param :[] * @return type:void * @method name:initViewPagerContainerScrollListener * @des:给底下的viewpager的父容器控件加滑动监听: 上滑时, 改变控件的位置, 并带动上面的head一起滑动 * 下滑时类似 * 手指抬起时,如果是下滑的,则做一个回弹效果 * 所有的滑动动作使用重置margin来实现 * @date 创建时间:2018/5/23 * @author Chuck **/ private void initViewPagerContainerScrollListener() { /************ * * 此监听器在控件的纯左右滑动时不会起作用,只在上下滑动时起作用. "醉美大连"业务里: tab已经浮动到了顶部,上滑则recyclerView消费滑动事件 下滑:如果此时headview正在顶部,getY为0,则父容器消费滑动事件 否recyclerView消费滑动事件(headview看不到了,下滑应该先把headview滑出来) tab不在顶部: 上滑:父容器消费 下滑:如果tab在原始位置,也就是最低位置,则recyclerView消费滑动事件 否则:父容器消费滑动事件 by:Chuck * * */ rlVpContainner.setmInterceptProvider(new MyDispatchRelativeLayout.InterceptProvider() { @Override public void onActionDown(int actionDownY) { mContainerLastY=actionDownY; LogUtil.e(TAG, "通过InterceptProvider接口记录首次按下的Y坐标:" + mContainerLastY); } @Override public boolean onInterceptTouchEvent(boolean isScrollUp) { return judgeIntercept(isScrollUp); } }); rlVpContainner.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(final View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下时记录纵坐标,注意,如果按下时没有记录的话,这个值会是空,所以,在父容器里要把 //按下一瞬间的值传过来(接口形式),否则首次的值会是0.结果造成滑动距离超标 if (mScroolMax == 0) {//是个正值,head在上滑的上限值 //悬浮于顶部,距离值 mScroolMax = adList.getHeight()-rlTitleFilled.getHeight(); // mScroolMax = mViewBind.llHead.getHeight() - mViewBind.llTabContainer.getHeight(); LogUtil.e(TAG, "能上滑的最大距离:" + mScroolMax); } mContainerLastY = (int) event.getRawY();//最后一个action时Y值 mContainerActionDownX = (int) event.getRawX();//按下的瞬间X LogUtil.e(TAG, "mContainerActionDownX:" + mContainerActionDownX); mContainerActionDownY = (int) event.getRawY();//按下的瞬间Y LogUtil.e(TAG, "mContainerActionDownY:" + mContainerActionDownY); LogUtil.e(TAG, "=============================ACTION_DOWN,mContainerLastY" + mContainerLastY); return true;//down事件必须消费 case MotionEvent.ACTION_MOVE: LogUtil.e(TAG, "=============================ACTION_MOVE"); LogUtil.e(TAG, "event.getRawX()=============================" + event.getRawX()); LogUtil.e(TAG, "event.getRawY()=============================" + event.getRawY()); LogUtil.e(TAG, "mContainerLastY============================="+mContainerLastY); /**************************************** * 测试:父容器不再拦截,直接在touch事件里处理掉.如果需要分发,则手动调用子类的onTounch * 如果左右滑动,分发 * 如果上下滑动,根据tab的位置判断是否分发 * */ /* boolean isScrollUp=event.getRawY()<=mContainerLastY;//上滑 LogUtil.e(TAG, "上滑?"+isScrollUp); boolean isHonrizonalScroll=Math.abs(Math.abs(event.getRawX())-Math.abs(mContainerActionDownX))>MyDispatchRelativeLayout.SCROLL_THRESHOLD; LogUtil.e(TAG, "横向滑动?"+isHonrizonalScroll); if( mDispatchEventToRecyclerView || isHonrizonalScroll || !judgeIntercept(isScrollUp) ){ mDispatchEventToRecyclerView=true; mViewBind.vp.onTouchEvent(event);//教给vp去消费 //mViewBind.vp.getO;//教给vp去消费 return false; }*/ /**************************************** * 测试:父容器不再拦截,直接在touch事件里处理掉.如果需要分发,则手动调用子类的onTounch * 如果左右滑动,分发 * 如果上下滑动,根据tab的位置判断是否分发 * */ int containerLastdY = (int) event.getRawY() - mContainerLastY; LogUtil.e(TAG, "containerLastdY=============================" + containerLastdY); mContainerSlidedDistance = (int) event.getRawY() - mContainerActionDownY; LogUtil.e(TAG, "父容器滑动距离:mSlidedDistance=" + mContainerSlidedDistance); final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); int left = params.leftMargin; int top = params.topMargin; int right = params.rightMargin; int bottom = params.bottomMargin; final ViewGroup.MarginLayoutParams vpParams = (ViewGroup.MarginLayoutParams) llHead.getLayoutParams(); //int left = vpParams.leftMargin; //int top = vpParams.topMargin; //int right = vpParams.rightMargin; //int bottom =vpParams.bottomMargin; LogUtil.e(TAG, "container:left:" + left + ",top:" + top + ",right:" + right + ",bottom" + bottom); int topNew = top + containerLastdY; int bottomNew = bottom - containerLastdY; LogUtil.e(TAG, "container:topNew:" + topNew + ",bottomNew" + bottomNew); //上滑极限是tab的位置,不可下滑(相对于初始位置来讲)!!!! topNew小于0是上滑 topNew大于0是下滑 by:Chuck 2018/05/23 if ((topNew <= 0 && Math.abs(topNew) <= mScroolMax)) { if(topNew <= 0 && Math.abs(topNew) >= mScroolMax){//滑动超标了.因为回调并不是一个像素一个像素的 topNew=-1 * mScroolMax;//赋值为阈值 } LogUtil.e(TAG, topNew + "container=============================MOVE"); params.setMargins(left, topNew, right, bottomNew); v.setLayoutParams(params); vpParams.setMargins(left, topNew, right, bottomNew); llHead.setLayoutParams(vpParams); mContainerLastY = (int) event.getRawY(); //重置顶部title的透明度 resetTitleAlpha(bottomNew); } /*else{//分发给viewpager ps:这样处理无效.结果就是,recyclerview在从底部滑到顶部时.需要松手再滑,才能使recyclerview滑到 vp.onTouchEvent(event); mContainerLastY = (int) event.getRawY(); resetTitleAlpha(bottomNew); return false; }*/ break; case MotionEvent.ACTION_UP://注意,全部是getRawX和getRawY LogUtil.e(TAG, "MotionEvent.ACTION_UP"); //vp父容器的布局参数 final ViewGroup.MarginLayoutParams paramsVpUp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); //head的布局参数 final ViewGroup.MarginLayoutParams paramsUp = (ViewGroup.MarginLayoutParams) llHead.getLayoutParams(); //上滑到一个临界值,container:left:0,top:-702,right:0,bottom702 则把它设置为悬浮状态 if(paramsUp.bottomMargin>0 && Math.abs(mScroolMax-paramsUp.bottomMargin)<=FLOAT_THRESHOLD_Y ){ paramsUp.setMargins(0, -1 * mScroolMax, 0,mScroolMax); v.setLayoutParams(paramsUp); paramsVpUp.setMargins(0, -1 *mScroolMax, 0,mScroolMax); rlVpContainner.setLayoutParams(paramsVpUp); //重置顶部title的透明度 resetTitleAlpha(mScroolMax); } //下滑,如果手指抬起来时,tab的位置与初始值的位置的阈值在MARGIN_THRESHOLD范围内,则把一切还原 else if(Math.abs(paramsUp.topMargin)<=FLOAT_THRESHOLD_Y){//已经在底部,初始的位置 LogUtil.e(TAG, "tab在最底部,下滑,不拦截>>>recyclerView消费滑动事件"); paramsVpUp.setMargins(0, 0,0, 0); v.setLayoutParams(paramsVpUp); paramsUp.setMargins(0, 0, 0, 0); llHead.setLayoutParams(paramsUp); try { CustomBaseFragment2 fragment = mPagerList.get(initIndex); LogUtil.e(TAG, "返回顶部,fragment.isVisible()?" + fragment.isVisible()); if (fragment!=null&&fragment.isVisible()) { fragment.setSelection(0);//滑动到顶部 } } catch (Exception e) { e.printStackTrace(); } //重置顶部title的透明度 resetTitleAlpha(0); } return true; //break; case MotionEvent.ACTION_CANCEL: break; } return false; } }); } /** * @method name:judgeIntercept * @des:是否拦截事件.true则拦截,拦截了则recyclerView滑动不了 * @param :[isScrollUp] * @return type:boolean * @date 创建时间:2018/5/24 * @author Chuck **/ protected boolean judgeIntercept(boolean isScrollUp) { final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) llHead.getLayoutParams(); int bottom = params.bottomMargin; LogUtil.e(TAG, "此时head的marginBottom:" + bottom); if (Math.abs(bottom-mScroolMax)<=MARGIN_THRESHOLD) {//满足这个条件,则表示,切换碎片的tab已经悬浮于顶部了,此时可以返回false,父容器不拦截,则子控件viewPager(里面是frgment)自行处理事件 //return false; if (isScrollUp) {//上滑 LogUtil.e(TAG, "tab在顶部,上滑,不拦截>>>recyclerView消费滑动事件"); return false; } else {//下滑 try { CustomBaseFragment2 fragment = mPagerList.get(initIndex); if (fragment!=null&&fragment.isHeadviewAtTopNow()) {//下滑,如果自定义的headview就在顶部,再下滑,就要返回true,不触发recyclerView的滑动事件 LogUtil.e(TAG, "tab在顶部,下滑,recyclerView的headview在顶部,拦截>>>父容器消费滑动事件"); return true; } else {//headview不再顶部,则让recyclerview消费滑动事件 LogUtil.e(TAG, "tab在顶部,下滑,recyclerView的headview不在顶部,不拦截>>>recyclerView消费滑动事件"); return false; } } catch (Exception e) { LogUtil.e(TAG, "抛异常,拦截>>>父容器消费滑动事件"); e.printStackTrace(); return true; } } } else { if (isScrollUp) {//上滑,父类消费滑动事件 LogUtil.e(TAG, "tab不在顶部,上滑,拦截>>>父容器消费滑动事件"); return true; } else {//下滑 if(Math.abs(params.topMargin)<=MARGIN_THRESHOLD){//已经在底部,初始的位置 LogUtil.e(TAG, "tab在最底部,下滑,不拦截>>>recyclerView消费滑动事件"); return false;// } else{ LogUtil.e(TAG, "tab在中间,下滑,拦截>>>父容器消费滑动事件"); return true; } } } } private void resetTitleAlpha(int llHeadMarginBottom){ LogUtil.i(TAG,"resetTitleAlpha:llHeadMarginBottom,="+llHeadMarginBottom); //原始值 int orginalMarginBottom = 0; //滑动到limit时,把mainActivity的titile的alpha值设置为1 int limit =mScroolMax;//tab处于顶部时的marginBottom float alpha = 0.0f; //llHeadMarginBottom alpha //707 1 //600 0.8 //500 0.5 //330 0.3 //0 0 if (llHeadMarginBottom <= orginalMarginBottom) { alpha=0; } else if (llHeadMarginBottom > orginalMarginBottom && llHeadMarginBottom < limit) {//上滑 alpha = 1.0f - (1.0f * (limit - llHeadMarginBottom) / (limit - orginalMarginBottom)); } else if (llHeadMarginBottom >= limit) { alpha = 1.0f; } LogUtil.i(TAG,"resetTitleAlpha:alpha,="+alpha); ivBackground.setAlpha(alpha); } /** * @param :[] * @return type:void * @method name:getScroolMax * @des:初始化滑动阈值 * @date 创建时间:2018/5/23 * @author Chuck **/ private void getScroolMax() { ViewTreeObserver viewTreeObserver = getView().getViewTreeObserver(); if (viewTreeObserver != null) {//绘制完成的监听 viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { LogUtil.i(TAG,"onGlobalLayout()"); LogUtil.i(TAG,"onGlobalLayout(),屏幕总高度:"+DensityUtil.getDisplayHeight(getActivity())); //如果虚拟物理键盘被隐藏了,这个应该会被回调.所以不再remove监听.只要界面有重绘,就重新设置viewPager的高度 getView().getViewTreeObserver().removeGlobalOnLayoutListener(this);//只需要监听一次,之后通过listener回调即可 //悬浮于顶部,距离值 mScroolMax =adList.getHeight()-rlTitleFilled.getHeight(); LogUtil.e(TAG, "能上滑的最大距离:" + mScroolMax); //重置viewPager的最大高度: 注意:虚拟返回键被收起后,看看获得到的屏幕高度是否有变化 //rl_vp_containner 屏幕高度-title-tab-底部tab //int maxHeight= DensityUtil.getDisplayHeight(getActivity()) - DensityUtil.dp2px(getActivity(),70+45+0); //不再使用这种方式获取能滑动的最大高度,因为界面可能不包含状态栏 //int maxHeight= DensityUtil.getDisplayHeight(getActivity())-rlTitleFilled.getHeight()-mTabContainer.getHeight(); int maxHeight= rl_content_root.getHeight()-rlTitleFilled.getHeight()-mTabContainer.getHeight(); ViewGroup.LayoutParams pa=rlVpContainner.getLayoutParams(); pa.height=maxHeight; LogUtil.i(TAG,"重置vp父容器的高度值:"+maxHeight); rlVpContainner.setLayoutParams(pa); } }); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { LogUtil.e(TAG, "POSITION:" + position); initIndex = position;//重置position } @Override public void onPageScrollStateChanged(int state) { } /** * ViewPager适配器 */ public class MyViewPager extends FragmentPagerAdapter { public MyViewPager(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int arg0) { return mPagerList.get(arg0); } @Override public int getCount() { return mPagerList.size(); } @Override public CharSequence getPageTitle(int position) { return ""; } } /** * @method name:getFragmnets * @des:填充碎片 * @param :[] * @return type:void * @date 创建时间:2018/5/29 * @author Chuck **/ private void getFragmnets() { /*for (int i = 0; i < mCropsBeans.size(); i++) { CateBean bean = mCropsBeans.get(i);//获取战队,传值进里面的fragment fragment = new HomeFragment(); // TODO: 2017/9/15 碎片传参数 Bundle bundle = new Bundle(); bundle.putInt(HomeFragment.INTENT_KEY_CROPS_ID, bean.getCateId()); fragment.setArguments(bundle); mPagerList.add(fragment); }*/ mPagerList.addAll(getSubFragments()); if (vp.getAdapter() == null) { //初始化ViewPager vp.setAdapter(new MyViewPager(getChildFragmentManager()));//注意fragmentAdapter的构造器 vp.setOnPageChangeListener(this); LogUtil.e(TAG, "initIndex:" + initIndex); if(initIndex<0||initIndex>=mPagerList.size()){ initIndex=0; } LogUtil.e(TAG, "initIndex:" + initIndex); vp.setCurrentItem(initIndex); //tab的初始化、tab和ViewPager的互相绑定 //tabs.setSmoothScroll(false); //tabs.setViewPager(mViewBind.vp); //tabs.setOnPageChangeListener(this); } } /** * @method name:getTitleBackgroundRes * @des:title背景资源设置 * @param :[] * @return type:int * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract int getTitleBackgroundRes(); /** * @method name:getTitleView * @des:设置titleview. * @param :[] * @return type:android.view.View * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract View getTitleView(); /** * @method name:getTitleViewParentHeight * @des:重置title父容器总高度(不包括getTitleView,因为getTitleView的父容器它有一个paddingTop) * @param :[] * @return type:int * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract int getTitleViewParentHeight(); /** * @method name:getTitleViewMarginTop * @des:重置title父容器paddingTop,单位:像素 * @param :[] * @return type:int * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract int getTitleViewMarginTop(); /** * @method name:getHeadView * @des:顶部的"headview" 可以是包含 左右滑动的banner.滑动事件的分发逻辑已经处理了.左右滑动时.事件将被banner消费 * @param :[] * @return type:android.view.View * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract View getHeadView(); /** * @method name:getFloatView * @des:悬浮控件设置,悬浮控件和底下viewpager的绑定,可以在onActivityCreated里添加 * @param :[] * @return type:android.view.View * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract View getFloatView(); /** * @method name:getSubFragments * @des:设置子碎片,CustomBaseFragment2是自定义类,里面必须实现的是isHeadviewAtTopNow() * @param :[] * @return type:java.util.ArrayList<linklink.com.scrollview_within_recyclerview.base.CustomBaseFragment2> * @date 创建时间:2018/6/25 * @author Chuck **/ public abstract ArrayList<CustomBaseFragment2> getSubFragments(); }
PS:
这个库有个最大的问题,目前我也处理不好:
把Viewpager从初始位置滑动到顶部阈值时,如果继续滑动,里面的recyclerView是不会有动作的.因为此时父容器的已经开始在消费move事件了,我不知道当父容器在不断消费move事件的情况下,如何再次把事件"移交"给子控件处理.也就是说,demo里把下面的viewpager滑到顶部时,要松手再滑,里面的recyclerView才能滑动.如果哪位大神有方案,欢迎指导!
Github地址:
https://github.com/506954774/ScrollviewWithinRecyclerviewAndFloatView