ScrollView和Fragment中的ListView、WebView滑动冲突问题的解决

        日常开发中很少会碰到ScrollView中嵌套listview或webview的情况,而且谷歌官方也不推荐这么做,但是也不是一定不会有这样的需求,毕竟定需求的不是我们程序员,而是产品经理。比如像下面这种需求:
       可以看到,整个页面有一个共同的头部,下面有两个tab,左边tab下是个可以滚动的webview,右边是个listview。要求listview和webview在默认情况下不滚动,但外部整个页面可以滚动,当外层页面滚动到底部时,也就是两个tab的位置大概位于actionbar下方的时候,要求listview和webview自己能滑动而外层不动,当listview或webview下拉到顶部时,又让外层接管滑动,此时共同的头部可以拉下来。
        碰到这种情况,一般的情况肯定是外层套个scrollview,两个tab里分别放个fragment,左右两个fragment分别放置listview。但是很不幸,这样做之后发现listview根本连显示都显示不了,更别提可以滑动了。读者一试便知。原因就在与scrollview和listview存在滑动事件的冲突,那么如何解决这个问题呢?网上有人给出了一个方法:手动计算listview的高度然后显示地设置他的高度。你只需要在listview.setAdapter()方法后调用如下代码:
   
    
    
  1. public void setListViewHeightBasedOnChildren(ListView listView) {
  2. ListAdapter listAdapter = listView.getAdapter();
  3. if (listAdapter == null) {
  4. return;
  5. }
  6. int totalHeight = 0;
  7. for (int i = 0; i < listAdapter.getCount(); i++) {
  8. View listItem = listAdapter.getView(i, null, listView);
  9. listItem.measure(0, 0);
  10. totalHeight += listItem.getMeasuredHeight();
  11. }
  12. ViewGroup.LayoutParams params = listView.getLayoutParams();
  13. params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
  14. Log.d(TAG, "params.height: "+params.height);
  15. listView.setLayoutParams(params);
  16. }
        很不幸!这段代码只有在没有fragment的情况下才有效果,现在的情况是scrollview在宿主Activity中,而listview却在其中一个fragment中,按理说fragment也是放在Activity中那就相当于listview也在Activity中,但是实际情况就是显示不出来listview,具体什么原因我暂时也不清楚。
        那既然在没有fragment的情况下才可以显示listview,那如果不用fragment怎么达到上述切换的效果呢?很简单!在Activity中切换的位置放置个空的容器FrameLayout,然后在切换tab的时候动态添加进想要的view,无论是webvie还是listview甚至是更复杂的view。其实在fragment出现之前,解耦Activity就采用的是这中方式。
