日常开发中很少会碰到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()方法后调用如下代码:
public void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
Log.d(TAG, "params.height: "+params.height);
listView.setLayoutParams(params);
}
很不幸!这段代码只有在没有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数据。
public abstract class BaseController
{
public View mRootView;
public Context mContext;
public BaseController(Context context){
this.mContext = context;
// 在构造中 就加载显示的view
mRootView = initView(context);
}
/**
* 初始化view的方法让子类去实现
* @return
*/
protected abstract View initView(Context context);
/**
* 加载数据的方法,子类可以实现,也可以不实现
*/
public void initData(){
}
/**
* 暴露出去的获得根view的方法
* @return
*/
public View getRootView()
{
return mRootView;
}
}
然后继承这个基类分别创建LeftController 和 RightController,我们先以RightController为例,来把显示listview显示出来。
public class RightController extends BaseController {
private static final String TAG = "RightController";
private ListView mListView;
private List<String> mDatas;
public RightController(Context context) {
super(context);
}
@Override
protected View initView(Context context) {
// TextView readView = new TextView(context);
// readView.setText("right-页面");
View rootView = View.inflate(context, R.layout.controller_right, null);
mListView = (ListView) rootView.findViewById(R.id.lv);
prepareData();
RightAdapter rightAdapter = new RightAdapter(context, mDatas);
mListView.setAdapter(rightAdapter);
setListViewHeightBasedOnChildren(mListView);
return rootView;
}
private void prepareData() {
mDatas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
mDatas.add("item_" + i);
}
}
/**
* 在scrollview中完整显示listview
*
* @param listView
*/
public void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
// for (int i = 0; i < listAdapter.getCount(); i++) {
for (int i = 0; i < 10; i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * 5);// (listAdapter.getCount() - 1));
Log.d(TAG, "params.height: "+params.height);
listView.setLayoutParams(params);
}
}
而在Activity的tab切换事件中只需这样两句代码即可把controller的view动态添加进Activity中:
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.rb_tab0:
mContainer.removeAllViews();
mContainer.addView(mLeftController.getRootView());
break;
case R.id.rb_tab1:
mContainer.removeAllViews();
mContainer.addView(mRightController.getRootView());
break;
default:
break;
}
}
这里再贴一下Activity的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:biglove="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<TextView
android:id="@+id/custom_title"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/holo_red_dark"
android:gravity="center"
android:text="专项帮扶"
android:textColor="@android:color/white"
android:textSize="15sp" />
<com.wangbiao.scrollviewlistview.ListenBottomScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/custom_title"
android:layout_marginBottom="50dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/custom_title"
android:layout_marginBottom="50dp"
android:background="@android:color/white"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_marginTop="12dp">
<ImageView
android:id="@+id/image1"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="fitXY"
android:src="@mipmap/ic_launcher" />
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="12dp"
android:background="#f7f7f7" />
<!-- 切换部分 -->
<RadioGroup
android:id="@+id/rg_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_tab0"
style="@style/helpPublicityActivityTabStyle"
android:text="项目详情" />
<RadioButton
android:id="@+id/rb_tab1"
style="@style/helpPublicityActivityTabStyle"
android:text="帮扶记录" />
</RadioGroup>
<View
android:background="@android:color/darker_gray"
android:layout_width="match_parent"
android:layout_height="0.5dp" />
<FrameLayout
android:id="@+id/frag_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
</com.wangbiao.scrollviewlistview.ListenBottomScrollView>
<Button
android:id="@+id/donate_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:text="我要捐助"></Button>
</RelativeLayout>
可以看到,这里的ListenBottomScrollView 并不是原生的ScrollView,而是我包装过的,原因是需求里我们要监听scrollview滚动到底部,但是原生的scrollview并没有提供这样的listener,所以只能自己动手写一个:
public class ListenBottomScrollView extends ScrollView {
private List<OnScrollToBottomListener> mOnScrollToBottomListeners = new ArrayList<>();
public ListenBottomScrollView(Context context) {
super(context);
}
public ListenBottomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListenBottomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
View view = (View) getChildAt(getChildCount() - 1);
int d = view.getBottom();
d -= (getHeight() + getScrollY());
if (d == 0) {
if (mOnScrollToBottomListeners.size() != 0) {
for (OnScrollToBottomListener listener : mOnScrollToBottomListeners) {
listener.onScrollBottom(true);
}
}
} else {
super.onScrollChanged(l, t, oldl, oldt);
}
}
public void setOnScrollToBottomListener(OnScrollToBottomListener listener) {
mOnScrollToBottomListeners.add(listener);
}
// 滚动到底部的监听器
public interface OnScrollToBottomListener {
void onScrollBottom(boolean isBottom);
}
}
注意:这里直说把所有监听器都放到一个List中,是因为还有LeftController里也需要监听他,这就是所谓的观察者模式了。
再看下RightController的完整代码:
public class RightController extends BaseController implements ListenBottomScrollView.OnScrollToBottomListener {
private static final String TAG = "RightController";
private ListView mListView;
private List<String> mDatas;
private boolean mIsListViewTop = true; // 记录listview是否到顶了
private ListenBottomScrollView mScrollView;
public RightController(Context context, ScrollView scrollView) {
super(context);
this.mScrollView = (ListenBottomScrollView)scrollView;
mScrollView.setOnScrollToBottomListener(this);
}
@Override
protected View initView(Context context) {
// TextView readView = new TextView(context);
// readView.setText("right-页面");
View rootView = View.inflate(context, R.layout.controller_right, null);
mListView = (ListView) rootView.findViewById(R.id.lv);
prepareData();
RightAdapter rightAdapter = new RightAdapter(context, mDatas);
mListView.setAdapter(rightAdapter);
setListViewHeightBasedOnChildren(mListView);
setListViewCanScroll(mListView);
setScrollViewMoveWhenListViewStopMoving(mListView);
return rootView;
}
private void setScrollViewMoveWhenListViewStopMoving(ListView listView) {
// 监听listview滚到最底部
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
// 当不滚动时
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
// 判断滚动到底部
if (view.getLastVisiblePosition() == (view.getCount() - 1)) {
Log.d(TAG, "onScrollStateChanged: " + "到底了");
}
// 判断滚动到顶部
if(view.getFirstVisiblePosition() == 0){
Log.d(TAG, "onScrollStateChanged: " + "到顶了");
mIsListViewTop = true;
}else{
mIsListViewTop = false;
}
break;
case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
mIsListViewTop = false;
break;
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
mIsListViewTop = false;
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});
}
// 设置listview什么时候接管滚动事件,什么时候让scrollview接管
private void setListViewCanScroll(final ListView listView) {
listView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: event=" + event.getAction());
Log.d(TAG, "onTouch: mIsListViewTop=" + mIsListViewTop);
if(mIsListViewTop){
Log.d(TAG, "onTouch: "+"scrollview该滚了!!!");
mScrollView.requestDisallowInterceptTouchEvent(false);
}else {
mScrollView.requestDisallowInterceptTouchEvent(true);
}
return false;
}
});
}
private void prepareData() {
mDatas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
mDatas.add("item_" + i);
}
}
/**
* 在scrollview中完整显示listview
*
* @param listView
*/
public void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
// for (int i = 0; i < listAdapter.getCount(); i++) {
// 这里之所以取10,是因为这个计算出的高度恰好使tab位于actionbar下方
// 实际项目中,这个高度可根据需求手动计算出来,不一定要按每个item的高度去算
for (int i = 0; i < 10; i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * 5);// (listAdapter.getCount() - 1));
Log.d(TAG, "params.height: "+params.height);
listView.setLayoutParams(params);
}
@Override
public void onScrollBottom(boolean isBottom) {
Log.d(TAG, "onScrollBottom: "+isBottom);
// 到底了就让listview接管事件
mIsListViewTop = false;
}
}
这里的mIsListViewTop标记了什么时候让listview接管滑动,什么时候让scrollView接管,也就是ScrollView作为外层View什么时候该拦截事件什么时候不该拦截,具体操作就用这行代码:mScrollView.requestDisallowInterceptTouchEvent( );
同理,LeftController中的WebView也可以用类似的方式实现:
public class LeftController extends BaseController implements ListenBottomScrollView.OnScrollToBottomListener {
private static final String TAG = "LeftController";
private ScrollWebView mWebView;
private boolean mIsWebViewTop = true; // 记录listview是否到顶了
private ListenBottomScrollView mScrollView;
public LeftController(Context context, ScrollView scrollView) {
super(context);
this.mScrollView = (ListenBottomScrollView) scrollView;
mScrollView.setOnScrollToBottomListener(this);
}
@Override
protected View initView(Context context) {
View rootView = View.inflate(context, R.layout.controller_left, null);
mWebView = (ScrollWebView) rootView.findViewById(R.id.webview);
initWebView();
setWebViewHeightBasedOnChildren(mWebView);
setWebViewCanScroll(mWebView);
setScrollViewMoveWhenWebViewStopMoving(mWebView);
return rootView;
}
private void setScrollViewMoveWhenWebViewStopMoving(ScrollWebView mWebView) {
// 监听webview滚到最底部
mWebView.setOnScrollChangeListener(new ScrollWebView.OnScrollChangeListener() {
@Override
public void onPageEnd(int l, int t, int oldl, int oldt) {
Log.d(TAG, "onPageEnd: ");
mIsWebViewTop = false;
}
@Override
public void onPageTop(int l, int t, int oldl, int oldt) {
Log.d(TAG, "onPageTop: ");
mIsWebViewTop = true;
}
@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
Log.d(TAG, "onScrollChanged: ");
mIsWebViewTop = false;
}
});
}
private void setWebViewCanScroll(WebView mWebView) {
mWebView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: event=" + event.getAction());
Log.d(TAG, "onTouch: mIsWebViewTop=" + mIsWebViewTop);
if (mIsWebViewTop) {
Log.d(TAG, "onTouch: " + "scrollview该滚了!!!");
mScrollView.requestDisallowInterceptTouchEvent(false);
} else {
mScrollView.requestDisallowInterceptTouchEvent(true);
}
return false;
}
});
}
private void setWebViewHeightBasedOnChildren(WebView mWebView) {
ViewGroup.LayoutParams params = mWebView.getLayoutParams();
// 这里之所以取930,是因为这个高度恰好使tab位于actionbar下方
// 实际项目中,这个高度可根据需求手动计算出来,这里为了方便直接写死了
params.height = 930;
mWebView.setLayoutParams(params);
}
private void initWebView() {
WebSettings settings = mWebView.getSettings();
// settings.
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
// webSettings.setDatabaseEnabled(true);
// 使用localStorage则必须打开
settings.setDomStorageEnabled(true);
settings.setGeolocationEnabled(true);
// 设置webView支持JavaScript脚本
settings.setJavaScriptEnabled(true);
// 设置可以访问文件
settings.setAllowFileAccess(true);
// 设置支持缩放
settings.setBuiltInZoomControls(true);
// 设置使用localStorage则必须打开
settings.setDomStorageEnabled(true);
// 设置webView能自动打开窗口
settings.setJavaScriptCanOpenWindowsAutomatically(true);
// 请求手势焦点
mWebView.requestFocusFromTouch();
mWebView.getSettings().setDisplayZoomControls(false);// 设定缩放控件隐藏
// webSettings.setMediaPlaybackRequiresUserGesture(false);
settings.setSupportZoom(true);
settings.setUseWideViewPort(true);// 这个很关键
settings.setLoadWithOverviewMode(true);
//测试
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
DisplayMetrics metrics = new DisplayMetrics();
int mDensity = metrics.densityDpi;
if (mDensity == 240) {
settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
} else {
if (mDensity == 160) {
settings.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM);
} else if (mDensity == 120) {
settings.setDefaultZoom(WebSettings.ZoomDensity.CLOSE);
} else if (mDensity == DisplayMetrics.DENSITY_XHIGH) {
settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
} else if (mDensity == DisplayMetrics.DENSITY_TV) {
settings.setDefaultZoom(WebSettings.ZoomDensity.FAR);
}
}
mWebView.loadUrl("https://www.baidu.com");
}
@Override
public void onScrollBottom(boolean isBottom) {
// 到底了就webview接管事件
Log.d(TAG, "onScrollBottom: ");
mIsWebViewTop = false;
}
}
可以看到,这里的WebView也不是原生的,也是重新包装了一下,原因是原生的WebView在api23以下是没有滚动监听的,所以必须自己手写:
public class ScrollWebView extends WebView {
private static final String TAG = "ScrollWebView";
public OnScrollChangeListener listener;
public ScrollWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ScrollWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScrollWebView(Context context) {
super(context);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
float webcontent = getContentHeight() * getScale();// webview的高度
float webnow = getHeight() + getScrollY();// 当前webview的高度
Log.i(TAG, "webview.getScrollY()====>>" + getScrollY());
if (Math.abs(webcontent - webnow) < 1) {
// 已经处于底端
Log.i("TAG1", "已经处于底端");
listener.onPageEnd(l, t, oldl, oldt);
} else if (getScrollY() == 0) {
Log.i("TAG1", "已经处于顶端");
listener.onPageTop(l, t, oldl, oldt);
} else {
listener.onScrollChanged(l, t, oldl, oldt);
}
}
public void setOnScrollChangeListener(OnScrollChangeListener listener) {
this.listener = listener;
}
public interface OnScrollChangeListener {
void onPageEnd(int l, int t, int oldl, int oldt);
void onPageTop(int l, int t, int oldl, int oldt);
void onScrollChanged(int l, int t, int oldl, int oldt);
}
}
好了,到这里,基本上已经可以满足文章开始提到的需求了,但是仍然还是有一些有待完善的地方,比如webview和Srollview的滚动到顶部或底部的监听有时不是那么灵敏,网上也有其他监听方法,试过发现还是这几个相对来说更灵敏一些。另外webview的左右滑动在Scrollview中也不是很流畅,这个也需要优化一下。总体需求算是满足了,希望对同样有这个需求的开发者有所帮助。同时也很感谢提供各种监听方法的作者们。