ScrollView 中的 HorizontalScrollView 触摸处理
我有一个 ScrollView 包围我的整个布局,以便整个屏幕都可以滚动。此 ScrollView 中的第一个元素是 HorizontalScrollView 块,它具有可以水平滚动的功能。我在horizontalscrollview中添加了一个ontouchlistener来处理触摸事件并强制视图“捕捉”到ACTION_UP事件上最接近的图像。
所以我想要的效果就像普通的 Android 主屏幕一样,你可以从一个屏幕滚动到另一个屏幕,当你抬起手指时它会捕捉到一个屏幕。
除了一个问题之外,这一切都很好:我需要几乎完美地水平从左向右滑动才能注册 ACTION_UP。如果我至少垂直滑动(我认为很多人在手机上左右滑动时往往会这样做),我将收到 ACTION_CANCEL 而不是 ACTION_UP。我的理论是,这是因为水平滚动视图位于滚动视图内,并且滚动视图劫持垂直触摸以允许垂直滚动。
如何从水平滚动视图中禁用滚动视图的触摸事件,但仍允许滚动视图中其他位置的正常垂直滚动?
这是我的代码示例:
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;
}
}
}
I have a ScrollView that surrounds my entire layout so that the entire screen is scrollable. The first element I have in this ScrollView is a HorizontalScrollView block that has features that can be scrolled through horizontally. I've added an ontouchlistener to the horizontalscrollview to handle touch events and force the view to "snap" to the closest image on the ACTION_UP event.
So the effect I'm going for is like the stock android homescreen where you can scroll from one to the other and it snaps to one screen when you lift your finger.
This all works great except for one problem: I need to swipe left to right almost perfectly horizontally for an ACTION_UP to ever register. If I swipe vertically in the very least (which I think many people tend to do on their phones when swiping side to side), I will receive an ACTION_CANCEL instead of an ACTION_UP. My theory is that this is because the horizontalscrollview is within a scrollview, and the scrollview is hijacking the vertical touch to allow for vertical scrolling.
How can I disable the touch events for the scrollview from just within my horizontal scrollview, but still allow for normal vertical scrolling elsewhere in the scrollview?
Here's a sample of my code:
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;
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
日期:2021 年 - 5 月 12 日
看起来很乱..但相信我,如果您想在垂直滚动视图中水平滚动任何视图,那么这是值得的!
有效在jetpack compose中,也可以通过创建自定义视图并扩展您想要水平滚动的视图;在垂直滚动视图中并在 AndroidView 可组合项中使用该自定义视图(现在,“Jetpack Compose 处于 1.0.0-beta06 版本”
如果您想自由水平和垂直滚动,这是最佳解决方案当你水平滚动时,没有垂直滚动条拦截你的触摸,并且只允许垂直滚动条在你垂直滚动水平滚动视图时拦截触摸:
这里,
ViewThatYouWannaScrollHorizontally
是视图您想要水平滚动,并且当您水平滚动时,您不希望垂直滚动条捕获触摸并认为“哦!用户正在垂直滚动,因此parent.requestDisallowInterceptTouchEvent( true)
基本上会说垂直滚动条“嘿,你!不要捕获任何触摸,因为用户正在水平滚动”在用户完成水平滚动并尝试垂直滚动后水平滚动条放置在垂直滚动条内,然后它会看到 Y 轴上的触摸速度大于 X 轴,这表明用户没有水平滚动,并且水平滚动条会说“嘿你!家长,你听到我了吗?..用户正在垂直滚动我,现在你可以拦截触摸并在垂直滚动中显示我下方的内容”
Date : 2021 - May 12
Looks jibberish..but trust me its worth the time if you wanna scroll any view horizontally in a vertical scrollview butter smooth!!
Works in jetpack compose as well by by making a custom view and extending the view that you wanna scroll horizontally in; inside a vertical scroll view and using that custom view inside AndroidView composable (Right now, "Jetpack Compose is in 1.0.0-beta06"
This is the most optimal solution if you wanna scroll horizontally freely and vertically freely without the vertical scrollbar intercepting ur touch when u are scrolling horizontally and only allowing the vertical scrollbar to intercept the touch when u are scrolling vertically through the horizontal scrolling view :
Here,
ViewThatYouWannaScrollHorizontally
is the view that you want to scroll horizontally in and when u are scrolling horizontally, you dont want the vertical scrollbar to capture the touch and think that "oh! the user is scrolling vertically soparent.requestDisallowInterceptTouchEvent(true)
will basically say the vertical scroll bar "hey you! dont capture any touch coz the user is scrolling horizontally"And after the user is done scrolling horizontally and tries to scroll vertically through the horizontal scrollbar which is placed inside a vertical scrollbar then it will see that the touch velocity in Y axis is greater than X axis, which shows user is not scrolling horizontally and the horizontal scrolling stuff will say "Hey you! parent, you hear me?..the user is scrolling vertically through me, now u can intercept the touch and show the stuffs present below me in the vertical scroll"
更新:我想通了。在我的 ScrollView 上,我需要重写 onInterceptTouchEvent 方法,以便仅在 Y 运动 > 时拦截触摸事件。 X 运动。看起来 ScrollView 的默认行为是每当有任何 Y 运动时都会拦截触摸事件。因此,通过修复,ScrollView 将仅在用户故意沿 Y 方向滚动时拦截该事件,并且在这种情况下将 ACTION_CANCEL 传递给子级。
以下是包含 HorizontalScrollView 的滚动视图类的代码:
Update: I figured this out. On my ScrollView, I needed to override the onInterceptTouchEvent method to only intercept the touch event if the Y motion is > the X motion. It seems like the default behavior of a ScrollView is to intercept the touch event whenever there is ANY Y motion. So with the fix, the ScrollView will only intercept the event if the user is deliberately scrolling in the Y direction and in that case pass off the ACTION_CANCEL to the children.
Here is the code for my Scroll View class that contains the HorizontalScrollView:
谢谢乔尔给我提供了如何解决这个问题的线索。
我简化了代码(不需要 GestureDetector)来达到相同的效果:
Thank you Joel for giving me a clue on how to resolve this problem.
I have simplified the code(without need for a GestureDetector) to achieve the same effect:
我想我找到了一个更简单的解决方案,只是它使用 ViewPager 的子类而不是(其父级)ScrollView。
更新 2013-07-16:我还添加了
onTouchEvent
的覆盖。尽管 YMMV,它可能有助于解决评论中提到的问题。这类似于 android.widget.Gallery 的 onScroll() 中使用的技术。
Google I/O 2013 演示文稿对此进行了进一步解释 为 Android 编写自定义视图。
更新 2013-12-10:Kirill Grouchnikov 发布的有关(当时的)Android Market 应用的帖子。
I think I found a simpler solution, only this uses a subclass of ViewPager instead of (its parent) ScrollView.
UPDATE 2013-07-16: I added an override for
onTouchEvent
as well. It could possibly help with the issues mentioned in the comments, although YMMV.This is similar to the technique used in android.widget.Gallery's onScroll().
It is further explained by the Google I/O 2013 presentation Writing Custom Views for Android.
Update 2013-12-10: A similar approach is also described in a post from Kirill Grouchnikov about the (then) Android Market app.
我发现有时一个 ScrollView 重新获得焦点,而另一个 ScrollView 失去焦点。您可以通过仅授予滚动视图焦点之一来防止这种情况:
I've found out that somethimes one ScrollView regains focus and the other loses focus. You can prevent that, by only granting one of the scrollView focus:
这对我来说效果不佳。我改变了它,现在可以顺利运行了。如果有人有兴趣的话。
It wasn't working well for me. I changed it and now it works smoothly. If anyone interested.
感谢 Neevek,他的答案对我有用,但当用户开始在水平方向上滚动水平视图(ViewPager)时,它不会锁定垂直滚动,然后在不垂直抬起手指滚动的情况下,它开始滚动底层容器视图(ScrollView) 。我通过对 Neevak 的代码进行轻微更改来修复它:
Thanks to Neevek his answer worked for me but it doesn't lock the vertical scrolling when user has started scrolling the horizontal view(ViewPager) in horizontal direction and then without lifting the finger scroll vertically it starts to scroll the underlying container view(ScrollView). I fixed it by making a slight change in Neevak's code:
这最终成为支持 v4 库的一部分, NestedScrollView 。因此,我猜大多数情况下不再需要本地黑客。
This finally became a part of support v4 library, NestedScrollView. So, no longer local hacks is needed for most of cases I'd guess.
在运行 3.2 及更高版本的设备上,Neevek 的解决方案比 Joel 的解决方案效果更好。 Android 中存在一个错误,如果在 scollview 内使用手势检测器,则会导致 java.lang.IllegalArgumentException:pointerIndex out of range。要重复该问题,请按照 Joel 的建议实现自定义 scollview 并在其中放置一个视图寻呼机。如果您将(不要抬起您的人物)拖动到一个方向(左/右),然后拖动到相反的方向,您将看到崩溃。同样在 Joel 的解决方案中,如果通过对角移动手指来拖动视图寻呼机,一旦手指离开视图寻呼机的内容视图区域,寻呼机将弹回其之前的位置。所有这些问题更多地与 Android 的内部设计或缺乏它有关,而不是 Joel 的实现,Joel 的实现本身就是一段聪明而简洁的代码。
http://code.google.com/p/android/issues/detail ?id=18990
Neevek's solution works better than Joel's on devices running 3.2 and above. There is a bug in Android that will cause java.lang.IllegalArgumentException: pointerIndex out of range if a gesture detector is used inside a scollview. To duplicate the issue, implement a custom scollview as Joel suggested and put a view pager inside. If you drag (don't lift you figure) to one direction (left/right) and then to the opposite, you will see the crash. Also in Joel's solution, if you drag the view pager by moving your finger diagonally, once your finger leave the view pager's content view area, the pager will spring back to its previous position. All these issues are more to do with Android's internal design or lack of it than Joel's implementation, which itself is a piece of smart and concise code.
http://code.google.com/p/android/issues/detail?id=18990