ScrollView触摸处理中的Horizo​​ntalScrollView

我有一个滚动视图,围绕我的整个布局,使整个屏幕滚动。 我在这个ScrollView中的第一个元素是一个Horizo​​ntalScrollView块,它具有可以水平滚动的function。 我已经在horizo​​ntalscrollview中添加了一个ontouchlistener来处理触摸事件,并强制视图“捕捉”ACTION_UP事件中最接近的图像。

所以我要去的效果就像股票的安卓主屏幕,你可以从一个滚动到另一个,当你拿起你的手指时,它会捕捉到一个屏幕。

这一切都很好,除了一个问题:我需要左右滑动几乎完全水平为ACTION_UP永远注册。 如果我至less垂直滑动(我认为很多人倾向于在左右滑动时使用手机),我将收到ACTION_CANCEL而不是ACTION_UP。 我的理论是,这是因为horizo​​ntalscrollview在滚动视图内,滚动视图劫持垂直触摸允许垂直滚动。

如何从我的水平滚动视图中禁用滚动视图的触摸事件,但是仍然允许在滚动视图中的其他地方进行正常的垂直滚动?

以下是我的代码示例:

public class HomeFeatureLayout extends HorizontalScrollView { private ArrayList<ListItem> items = null; private GestureDetector gestureDetector; View.OnTouchListener gestureListener; private static final int SWIPE_MIN_DISTANCE = 5; private static final int SWIPE_THRESHOLD_VELOCITY = 300; private int activeFeature = 0; public HomeFeatureLayout(Context context, ArrayList<ListItem> items){ super(context); setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); setFadingEdgeLength(0); this.setHorizontalScrollBarEnabled(false); this.setVerticalScrollBarEnabled(false); LinearLayout internalWrapper = new LinearLayout(context); internalWrapper.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); internalWrapper.setOrientation(LinearLayout.HORIZONTAL); addView(internalWrapper); this.items = items; for(int i = 0; i< items.size();i++){ LinearLayout featureLayout = (LinearLayout) View.inflate(this.getContext(),R.layout.homefeature,null); TextView header = (TextView) featureLayout.findViewById(R.id.featureheader); ImageView image = (ImageView) featureLayout.findViewById(R.id.featureimage); TextView title = (TextView) featureLayout.findViewById(R.id.featuretitle); title.setTag(items.get(i).GetLinkURL()); TextView date = (TextView) featureLayout.findViewById(R.id.featuredate); header.setText("FEATURED"); Image cachedImage = new Image(this.getContext(), items.get(i).GetImageURL()); image.setImageDrawable(cachedImage.getImage()); title.setText(items.get(i).GetTitle()); date.setText(items.get(i).GetDate()); internalWrapper.addView(featureLayout); } gestureDetector = new GestureDetector(new MyGestureDetector()); setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (gestureDetector.onTouchEvent(event)) { return true; } else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){ int scrollX = getScrollX(); int featureWidth = getMeasuredWidth(); activeFeature = ((scrollX + (featureWidth/2))/featureWidth); int scrollTo = activeFeature*featureWidth; smoothScrollTo(scrollTo, 0); return true; } else{ return false; } } }); } class MyGestureDetector extends SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { try { //right to left if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { activeFeature = (activeFeature < (items.size() - 1))? activeFeature + 1:items.size() -1; smoothScrollTo(activeFeature*getMeasuredWidth(), 0); return true; } //left to right else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { activeFeature = (activeFeature > 0)? activeFeature - 1:0; smoothScrollTo(activeFeature*getMeasuredWidth(), 0); return true; } } catch (Exception e) { // nothing } return false; } } 

}

更新:我明白了这一点。 在我的ScrollView中,我需要重写onInterceptTouchEvent方法,以便只在Y运动> X运动时拦截触摸事件。 看起来ScrollView的默认行为是在有任何Y动作的时候拦截触摸事件。 因此,如果用户故意在Y方向上滚动,ScrollView将只拦截该事件,并在这种情况下将ACTION_CANCEL传递给子节点。

这里是我的Scroll View类包含Horizo​​ntalScrollView的代码:

 public class CustomScrollView extends ScrollView { private GestureDetector mGestureDetector; public CustomScrollView(Context context, AttributeSet attrs) { super(context, attrs); mGestureDetector = new GestureDetector(context, new YScrollDetector()); setFadingEdgeLength(0); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev); } // Return false if we're scrolling in the x direction class YScrollDetector extends SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return Math.abs(distanceY) > Math.abs(distanceX); } } } 

谢谢Joel给我提供了解决这个问题的线索。

我简化了代码(不需要GestureDetector )来达到同样的效果:

 public class VerticalScrollView extends ScrollView { private float xDistance, yDistance, lastX, lastY; public VerticalScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); xDistance += Math.abs(curX - lastX); yDistance += Math.abs(curY - lastY); lastX = curX; lastY = curY; if(xDistance > yDistance) return false; } return super.onInterceptTouchEvent(ev); } } 

