最近公司没事,研究了下多嵌套滚动组件的事件分发,虽然以前也接触过,但都是拿网上的用,也是特别简单的,正好朋友也需要,就研究了下
这个Demo也不是很完善,放上来也是让各位大牛给指点一下,优化优化
使用情景:
小米商城商品详情界面,界面看似ScrollView,但当正常滚动到底部时,提示继续上拉显示更多详情,上拉后直接滚动到第二屏,第二屏是个ViewPager,ViewPager里面的各个pager有的是WebView有的是ListView,有的是ScrollView,一开始想想就特别头晕,后来理清思路后,实现起来却处处碰壁,不是ViewPager不能左右滑动就是ListView不能上拉,网上也搜索了很多相关Demo,但都没有完善一点的,也许根本没几个人使用这样的无脑嵌套吧,好吧,既然这样,就只有自己动手了。
花了1周时间,总算出来点效果了,重写了几个组件:InnerScrollView、InnerWebView、InnerListView
如果内部ScrollView是固定高度,那么需要滚动,外部的当然也需要滚动,所以要判断当内部滚动到顶部并且手指继续下滑时,把事件交父类处理,同样当滚动到底部并继续上滑时也要交出去,如果InnerScrollView的ChildView高度小于等于InnerScrollView高度(就是不出现滚动条)时,把事件交给父类处理。
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ScrollView;
/**
* 内部ScrollView,解决滑动内部ScrollView时,触发外部滚动问题
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerScrollView extends ScrollView {
private final String TAG = "XinInnerScrollView";
private float childHeight = 0;
private float downX, downY; // 按下时
private float currX, currY; // 移动时
private float moveY; // 从按下到移动的Y距离
private float scrollViewHeight;
private boolean isOnTop; // ScrollView是否处于屏幕顶端
private boolean isOnBottom; // ScrollView是否处于屏幕底端
private boolean debug = true;
private Position position = Position.NONE;
public XinInnerScrollView(Context context) {
this(context, null);
}
public XinInnerScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XinInnerScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
downX = ev.getX();
downY = ev.getY();
childHeight = getChildAt(0).getMeasuredHeight();
scrollViewHeight = getHeight();
break;
case MotionEvent.ACTION_MOVE:
currX = ev.getX();
currY = ev.getY();
moveY = Math.abs(currY - downY);
isOnTop = getScrollY() == 0;
isOnBottom = (getScrollY() + scrollViewHeight) == childHeight;
// 垂直滑动
if (moveY > Math.abs(currX - downX)) {
if (childHeight <= scrollViewHeight) {
printLog("onTouchEvent ACTION_MOVE 不能滚动 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else if (isOnTop) { // 当前处于ScrollView顶部
if (currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 已到顶部 下滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到顶部 上滑 子处理");
}
} else if (isOnBottom) {
// 当前处于ScrollView底部
if (currY - downY < 0) {
printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子处理");
}
} else {
// 当前处于ScrollView中间
printLog("onTouchEvent ACTION_MOVE 在中间 子处理");
}
}
// 水平滚动
else {
if(position.equals(Position.TOP)){
printLog("onTouchEvent ACTION_MOVE 水平滚动 position=TOP 子处理");
} else {
if(Math.abs(currX - downX) > 30){
printLog("onTouchEvent ACTION_MOVE 水平滚动 position!=TOP 横向滑动距离>30 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 水平滚动 position!=TOP 横向滑动距离<=30 子处理");
}
}
}
break;
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP ========================");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
/**
* 为了更好的处理手势滑动事件,设置该组件所处的位置;
* 比如只有上下两屏时,如果该View是在第一屏,那么设置为Position.TOP,如果在第二屏,则设置为Position.BOTTOM
*
* @param position
*/
public void setPosition(Position position) {
this.position = position;
}
public static enum Position {
/**
* 顶部View,横向滑动时将不考虑将事件交给父View。(该设计只为第一屏为纯ScrollView考虑)
*/
TOP,
/**
* 底部View, 横向滑动时,将把事件交给父View处理
*/
BOTTOM,
/**
* 不设置,将自动判断(自动判断并不是很精准)
*/
NONE
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.webkit.WebView;
/**
* 内部WebView, 该View只适合放在最后一屏
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerWebView extends WebView {
private final String TAG = "XinInnerScrollView";
private boolean debug = true;
private float downX, downY; // 按下时
private float currX, currY; // 移动时
private float moveX; // 移动长度-横向
public XinInnerWebView(Context context) {
super(context);
}
public XinInnerWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public XinInnerWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
printLog("onTouchEvent ACTION_DOWN");
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
currX = ev.getX();
currY = ev.getY();
moveX = Math.abs(currX - downX);
printLog("onTouchEvent ACTION_MOVE getScrollX()="+getScrollX() + " getScrollY()="+getScrollY());
// 垂直滑动
if (Math.abs(currY - downY) > moveX) {
// 处于顶部或者无法滚动,并且继续下滑,交出事件(currY-downY >0是下滑, <0则是上滑)
if (getScrollY() == 0 && currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 在顶部 下滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
// 已到底部且继续上滑时,把事件交出去
else if(getContentHeight()*getScale() - (getHeight() + getScrollY()) <= 1 && currY - downY < 0){
printLog("onTouchEvent ACTION_MOVE 在底部 上滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
// 水平滚动,横向滑动长度大于20像素时再交出去,不然都当做是垂直滑动。
else if(moveX > 20){
// 横向滑动事不直接交出去,是因为可能页面出现水平滚动条,就是网页宽度比屏幕还宽的情况下就需要判断滑到左边和滑到右边的情况。
// printLog("onTouchEvent ACTION_MOVE 横向滑动 父处理");
// getParent().getParent().requestDisallowInterceptTouchEvent(false);
// 已在左边且继续右滑时,把事件交出去(currX - downX >0是右滑, <0则是左滑)
if (getScrollX() == 0 && currX - downX > 0) {
printLog("onTouchEvent ACTION_MOVE 在左边 右滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
// 已在右边且继续左滑时,把事件交出去
else if(getRight()*getScale() - (getWidth() + getScrollX()) <= 1 && currX - downX < 0){
printLog("onTouchEvent ACTION_MOVE 在右边 左滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
package com.wuguangxin.morescrolldemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.ListView;
/**
* 内部ListView, 该组件不支持下拉刷新,上拉加载更多,是为了嵌套在ScrollView或者ViewPager中的,适合数据少的环境使用,最好是一次性显示所有数据
* 提供一个接口OnLastItemVisibleListener,当最后一个item完全显示时,回调onLastItemVisible(),可以去加载更多数据。
*
* @author wuguangxin
* @date 16/7/1 上午10:34
*/
public class XinInnerListView extends ListView implements AbsListView.OnScrollListener {
private final String TAG = "XinInnerScrollView";
private boolean debug = true;
private boolean isFirstItemVisible; // 第一个item是否可见
private boolean isLastItemVisible; // 最后一个item是否可见
private int downX, downY; // 按下时
private int currX, currY; // 移动时
private int moveY; // 从按下到移动的Y距离
public XinInnerListView(Context context) {
super(context);
setOnScrollListener(this);
}
public XinInnerListView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnScrollListener(this);
}
public XinInnerListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnScrollListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().getParent().requestDisallowInterceptTouchEvent(true);
downX = (int)ev.getX();
downY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
currX = (int)ev.getX();
currY = (int)ev.getY();
moveY = Math.abs(currY - downY);
if(currY == downY){
break;
}
// 垂直滑动
if (moveY > Math.abs(currX - downX)) {
if (isFirstItemVisible) { // 当前处于顶部
if (currY - downY > 0) {
printLog("onTouchEvent ACTION_MOVE 已到顶部 下滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到顶部 上滑 子处理");
}
} else if (isLastItemVisible) {
// 当前处于底部
if (currY - downY < 0) {
printLog("onTouchEvent ACTION_MOVE 已到底部 上滑 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
} else {
printLog("onTouchEvent ACTION_MOVE 已到底部 下滑 子处理");
}
} else {
// 当前处于中间
printLog("onTouchEvent ACTION_MOVE 在中间 子处理");
}
} else {
// 水平滚动
printLog("onTouchEvent ACTION_MOVE 水平滚动 父处理");
getParent().getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
printLog("onTouchEvent ACTION_UP ========================");
getParent().getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(ev);
}
/**
* 判断最后listView中最后一个item是否完全显示出来
* @return
*/
protected boolean isLastItemVisible() {
Adapter adapter = getAdapter();
if (null == adapter || adapter.isEmpty()) {
return true;
}
int lastVisiblePosition = getLastVisiblePosition();
if (lastVisiblePosition >= (adapter.getCount() - 1) - 1) {
View lastVisibleChild = getChildAt(Math.min(lastVisiblePosition - getFirstVisiblePosition(), getChildCount() - 1));
if (lastVisibleChild != null) {
return lastVisibleChild.getBottom() <= getBottom();
}
}
return false;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
isFirstItemVisible = firstVisibleItem == 0 && getScrollY() == 0;
isLastItemVisible = isLastItemVisible();
if (isLastItemVisible) {
if (onLastItemVisibleListener != null) {
onLastItemVisibleListener.onLastItemVisible(view, firstVisibleItem + visibleItemCount - 1, getAdapter());
}
}
}
public void setOnLastItemVisibleListener(OnLastItemVisibleListener onLastItemVisibleListener) {
this.onLastItemVisibleListener = onLastItemVisibleListener;
}
private OnLastItemVisibleListener onLastItemVisibleListener;
/**
* 最后一个Item显示的监听器
*/
public interface OnLastItemVisibleListener {
void onLastItemVisible(AbsListView view, int position, Adapter adapter);
}
public void printLog(String msg) {
if (debug) {
Log.d(TAG, msg);
}
}
}
在代码中出现很多没有代码的else,只是为了看日志方便而已。
白色区域是可以单独滚动的
ScrollViewInScrollViewActivity.java
public class ScrollViewInScrollViewActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrollview);
setTitle("ScrollView In ScrollView");
}
}
activity_scrollview.xml
黑色背景是原生ScrollView,中间是InnerWebView,该WebView设置了固定高度,所以可以上下滑动,如果高度设置为wrap_content,则会把整个网页撑开,与外部ScrollView融合在一起。
WebViewInScrollViewActivity.java
package com.wuguangxin.morescrolldemo.ui;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.wuguangxin.morescrolldemo.Configs;
import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerWebView;
/**
* WebView In ScrollView
*
* @author wuguangxin
* @date 16/7/5 上午11:35
*/
public class WebViewInScrollViewActivity extends FragmentActivity {
private XinInnerWebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
setTitle("WebView In ScrollView");
mWebView = (XinInnerWebView) findViewById(R.id.webview);
mWebView.loadUrl(Configs.URL_BAIDU);
initWebView();
}
private void initWebView(){
WebSettings webSet = mWebView.getSettings();
webSet.setSupportZoom(true); // 支持缩放
webSet.setAllowFileAccess(true); // 设置可以访问文件
webSet.setJavaScriptEnabled(true); // 启用JavaScript
webSet.setBlockNetworkImage(false); // 限制网络图片
webSet.setBuiltInZoomControls(true); // 控制页面缩放
webSet.setLoadWithOverviewMode(true); // 设置webview加载的页面的模式,
webSet.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM); // 设置默认的缩放级别
webSet.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
mWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url){
view.loadUrl(url);
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon){
}
@Override
public void onPageFinished(WebView view, String url){
}
});
}
}
activity_webview.xml
ListViewInScrollViewActivity.java
package com.wuguangxin.morescrolldemo.ui;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.wuguangxin.morescrolldemo.R;
import com.wuguangxin.morescrolldemo.view.XinInnerListView;
import java.util.ArrayList;
import java.util.List;
/**
* ListView In ScrollView
*
* @author wuguangxin
* @date 16/7/5 上午10:59
*/
public class ListViewInScrollViewActivity extends FragmentActivity {
private int maxListSize = 300;
private List list;
private MyAdapter mAdapter;
private int i;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview);
setTitle("ListView In ScrollView");
XinInnerListView mListView = (XinInnerListView) findViewById(R.id.listview);
list = getList();
mAdapter = new MyAdapter(this, list);
mListView.setAdapter(mAdapter);
mListView.setOnLastItemVisibleListener(new XinInnerListView.OnLastItemVisibleListener() {
@Override
public void onLastItemVisible(AbsListView view, int position, Adapter adapter) {
if(list.size() < maxListSize){
list.addAll(getList());
mAdapter.setData(list);
mAdapter.notifyDataSetChanged();
}
}
});
}
private List getList(){
if(list == null){
list = new ArrayList<>();
}
List tempList = new ArrayList<>();
for (i = 0; i < 50; i++) {
tempList.add("item " + (this.list.size() + i+1) + " / "+maxListSize);
}
return tempList;
}
public class MyAdapter extends BaseAdapter {
private List list;
private Context context;
public MyAdapter(Context context, List list) {
this.context = context;
this.list = list;
}
@Override
public int getCount() {
return list == null ? 0 : list.size();
}
@Override
public String getItem(int position) {
return list == null ? null : list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder = null;
if(convertView == null){
holder = new Holder();
convertView = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, null);
holder.mTextView = (TextView)convertView;
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
}
holder.mTextView.setText(getItem(position));
return convertView;
}
public void setData(List list) {
this.list = list;
}
class Holder {
private TextView mTextView;
}
}
}
activity_listview.java