我们采用mvc的思想抽象出一个controller基类,功能类似于一个简单的fragment,里面可以绑定view视图和model数据。
   
    
    
  1. public abstract class BaseController
  2. {
  3. public View mRootView;
  4. public Context mContext;
  5. public BaseController(Context context){
  6. this.mContext = context;
  7. // 在构造中 就加载显示的view
  8. mRootView = initView(context);
  9. }
  10. /**
  11. * 初始化view的方法让子类去实现
  12. * @return
  13. */
  14. protected abstract View initView(Context context);
  15. /**
  16. * 加载数据的方法,子类可以实现,也可以不实现
  17. */
  18. public void initData(){
  19. }
  20. /**
  21. * 暴露出去的获得根view的方法
  22. * @return
  23. */
  24. public View getRootView()
  25. {
  26. return mRootView;
  27. }
  28. }
        然后继承这个基类分别创建LeftController 和 RightController,我们先以RightController为例,来把显示listview显示出来。
   
    
    
  1. public class RightController extends BaseController {
  2. private static final String TAG = "RightController";
  3. private ListView mListView;
  4. private List<String> mDatas;
  5. public RightController(Context context) {
  6. super(context);
  7. }
  8. @Override
  9. protected View initView(Context context) {
  10. // TextView readView = new TextView(context);
  11. // readView.setText("right-页面");
  12. View rootView = View.inflate(context, R.layout.controller_right, null);
  13. mListView = (ListView) rootView.findViewById(R.id.lv);
  14. prepareData();
  15. RightAdapter rightAdapter = new RightAdapter(context, mDatas);
  16. mListView.setAdapter(rightAdapter);
  17. setListViewHeightBasedOnChildren(mListView);
  18. return rootView;
  19. }
  20. private void prepareData() {
  21. mDatas = new ArrayList<>();
  22. for (int i = 0; i < 20; i++) {
  23. mDatas.add("item_" + i);
  24. }
  25. }
  26. /**
  27. * 在scrollview中完整显示listview
  28. *
  29. * @param listView
  30. */
  31. public void setListViewHeightBasedOnChildren(ListView listView) {
  32. ListAdapter listAdapter = listView.getAdapter();
  33. if (listAdapter == null) {
  34. return;
  35. }
  36. int totalHeight = 0;
  37. // for (int i = 0; i < listAdapter.getCount(); i++) {
  38. for (int i = 0; i < 10; i++) {
  39. View listItem = listAdapter.getView(i, null, listView);
  40. listItem.measure(0, 0);
  41. totalHeight += listItem.getMeasuredHeight();
  42. }
  43. ViewGroup.LayoutParams params = listView.getLayoutParams();
  44. params.height = totalHeight + (listView.getDividerHeight() * 5);// (listAdapter.getCount() - 1));
  45. Log.d(TAG, "params.height: "+params.height);
  46. listView.setLayoutParams(params);
  47. }
  48. }
        而在Activity的tab切换事件中只需这样两句代码即可把controller的view动态添加进Activity中:
   
    
    
  1. @Override
  2. public void onCheckedChanged(RadioGroup group, int checkedId) {
  3. switch (checkedId) {
  4. case R.id.rb_tab0:
  5. mContainer.removeAllViews();
  6. mContainer.addView(mLeftController.getRootView());
  7. break;
  8. case R.id.rb_tab1:
  9. mContainer.removeAllViews();
  10. mContainer.addView(mRightController.getRootView());
  11. break;
  12. default:
  13. break;
  14. }
  15. }
        这里再贴一下Activity的布局:
   
    
    
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:biglove="http://schemas.android.com/apk/res-auto"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:background="@android:color/white"
  7. android:orientation="vertical">
  8. <TextView
  9. android:id="@+id/custom_title"
  10. android:layout_width="match_parent"
  11. android:layout_height="48dp"
  12. android:background="@android:color/holo_red_dark"
  13. android:gravity="center"
  14. android:text="专项帮扶"
  15. android:textColor="@android:color/white"
  16. android:textSize="15sp" />
  17. <com.wangbiao.scrollviewlistview.ListenBottomScrollView
  18. android:id="@+id/scroll_view"
  19. android:layout_width="match_parent"
  20. android:layout_height="match_parent"
  21. android:layout_below="@id/custom_title"
  22. android:layout_marginBottom="50dp">
  23. <LinearLayout
  24. android:layout_width="match_parent"
  25. android:layout_height="match_parent"
  26. android:layout_below="@id/custom_title"
  27. android:layout_marginBottom="50dp"
  28. android:background="@android:color/white"
  29. android:orientation="vertical">
  30. <FrameLayout
  31. android:layout_width="match_parent"
  32. android:layout_height="200dp"
  33. android:layout_marginLeft="12dp"
  34. android:layout_marginRight="12dp"
  35. android:layout_marginTop="12dp">
  36. <ImageView
  37. android:id="@+id/image1"
  38. android:layout_width="match_parent"
  39. android:layout_height="200dp"
  40. android:scaleType="fitXY"
  41. android:src="@mipmap/ic_launcher" />
  42. </FrameLayout>
  43. <View
  44. android:layout_width="match_parent"
  45. android:layout_height="12dp"
  46. android:background="#f7f7f7" />
  47. <!-- 切换部分 -->
  48. <RadioGroup
  49. android:id="@+id/rg_tab"
  50. android:layout_width="match_parent"
  51. android:layout_height="wrap_content"
  52. android:background="@android:color/white"
  53. android:gravity="center"
  54. android:orientation="horizontal">
  55. <RadioButton
  56. android:id="@+id/rb_tab0"
  57. style="@style/helpPublicityActivityTabStyle"
  58. android:text="项目详情" />
  59. <RadioButton
  60. android:id="@+id/rb_tab1"
  61. style="@style/helpPublicityActivityTabStyle"
  62. android:text="帮扶记录" />
  63. </RadioGroup>
  64. <View
  65. android:background="@android:color/darker_gray"
  66. android:layout_width="match_parent"
  67. android:layout_height="0.5dp" />
  68. <FrameLayout
  69. android:id="@+id/frag_container"
  70. android:layout_width="match_parent"
  71. android:layout_height="match_parent"
  72. />
  73. </LinearLayout>
  74. </com.wangbiao.scrollviewlistview.ListenBottomScrollView>
  75. <Button
  76. android:id="@+id/donate_btn"
  77. android:layout_width="match_parent"
  78. android:layout_height="wrap_content"
  79. android:layout_alignParentBottom="true"
  80. android:gravity="center"
  81. android:text="我要捐助"></Button>
  82. </RelativeLayout>
        可以看到,这里的ListenBottomScrollView 并不是原生的ScrollView,而是我包装过的,原因是需求里我们要监听scrollview滚动到底部,但是原生的scrollview并没有提供这样的listener,所以只能自己动手写一个:
   
    
    
  1. public class ListenBottomScrollView extends ScrollView {
  2. private List<OnScrollToBottomListener> mOnScrollToBottomListeners = new ArrayList<>();
  3. public ListenBottomScrollView(Context context) {
  4. super(context);
  5. }
  6. public ListenBottomScrollView(Context context, AttributeSet attrs) {
  7. super(context, attrs);
  8. }
  9. public ListenBottomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
  10. super(context, attrs, defStyleAttr);
  11. }
  12. @Override
  13. protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  14. View view = (View) getChildAt(getChildCount() - 1);
  15. int d = view.getBottom();
  16. d -= (getHeight() + getScrollY());
  17. if (d == 0) {
  18. if (mOnScrollToBottomListeners.size() != 0) {
  19. for (OnScrollToBottomListener listener : mOnScrollToBottomListeners) {
  20. listener.onScrollBottom(true);
  21. }
  22. }
  23. } else {
  24. super.onScrollChanged(l, t, oldl, oldt);
  25. }
  26. }
  27. public void setOnScrollToBottomListener(OnScrollToBottomListener listener) {
  28. mOnScrollToBottomListeners.add(listener);
  29. }
  30. // 滚动到底部的监听器
  31. public interface OnScrollToBottomListener {
  32. void onScrollBottom(boolean isBottom);
  33. }
  34. }
        注意:这里直说把所有监听器都放到一个List中,是因为还有LeftController里也需要监听他,这就是所谓的观察者模式了。