我想我find了一个更简单的解决scheme,只有这个使用ViewPager的子类而不是(它的父级)ScrollView。

更新 onTouchEvent :我也添加了onTouchEvent覆盖。 尽pipeYMMV,它可能有助于解决评论中提到的问题。

 public class UninterceptableViewPager extends ViewPager { public UninterceptableViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean ret = super.onInterceptTouchEvent(ev); if (ret) getParent().requestDisallowInterceptTouchEvent(true); return ret; } @Override public boolean onTouchEvent(MotionEvent ev) { boolean ret = super.onTouchEvent(ev); if (ret) getParent().requestDisallowInterceptTouchEvent(true); return ret; } } 

这与android.widget.Gallery的onScroll()中使用的技术类似。 Google I / O 2013演示文稿为Android编写自定义视图进一步说明了这一点。

更新2013年12月10日 :类似的方法也描述了从基里尔Grouchnikov关于(当时)Android Market应用程序的post 。

我发现一些ScrollView重新获得焦点,另一个失去焦点。 您可以通过只授予其中一个scrollView焦点来防止这种情况:

  scrollView1= (ScrollView) findViewById(R.id.scrollscroll); scrollView1.setAdapter(adapter); scrollView1.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { scrollView1.getParent().requestDisallowInterceptTouchEvent(true); return false; } }); 

感谢Neevek他的答案为我工作,但它不locking垂直滚动时,用户已经开始在水平方向滚动水平视图(ViewPager),然后不垂直垂直手指滚动它开始滚动底层容器视图(滚动视图) 。 我通过在Neevak的代码中稍作改动来解决这个问题:

 private float xDistance, yDistance, lastX, lastY; int lastEvent=-1; boolean isLastEventIntercepted=false; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; lastX = ev.getX(); lastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); xDistance += Math.abs(curX - lastX); yDistance += Math.abs(curY - lastY); lastX = curX; lastY = curY; if(isLastEventIntercepted && lastEvent== MotionEvent.ACTION_MOVE){ return false; } if(xDistance > yDistance ) { isLastEventIntercepted=true; lastEvent = MotionEvent.ACTION_MOVE; return false; } } lastEvent=ev.getAction(); isLastEventIntercepted=false; return super.onInterceptTouchEvent(ev); } 

这对我来说不是很好。 我改变了它,现在它工作顺利。 如果有人感兴趣。

 public class ScrollViewForNesting extends ScrollView { private final int DIRECTION_VERTICAL = 0; private final int DIRECTION_HORIZONTAL = 1; private final int DIRECTION_NO_VALUE = -1; private final int mTouchSlop; private int mGestureDirection; private float mDistanceX; private float mDistanceY; private float mLastX; private float mLastY; public ScrollViewForNesting(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); } public ScrollViewForNesting(Context context, AttributeSet attrs) { this(context, attrs,0); } public ScrollViewForNesting(Context context) { this(context,null); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDistanceY = mDistanceX = 0f; mLastX = ev.getX(); mLastY = ev.getY(); mGestureDirection = DIRECTION_NO_VALUE; break; case MotionEvent.ACTION_MOVE: final float curX = ev.getX(); final float curY = ev.getY(); mDistanceX += Math.abs(curX - mLastX); mDistanceY += Math.abs(curY - mLastY); mLastX = curX; mLastY = curY; break; } return super.onInterceptTouchEvent(ev) && shouldIntercept(); } private boolean shouldIntercept(){ if((mDistanceY > mTouchSlop || mDistanceX > mTouchSlop) && mGestureDirection == DIRECTION_NO_VALUE){ if(Math.abs(mDistanceY) > Math.abs(mDistanceX)){ mGestureDirection = DIRECTION_VERTICAL; } else{ mGestureDirection = DIRECTION_HORIZONTAL; } } if(mGestureDirection == DIRECTION_VERTICAL){ return true; } else{ return false; } } } 

这终于成为支持v4库, NestedScrollView的一部分 。 所以,我猜大部分情况下不再需要本地黑客。

在运行3.2及更高版本的设备上,Neevek的解决scheme比Joel的更好。 Android中有一个错误会导致java.lang.IllegalArgumentException:如果在scollview中使用手势检测器,pointerIndex超出范围。 为了复制这个问题,像Joelbuild议的那样实现一个自定义的scollview,并在里面放置一个view pager。 如果你拖动(不提起你的身影)到一个方向(左/右),然后到相反的方向,你会看到崩溃。 同样在Joel的解决scheme中,如果通过对angular移动手指来拖动视图寻呼机,一旦手指离开视图寻呼机的内容视图区域,寻呼机将弹回到其先前的位置。 所有这些问题都与Android的内部devise有关,或者比Joel的实现更缺乏,这本身就是一个简洁明了的代码。

http://code.google.com/p/android/issues/detail?id=18990