【Android 手势冲突】彻底解决RecyclerView与ScrollView滑动冲突问题,并实现RecyclerView悬停导航栏
介绍
在新一期的需求中,产品要求我们做出和美团某个页面类似的功能,即一个页面包含在scrollView中,上面一个部分放置一些常用的广告banner、宫格tab等,下面放置一个RecyclerView用于展示具体的产品列表。
要想实现上述功能,不可避免地要用到ScrollView嵌套RecyclerView。为什么要用RecyclerView?因为下面的产品列表项非常多,有60条,如果一次性加载到内存里肯定不现实,所以下方一定要用到可复用的RecyclerView。
而RecyclerView和ScrollView怎么嵌套使用呢?在以前,我总是习惯性地把RecyclerView设置为wrap_content,并且把RecyclerView的setNestedScrollingEnbaled设置为false,这样从来没有遇到过滑动冲突的问题,并且我看到团队里的很多大咖也是这么用。
然而,我们的产品有个需求是在滑动RecyclerView的过程中,RecyclerView顶部的悬停导航栏是要跟着滑动的,于是我就想到在RecyclerView的addOnScrollingListener里设置监听,并且利用linearLayouManager的findLastVisibleItemPosition、findFirstVisibleItemPosition、getChildCount这几个方法来判断当前滑动到RecyclerView的什么位置了,然后去对顶部悬停的导航栏进行联动。问题出现了。无论我怎么滑动,firstVisiblePosition永远为0,lastVisiblePosition永远为item总数-1,getChildCount永远为item总数。WTF,这是什么情况?后来查看资料发现,把RecyclerView高度设置为wrap_content居然是把所有的item都一次性加载进来,并没有用到复用和回收!!!!
对于一直强调代码性能的我,这绝对是我无法忍受的。那么,在为RecyclerView设置一个高度,并把setNestedScrollingEnabled(是否允许嵌套滑动)方法设置为true之后,滑动冲突问题出现了。那么,怎么解决呢?
只需要对ScrollView进行简单的修改,就可以实现。实现原理是,在进到页面中默认把滑动事件交给ScrollView,同时屏蔽RecyclerView的滑动事件;在RecyclerView滑动到顶部的时候,把滑动事件交给RecyclerView。
那么,怎么判断RecyclerView是否滑动到了屏幕顶部了呢?实现方法也是非常简单!通过recyclerview的getTop方法得到recyclerview距离顶部的距离,然后通过scrollView的getScrollY方法得到ScrollView滑动的距离。只需要比较这两个值就可以了。这里,我设置了两个接口回调,在Activity里设置ReyclerView的setNestedScrollingEnabled方法。
package com.example.zhshan.hoveringScrollView;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* @author Zhenhua on 2017/5/24 11:15.
* @email [email protected]
*/
public class MyscrollView extends ScrollView{
public MyscrollView(Context context) {
super(context);
}
public MyscrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyscrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
View view;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(changed){
LinearLayout v = (LinearLayout) getChildAt(0);
if(v != null){
for(int i=0;i<v.getChildCount();i++){
if(v.getChildAt(i).getTag() != null && ((String)v.getChildAt(i).getTag()).equals("aaa")){
view = v.getChildAt(i);
break;
}
}
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if(getScrollY() >= view.getTop()){
fixHead();
//此处代码为实现悬停导航栏,如果只是单纯想解决滑动冲突,可删掉
canvas.save();
canvas.translate(0,getScrollY());
canvas.clipRect(0,0,view.getWidth(),view.getHeight());
view.draw(canvas);
canvas.restore();
}else {
resetHead();
}
}
private OnFixHeadListener listener;
private void fixHead() {
if (listener != null) {
listener.onFix();
}
}
private void resetHead() {
if (listener != null) {
listener.onReset();
}
}
public void setFixHeadListener(OnFixHeadListener listener) {
this.listener = listener;
}
public interface OnFixHeadListener {
void onFix();
void onReset();
}
}
canvas.save();
canvas.translate(0,getScrollY());
canvas.clipRect(0,0,view.getWidth(),view.getHeight());
view.draw(canvas);
canvas.restore();
}else {
resetHead();
}
}
private OnFixHeadListener listener;
private void fixHead() {
if (listener != null) {
listener.onFix();
}
}
private void resetHead() {
if (listener != null) {
listener.onReset();
}
}
public void setFixHeadListener(OnFixHeadListener listener) {
this.listener = listener;
}
public interface OnFixHeadListener {
void onFix();
void onReset();
}
}
通过这样的方法能够非常完美的实现 解决RecyclerView和ScrollView滑动冲突,与RecyclerView悬停导航栏功能。
下面附上demo(点击下载),并贴上两张demo截图。
PS: demo中也完美地实现了ReyclerView指定item置顶功能。
~~~~~~~华丽丽的分割线:问题进一步升级!!5.0以下手机无法解决滑动冲突问题~~~~~~~~~~
1、问题的背景:在RecyclerView需要把滑动事件处理权力交给ScrollView时,调用RecyclerView的setnestedscrollenable(false)方法;在ScrollView需要把滑动事件处理权力交给RecyclerView时,调用RecyclerView的setnestedscrollenable(true)方法。本以为这样就可以完美解决滑动冲突问题,然而测试却在我提测之后第一天就提了bug,我心灰意冷地打开后发现,5.0以下的手机仍然存在滑动冲突问题。去查了下recyclerview的setnestedscrollenable方法的文档才发现,这个方法只有在5.0以上手机有用。擦。。
2、问题如何得到解决?
这个时候就只能按照最原始的方法,根据Android的事件传递原理去一步步解决滑动冲突了。其实,我在2014年的时候就解决过一个滑动冲突问题,并做了一些总结。解决滑动冲突问题其实很简单!!
解决滑动冲突的原理:
(1)刚开始把滑动事件给ScrollView,在ScrollView滑动到某一个位置时,再把滑动事件给RecyclerView。
(2)在RecyclerView滑动到某一个位置时,再把滑动事件交给ScrollView。
总结一下,解决滑动冲突需要知道(1)什么时候把滑动事件传给内部View;(2)什么时候内部View再把滑动事件传给外部View。
先来看一下,如何把事件传给内部View?只需要在ScrollView滑动到某个位置后,使用接口回调,并且让RecyclerView在接口回调里,getParent().requestdisallowintercept()。具体代码如下:
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getScrollY() >= fixView.getTop()) {
fix();
} else {
dismiss();
}
}
在fix回调里,requestdisallowintercept(true)来让ScrollView不拦截。
那么,RecyclerView如何把事件传给外部?
需要给RecyclerView设置ontouchlistener,然后在RecyclerView滑动到第一个item,并且正在向下滑动时,requestdisallowintercept(false)来让ScrollView拦截。
~~~~~~~~~~~~~华丽丽的分割线:滑动冲突完全解析~~~~~~~~~~~~~~~~~~~~~~
1、外部拦截法
所有点击事件都先经过父容器拦截处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//必须返回false,否则子控件永远无法拿到焦点
return false;
case MotionEvent.ACTION_MOVE:
if(事件交给子控件的条件) {
return false;
} else {
return super.onInterceptTouchEvent(ev);
}
case MotionEvent.ACTION_UP:
//必须返回false,否则子控件永远无法拿到焦点
return false;
default:
return super.onInterceptTouchEvent(ev);
}
}
2、内部拦截法
所有点击事件都先交给子控件处理
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//父容器禁止拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(事件交给父容器的条件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
在使用内部拦截法的时候,必须在父容器的Touch_down方法里返回false
~~~~~~~~~~~~~华丽丽的分割线:框架进一步升级,使用RecyclerView代替~~~~~~~~~~~~~~~~~~~~~~
时隔几个月后,确实发现之前的框架存在一定问题,ScrollView嵌套RecyclerView的方式着实让人头痛。趁着不忙的时候,我已经对框架进行了升级,抛弃了过去ScrollView嵌套RecyclerView的方式,而采用多类型RecylerView的方式。这种方式能很好地实现悬停!!
具体请看我的下一篇文章《RecyclerView实现悬停导航栏》。
~~~~~~~~~~~~~华丽丽的分割线:框架进一步升级,近期逛github时找到了嵌套滑动的终极解决办法~~~~~~~~~~~~~~~~~~~~~~
重写了ScrollView!!只有两个类!!使用起来非常容易!!完美解决了各种滑动冲突问题!!
public class HeaderScrollHelper {
private int sysVersion; //当前sdk版本,用于判断api版本
private ScrollableContainer mCurrentScrollableContainer;
public HeaderScrollHelper() {
sysVersion = Build.VERSION.SDK_INT;
}
/** 包含有 ScrollView ListView RecyclerView 的组件 */
public interface ScrollableContainer {
/** @return ScrollView ListView RecyclerView 或者其他的布局的实例 */
View getScrollableView();
}
public void setCurrentScrollableContainer(ScrollableContainer scrollableContainer) {
this.mCurrentScrollableContainer = scrollableContainer;
}
private View getScrollableView() {
if (mCurrentScrollableContainer == null) return null;
return mCurrentScrollableContainer.getScrollableView();
}
/**
* 判断是否滑动到顶部方法,ScrollAbleLayout根据此方法来做一些逻辑判断
* 目前只实现了AdapterView,ScrollView,RecyclerView
* 需要支持其他view可以自行补充实现
*/
public boolean isTop() {
View scrollableView = getScrollableView();
if (scrollableView == null) {
throw new NullPointerException("You should call ScrollableHelper.setCurrentScrollableContainer() to set ScrollableContainer.");
}
if (scrollableView instanceof AdapterView) {
return isAdapterViewTop((AdapterView) scrollableView);
}
if (scrollableView instanceof ScrollView) {
return isScrollViewTop((ScrollView) scrollableView);
}
if (scrollableView instanceof RecyclerView) {
return isRecyclerViewTop((RecyclerView) scrollableView);
}
if (scrollableView instanceof WebView) {
return isWebViewTop((WebView) scrollableView);
}
throw new IllegalStateException("scrollableView must be a instance of AdapterView|ScrollView|RecyclerView");
}
private boolean isRecyclerViewTop(RecyclerView recyclerView) {
if (recyclerView != null) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
int firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
View childAt = recyclerView.getChildAt(0);
if (childAt == null || (firstVisibleItemPosition == 0 && childAt.getTop() == 0)) {
return true;
}
}
}
return false;
}
private boolean isAdapterViewTop(AdapterView adapterView) {
if (adapterView != null) {
int firstVisiblePosition = adapterView.getFirstVisiblePosition();
View childAt = adapterView.getChildAt(0);
if (childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0)) {
return true;
}
}
return false;
}
private boolean isScrollViewTop(ScrollView scrollView) {
if (scrollView != null) {
int scrollViewY = scrollView.getScrollY();
return scrollViewY <= 0;
}
return false;
}
private boolean isWebViewTop(WebView scrollView) {
if (scrollView != null) {
int scrollViewY = scrollView.getScrollY();
return scrollViewY <= 0;
}
return false;
}
/**
* 将特定的view按照初始条件滚动
*
* @param velocityY 初始滚动速度
* @param distance 需要滚动的距离
* @param duration 允许滚动的时间
*/
@SuppressLint("NewApi")
public void smoothScrollBy(int velocityY, int distance, int duration) {
View scrollableView = getScrollableView();
if (scrollableView instanceof AbsListView) {
AbsListView absListView = (AbsListView) scrollableView;
if (sysVersion >= 21) {
absListView.fling(velocityY);
} else {
absListView.smoothScrollBy(distance, duration);
}
} else if (scrollableView instanceof ScrollView) {
((ScrollView) scrollableView).fling(velocityY);
} else if (scrollableView instanceof RecyclerView) {
((RecyclerView) scrollableView).fling(0, velocityY);
} else if (scrollableView instanceof WebView) {
((WebView) scrollableView).flingScroll(0, velocityY);
}
}
}
public class HeaderScrollView extends LinearLayout {
private static final int DIRECTION_UP = 1;
private static final int DIRECTION_DOWN = 2;
private int topOffset = 0; //滚动的最大偏移量
private Scroller mScroller;
private int mTouchSlop; //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
private int mMinimumVelocity; //允许执行一个fling手势动作的最小速度值
private int mMaximumVelocity; //允许执行一个fling手势动作的最大速度值
private int sysVersion; //当前sdk版本,用于判断api版本
private View mHeadView; //需要被滑出的头部
private int mHeadHeight; //滑出头部的高度
private int maxY = 0; //最大滑出的距离,等于 mHeadHeight
private int minY = 0; //最小的距离, 头部在最顶部
private int mCurY; //当前已经滚动的距离
private VelocityTracker mVelocityTracker;
private int mDirection;
private int mLastScrollerY;
private boolean mDisallowIntercept; //是否允许拦截事件
private boolean isClickHead; //当前点击区域是否在头部
private OnScrollListener onScrollListener; //滚动的监听
private HeaderScrollHelper mScrollable;
public interface OnScrollListener {
void onScroll(int currentY, int maxY);
}
public void setOnScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}
public HeaderScrollView(Context context) {
this(context, null);
}
public HeaderScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeaderScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CTTourHeaderScrollView);
topOffset = a.getDimensionPixelSize(R.styleable.CTTourHeaderScrollView_top_offset, topOffset);
a.recycle();
mScroller = new Scroller(context);
mScrollable = new HeaderScrollHelper();
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop(); //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); //允许执行一个fling手势动作的最小速度值
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); //允许执行一个fling手势动作的最大速度值
sysVersion = Build.VERSION.SDK_INT;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mHeadView != null && !mHeadView.isClickable()) {
mHeadView.setClickable(true);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mHeadView = getChildAt(0);
measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);
mHeadHeight = mHeadView.getMeasuredHeight();
maxY = mHeadHeight - topOffset;
//让测量高度加上头部的高度
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));
}
/** @param disallowIntercept 作用同 requestDisallowInterceptTouchEvent */
public void requestHeaderViewPagerDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
mDisallowIntercept = disallowIntercept;
}
private float mDownX; //第一次按下的x坐标
private float mDownY; //第一次按下的y坐标
private float mLastY; //最后一次移动的Y坐标
private boolean verticalScrollFlag = false; //是否允许垂直滚动
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float currentX = ev.getX(); //当前手指相对于当前view的X坐标
float currentY = ev.getY(); //当前手指相对于当前view的Y坐标
float shiftX = Math.abs(currentX - mDownX); //当前触摸位置与第一次按下位置的X偏移量
float shiftY = Math.abs(currentY - mDownY); //当前触摸位置与第一次按下位置的Y偏移量
float deltaY; //滑动的偏移量,即连续两次进入Move的偏移量
obtainVelocityTracker(ev); //初始化速度追踪器
switch (ev.getAction()) {
//Down事件主要初始化变量
case MotionEvent.ACTION_DOWN:
mDisallowIntercept = false;
verticalScrollFlag = false;
mDownX = currentX;
mDownY = currentY;
mLastY = currentY;
checkIsClickHead((int) currentY, mHeadHeight, getScrollY());
mScroller.abortAnimation();
break;
case MotionEvent.ACTION_MOVE:
if (mDisallowIntercept) break;
deltaY = mLastY - currentY; //连续两次进入move的偏移量
mLastY = currentY;
if (shiftX > mTouchSlop && shiftX > shiftY) {
//水平滑动
verticalScrollFlag = false;
} else if (shiftY > mTouchSlop && shiftY > shiftX) {
//垂直滑动
verticalScrollFlag = true;
}
/**
* 这里要注意,对于垂直滑动来说,给出以下三个条件
* 头部没有固定,允许滑动的View处于第一条可见,当前按下的点在头部区域
* 三个条件满足一个即表示需要滚动当前布局,否者不处理,将事件交给子View去处理
*/
if (verticalScrollFlag && (!isStickied() || mScrollable.isTop() || isClickHead)) {
//如果是向下滑,则deltaY小于0,对于scrollBy来说
//正值为向上和向左滑,负值为向下和向右滑,这里要注意
scrollBy(0, (int) (deltaY + 0.5));
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (verticalScrollFlag) {
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); //1000表示单位,每1000毫秒允许滑过的最大距离是mMaximumVelocity
float yVelocity = mVelocityTracker.getYVelocity(); //获取当前的滑动速度
mDirection = yVelocity > 0 ? DIRECTION_DOWN : DIRECTION_UP; //下滑速度大于0,上滑速度小于0
//根据当前的速度和初始化参数,将滑动的惯性初始化到当前View,至于是否滑动当前View,取决于computeScroll中计算的值
//这里不判断最小速度,确保computeScroll一定至少执行一次
mScroller.fling(0, getScrollY(), 0, -(int) yVelocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
mLastScrollerY = getScrollY();
invalidate(); //更新界面,该行代码会导致computeScroll中的代码执行
//阻止快读滑动的时候点击事件的发生,滑动的时候,将Up事件改为Cancel就不会发生点击了
if ((shiftX > mTouchSlop || shiftY > mTouchSlop)) {
if (isClickHead || !isStickied()) {
int action = ev.getAction();
ev.setAction(MotionEvent.ACTION_CANCEL);
boolean dd = super.dispatchTouchEvent(ev);
ev.setAction(action);
return dd;
}
}
}
recycleVelocityTracker();
break;
case MotionEvent.ACTION_CANCEL:
recycleVelocityTracker();
break;
default:
break;
}
//手动将事件传递给子View,让子View自己去处理事件
super.dispatchTouchEvent(ev);
//消费事件,返回True表示当前View需要消费事件,就是事件的TargetView
return true;
}
private void checkIsClickHead(int downY, int headHeight, int scrollY) {
isClickHead = ((downY + scrollY) <= headHeight);
}
private void obtainVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int currY = mScroller.getCurrY();
if (mDirection == DIRECTION_UP) {
// 手势向上划
if (isStickied()) {
//这里主要是将快速滚动时的速度对接起来,让布局看起来滚动连贯
int distance = mScroller.getFinalY() - currY; //除去布局滚动消耗的时间后,剩余的时间
int duration = calcDuration(mScroller.getDuration(), mScroller.timePassed()); //除去布局滚动的距离后,剩余的距离
mScrollable.smoothScrollBy(getScrollerVelocity(distance, duration), distance, duration);
//外层布局已经滚动到指定位置,不需要继续滚动了
mScroller.abortAnimation();
return;
} else {
scrollTo(0, currY); //将外层布局滚动到指定位置
invalidate(); //移动完后刷新界面
}
} else {
// 手势向下划,内部View已经滚动到顶了,需要滚动外层的View
if (mScrollable.isTop() || isClickHead) {
int deltaY = (currY - mLastScrollerY);
int toY = getScrollY() + deltaY;
scrollTo(0, toY);
if (mCurY <= minY) {
mScroller.abortAnimation();
return;
}
}
//向下滑动时,初始状态可能不在顶部,所以要一直重绘,让computeScroll一直调用
//确保代码能进入上面的if判断
invalidate();
}
mLastScrollerY = currY;
}
}
@SuppressLint("NewApi")
private int getScrollerVelocity(int distance, int duration) {
if (mScroller == null) {
return 0;
} else if (sysVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return (int) mScroller.getCurrVelocity();
} else {
return distance / duration;
}
}
/** 对滑动范围做限制 */
@Override
public void scrollBy(int x, int y) {
int scrollY = getScrollY();
int toY = scrollY + y;
if (toY >= maxY) {
toY = maxY;
} else if (toY <= minY) {
toY = minY;
}
y = toY - scrollY;
super.scrollBy(x, y);
}
/** 对滑动范围做限制 */
@Override
public void scrollTo(int x, int y) {
if (y >= maxY) {
y = maxY;
} else if (y <= minY) {
y = minY;
}
mCurY = y;
if (onScrollListener != null) {
onScrollListener.onScroll(y, maxY);
}
super.scrollTo(x, y);
}
/** 头部是否已经固定 */
public boolean isStickied() {
return mCurY == maxY;
}
private int calcDuration(int duration, int timepass) {
return duration - timepass;
}
public int getMaxY() {
return maxY;
}
public boolean isHeadTop() {
return mCurY == minY;
}
/** 是否允许下拉,与PTR结合使用 */
public boolean canPtr() {
return verticalScrollFlag && mCurY == minY && mScrollable.isTop();
}
public void setTopOffset(int topOffset) {
this.topOffset = topOffset;
}
public void setCurrentScrollableContainer(HeaderScrollHelper.ScrollableContainer scrollableContainer) {
mScrollable.setCurrentScrollableContainer(scrollableContainer);
}
}
福利!!福利!!此代码已经应用在我们的产品里,并且已经上线,且稳定运行了三个大版本。可直接拿去用!如有不理解,可直接留言提问,博主每天都会查看!
~~~~~~~~~~~~~华丽丽的分割线:解答朋友们关心的几个问题~~~~~~~~~~~~~~~~~~~~~~
首先感谢各位朋友的支持,看到你们能给我的github一个star或者fork我写的demo,我的内心充满了感恩。
为了能给更大家的开发带来更大的便捷,我决定还是更新一下github,并且把最新的代码合到了demo里,欢迎下载。
看到有一些朋友问为什么不采取多类型recyclerview的方式,我这里试着解答一下。问这个问题的朋友,我相信你肯定只是没有遇到这样必须要scrollview嵌套另外一个可滑动layout的需求。在我们的产品详情页里需要把webview置顶,如果你来实现能有什么更好的方式吗?只能用ScrollView嵌套一个webview吧。另外,我在博文里确实也已经提到过,如果只是实现吸顶功能,确实使用recyclerview就可以实现了,我也已经实现过并且代码也已经上线几个月了。我的一篇博文《【Android 编程架构 程序设计】多Item类型的RecyclerView替代scrollView(附demo)》介绍了一种多类型RecyclerView的编程框架,该代码已经上线并且稳定运行几个月了,如有需要可以去查看。
~~~~~~~~~~~~~华丽丽的分割线:新增高效便捷实现悬停的代码~~~~~~~~~~~~~~~~~~~~~~
以上代码其实不光实现了悬停,更解决了滑动冲突。那么,如果不需要解决滑动冲突,我只希望能将ScrollView中的一个View实现悬停,并没有在ScrollView嵌套RecyclerView的场景中的话,我的代码该怎么写呢?我在demo中新增了单纯实现悬停的方法,这里只有一个view,代码看起来逻辑清晰,并且很清爽,并没有采用两个View隐藏展示的方法。如有需要,欢迎下载demo查看。
---------------------
感谢这位作者的分享,在此表示感谢。
作者:Colin_Mindset
来源:****
原文:https://blog.****.net/colinandroid/article/details/72770863