再看下RightController的完整代码:
   
    
    
  1. public class RightController extends BaseController implements ListenBottomScrollView.OnScrollToBottomListener {
  2. private static final String TAG = "RightController";
  3. private ListView mListView;
  4. private List<String> mDatas;
  5. private boolean mIsListViewTop = true; // 记录listview是否到顶了
  6. private ListenBottomScrollView mScrollView;
  7. public RightController(Context context, ScrollView scrollView) {
  8. super(context);
  9. this.mScrollView = (ListenBottomScrollView)scrollView;
  10. mScrollView.setOnScrollToBottomListener(this);
  11. }
  12. @Override
  13. protected View initView(Context context) {
  14. // TextView readView = new TextView(context);
  15. // readView.setText("right-页面");
  16. View rootView = View.inflate(context, R.layout.controller_right, null);
  17. mListView = (ListView) rootView.findViewById(R.id.lv);
  18. prepareData();
  19. RightAdapter rightAdapter = new RightAdapter(context, mDatas);
  20. mListView.setAdapter(rightAdapter);
  21. setListViewHeightBasedOnChildren(mListView);
  22. setListViewCanScroll(mListView);
  23. setScrollViewMoveWhenListViewStopMoving(mListView);
  24. return rootView;
  25. }
  26. private void setScrollViewMoveWhenListViewStopMoving(ListView listView) {
  27. // 监听listview滚到最底部
  28. listView.setOnScrollListener(new AbsListView.OnScrollListener() {
  29. @Override
  30. public void onScrollStateChanged(AbsListView view, int scrollState) {
  31. switch (scrollState) {
  32. // 当不滚动时
  33. case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
  34. // 判断滚动到底部
  35. if (view.getLastVisiblePosition() == (view.getCount() - 1)) {
  36. Log.d(TAG, "onScrollStateChanged: " + "到底了");
  37. }
  38. // 判断滚动到顶部
  39. if(view.getFirstVisiblePosition() == 0){
  40. Log.d(TAG, "onScrollStateChanged: " + "到顶了");
  41. mIsListViewTop = true;
  42. }else{
  43. mIsListViewTop = false;
  44. }
  45. break;
  46. case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
  47. mIsListViewTop = false;
  48. break;
  49. case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
  50. mIsListViewTop = false;
  51. break;
  52. }
  53. }
  54. @Override
  55. public void onScroll(AbsListView view, int firstVisibleItem,
  56. int visibleItemCount, int totalItemCount) {
  57. }
  58. });
  59. }
  60. // 设置listview什么时候接管滚动事件,什么时候让scrollview接管
  61. private void setListViewCanScroll(final ListView listView) {
  62. listView.setOnTouchListener(new View.OnTouchListener() {
  63. public boolean onTouch(View v, MotionEvent event) {
  64. Log.d(TAG, "onTouch: event=" + event.getAction());
  65. Log.d(TAG, "onTouch: mIsListViewTop=" + mIsListViewTop);
  66. if(mIsListViewTop){
  67. Log.d(TAG, "onTouch: "+"scrollview该滚了!!!");
  68. mScrollView.requestDisallowInterceptTouchEvent(false);
  69. }else {
  70. mScrollView.requestDisallowInterceptTouchEvent(true);
  71. }
  72. return false;
  73. }
  74. });
  75. }
  76. private void prepareData() {
  77. mDatas = new ArrayList<>();
  78. for (int i = 0; i < 20; i++) {
  79. mDatas.add("item_" + i);
  80. }
  81. }
  82. /**
  83. * 在scrollview中完整显示listview
  84. *
  85. * @param listView
  86. */
  87. public void setListViewHeightBasedOnChildren(ListView listView) {
  88. ListAdapter listAdapter = listView.getAdapter();
  89. if (listAdapter == null) {
  90. return;
  91. }
  92. int totalHeight = 0;
  93. // for (int i = 0; i < listAdapter.getCount(); i++) {
  94. // 这里之所以取10,是因为这个计算出的高度恰好使tab位于actionbar下方
  95. // 实际项目中,这个高度可根据需求手动计算出来,不一定要按每个item的高度去算
  96. for (int i = 0; i < 10; i++) {
  97. View listItem = listAdapter.getView(i, null, listView);
  98. listItem.measure(0, 0);
  99. totalHeight += listItem.getMeasuredHeight();
  100. }
  101. ViewGroup.LayoutParams params = listView.getLayoutParams();
  102. params.height = totalHeight + (listView.getDividerHeight() * 5);// (listAdapter.getCount() - 1));
  103. Log.d(TAG, "params.height: "+params.height);
  104. listView.setLayoutParams(params);
  105. }
  106. @Override
  107. public void onScrollBottom(boolean isBottom) {
  108. Log.d(TAG, "onScrollBottom: "+isBottom);
  109. // 到底了就让listview接管事件
  110. mIsListViewTop = false;
  111. }
  112. }
        这里的mIsListViewTop标记了什么时候让listview接管滑动,什么时候让scrollView接管,也就是ScrollView作为外层View什么时候该拦截事件什么时候不该拦截,具体操作就用这行代码:mScrollView.requestDisallowInterceptTouchEvent( );
        同理,LeftController中的WebView也可以用类似的方式实现:
   
    
    
  1. public class LeftController extends BaseController implements ListenBottomScrollView.OnScrollToBottomListener {
  2. private static final String TAG = "LeftController";
  3. private ScrollWebView mWebView;
  4. private boolean mIsWebViewTop = true; // 记录listview是否到顶了
  5. private ListenBottomScrollView mScrollView;
  6. public LeftController(Context context, ScrollView scrollView) {
  7. super(context);
  8. this.mScrollView = (ListenBottomScrollView) scrollView;
  9. mScrollView.setOnScrollToBottomListener(this);
  10. }
  11. @Override
  12. protected View initView(Context context) {
  13. View rootView = View.inflate(context, R.layout.controller_left, null);
  14. mWebView = (ScrollWebView) rootView.findViewById(R.id.webview);
  15. initWebView();
  16. setWebViewHeightBasedOnChildren(mWebView);
  17. setWebViewCanScroll(mWebView);
  18. setScrollViewMoveWhenWebViewStopMoving(mWebView);
  19. return rootView;
  20. }
  21. private void setScrollViewMoveWhenWebViewStopMoving(ScrollWebView mWebView) {
  22. // 监听webview滚到最底部
  23. mWebView.setOnScrollChangeListener(new ScrollWebView.OnScrollChangeListener() {
  24. @Override
  25. public void onPageEnd(int l, int t, int oldl, int oldt) {
  26. Log.d(TAG, "onPageEnd: ");
  27. mIsWebViewTop = false;
  28. }
  29. @Override
  30. public void onPageTop(int l, int t, int oldl, int oldt) {
  31. Log.d(TAG, "onPageTop: ");
  32. mIsWebViewTop = true;
  33. }
  34. @Override
  35. public void onScrollChanged(int l, int t, int oldl, int oldt) {
  36. Log.d(TAG, "onScrollChanged: ");
  37. mIsWebViewTop = false;
  38. }
  39. });
  40. }
  41. private void setWebViewCanScroll(WebView mWebView) {
  42. mWebView.setOnTouchListener(new View.OnTouchListener() {
  43. public boolean onTouch(View v, MotionEvent event) {
  44. Log.d(TAG, "onTouch: event=" + event.getAction());
  45. Log.d(TAG, "onTouch: mIsWebViewTop=" + mIsWebViewTop);
  46. if (mIsWebViewTop) {
  47. Log.d(TAG, "onTouch: " + "scrollview该滚了!!!");
  48. mScrollView.requestDisallowInterceptTouchEvent(false);
  49. } else {
  50. mScrollView.requestDisallowInterceptTouchEvent(true);
  51. }
  52. return false;
  53. }
  54. });
  55. }
  56. private void setWebViewHeightBasedOnChildren(WebView mWebView) {
  57. ViewGroup.LayoutParams params = mWebView.getLayoutParams();
  58.         // 这里之所以取930,是因为这个高度恰好使tab位于actionbar下方
  59. // 实际项目中,这个高度可根据需求手动计算出来,这里为了方便直接写死了
  60. params.height = 930;
  61. mWebView.setLayoutParams(params);
  62. }
  63. private void initWebView() {
  64. WebSettings settings = mWebView.getSettings();
  65. // settings.
  66. settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
  67. // webSettings.setDatabaseEnabled(true);
  68. // 使用localStorage则必须打开
  69. settings.setDomStorageEnabled(true);
  70. settings.setGeolocationEnabled(true);
  71. // 设置webView支持JavaScript脚本
  72. settings.setJavaScriptEnabled(true);
  73. // 设置可以访问文件
  74. settings.setAllowFileAccess(true);
  75. // 设置支持缩放
  76. settings.setBuiltInZoomControls(true);
  77. // 设置使用localStorage则必须打开
  78. settings.setDomStorageEnabled(true);
  79. // 设置webView能自动打开窗口
  80. settings.setJavaScriptCanOpenWindowsAutomatically(true);
  81. // 请求手势焦点
  82. mWebView.requestFocusFromTouch();
  83. mWebView.getSettings().setDisplayZoomControls(false);// 设定缩放控件隐藏
  84. // webSettings.setMediaPlaybackRequiresUserGesture(false);
  85. settings.setSupportZoom(true);
  86. settings.setUseWideViewPort(true);// 这个很关键
  87. settings.setLoadWithOverviewMode(true);
  88. //测试
  89. settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
  90. DisplayMetrics metrics = new DisplayMetrics();
  91. int mDensity = metrics.densityDpi;
  92. if (mDensity == 240) {
  93. settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
  94. } else {
  95. if (mDensity == 160) {
  96. settings.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM);
  97. } else if (mDensity == 120) {
  98. settings.setDefaultZoom(WebSettings.ZoomDensity.CLOSE);
  99. } else if (mDensity == DisplayMetrics.DENSITY_XHIGH) {
  100. settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
  101. } else if (mDensity == DisplayMetrics.DENSITY_TV) {
  102. settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
  103. }
  104. }
  105. mWebView.loadUrl("https://www.baidu.com");
  106. }
  107. @Override
  108. public void onScrollBottom(boolean isBottom) {
  109. // 到底了就webview接管事件
  110. Log.d(TAG, "onScrollBottom: ");
  111. mIsWebViewTop = false;
  112. }
  113. }
        可以看到,这里的WebView也不是原生的,也是重新包装了一下,原因是原生的WebView在api23以下是没有滚动监听的,所以必须自己手写:
   
    
    
  1. public class ScrollWebView extends WebView {
  2. private static final String TAG = "ScrollWebView";
  3. public OnScrollChangeListener listener;
  4. public ScrollWebView(Context context, AttributeSet attrs, int defStyle) {
  5. super(context, attrs, defStyle);
  6. }
  7. public ScrollWebView(Context context, AttributeSet attrs) {
  8. super(context, attrs);
  9. }
  10. public ScrollWebView(Context context) {
  11. super(context);
  12. }
  13. @Override
  14. protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  15. super.onScrollChanged(l, t, oldl, oldt);
  16. float webcontent = getContentHeight() * getScale();// webview的高度
  17. float webnow = getHeight() + getScrollY();// 当前webview的高度
  18. Log.i(TAG, "webview.getScrollY()====>>" + getScrollY());
  19. if (Math.abs(webcontent - webnow) < 1) {
  20. // 已经处于底端
  21. Log.i("TAG1", "已经处于底端");
  22. listener.onPageEnd(l, t, oldl, oldt);
  23. } else if (getScrollY() == 0) {
  24. Log.i("TAG1", "已经处于顶端");
  25. listener.onPageTop(l, t, oldl, oldt);
  26. } else {
  27. listener.onScrollChanged(l, t, oldl, oldt);
  28. }
  29. }
  30. public void setOnScrollChangeListener(OnScrollChangeListener listener) {
  31. this.listener = listener;
  32. }
  33. public interface OnScrollChangeListener {
  34. void onPageEnd(int l, int t, int oldl, int oldt);
  35. void onPageTop(int l, int t, int oldl, int oldt);
  36. void onScrollChanged(int l, int t, int oldl, int oldt);
  37. }
  38. }
        好了,到这里,基本上已经可以满足文章开始提到的需求了,但是仍然还是有一些有待完善的地方,比如webview和Srollview的滚动到顶部或底部的监听有时不是那么灵敏,网上也有其他监听方法,试过发现还是这几个相对来说更灵敏一些。另外webview的左右滑动在Scrollview中也不是很流畅,这个也需要优化一下。总体需求算是满足了,希望对同样有这个需求的开发者有所帮助。同时也很感谢提供各种监听方法的作者们。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/72842312