HorizontalScrollView w obszarze ScrollView Obsługa dotykowa
Mam widok przewijania, który otacza cały mój układ, dzięki czemu cały ekran jest przewijany. Pierwszym elementem, który mam w tym ScrollView jest blok HorizontalScrollView, który ma funkcje, które można przewijać w poziomie. Dodałem ontouchlistener do horizontalscrollview, aby obsługiwać zdarzenia dotykowe i wymusić "przyciąganie" widoku do najbliższego obrazu w zdarzeniu ACTION_UP.
Więc efekt mam zamiar jest jak stock Android homescreen, gdzie można może przewijać z jednego do drugiego i przyciąga się do jednego ekranu po uniesieniu palca.
To wszystko działa świetnie, z wyjątkiem jednego problemu: muszę przesunąć palcem od lewej do prawej prawie idealnie poziomo, aby ACTION_UP kiedykolwiek się zarejestrował. Jeśli przesuwam pionowo w co najmniej (co myślę, że wiele osób ma tendencję do robienia na swoich telefonach podczas przesuwania obok siebie), otrzymam ACTION_CANCEL zamiast ACTION_UP. Moja teoria jest taka, że dzieje się tak dlatego, że horizontalscrollview znajduje się w scrollview, a scrollview przejmuje pionowy dotyk, aby umożliwić przewijanie w pionie.
Jak wyłączyć zdarzenia dotykowe dla widoku przewijania tylko z poziomu przewijania, ale nadal zezwalać na normalne przewijanie w pionie w innym miejscu widoku przewijania?
Oto próbka mojego kodu:
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;
}
}
}
8 answers
Update: rozgryzłem to. W widoku przewijania musiałem nadpisać metodę onInterceptTouchEvent, aby przechwycić Zdarzenie dotyku tylko wtedy, gdy ruch Y jest > ruch X. Wygląda na to, że domyślnym zachowaniem widoku przewijania jest przechwytywanie zdarzenia dotykowego, gdy występuje jakikolwiek ruch Y. Tak więc z poprawką, ScrollView przechwyci Zdarzenie tylko wtedy, gdy użytkownik celowo przewija w kierunku Y i w takim przypadku przekaże action_cancel dzieciom.
Oto kod dla mojej klasy Scroll View, która zawiera HorizontalScrollView:
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);
}
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-30 05:14:51
Dziękuję Joel za podpowiedź, jak rozwiązać ten problem.
Uprościłem kod (bez potrzeby użycia GestureDetector), aby osiągnąć ten sam efekt:
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);
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-19 09:50:20
Myślę, że znalazłem prostsze rozwiązanie, tylko to używa podklasy ViewPager zamiast (jego rodzica) ScrollView.
Aktualizacja 2013-07-16 : dodałem również nadpisanie dla onTouchEvent
. To może pomóc w kwestiach wymienionych w komentarzach, chociaż YMMV.
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;
}
}
[[2]}jest to podobne do techniki używanej w Androidzie.widget.Gallery ' s onScroll () .
Jest to dodatkowo wyjaśnione przez prezentację Google I / O 2013 pisanie niestandardowych widoków dla Android .
Aktualizacja 2013-12-10: podobne podejście opisano również w post Kirilla Grouchnikova na temat (wtedy) aplikacji Android Market .
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-16 07:56:42
Dowiedziałem się, że niektóre rzeczy jeden ScrollView odzyskuje ostrość, a drugi traci ostrość. Można temu zapobiec, przyznając tylko jeden z fokusów 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;
}
});
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-27 20:20:21
To nie działało dobrze dla mnie. Zmieniłem go i teraz działa sprawnie. Jeśli ktoś jest zainteresowany.
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;
}
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-19 10:10:14
Dzięki Neevkowi jego odpowiedź zadziałała na mnie, ale nie blokuje przewijania w pionie, gdy użytkownik zaczął przewijać widok poziomy(ViewPager) w kierunku poziomym, a następnie bez podnoszenia palca przewijania w pionie zaczyna przewijać podstawowy widok kontenera (ScrollView). Naprawiłem to, dokonując drobnej zmiany w kodzie Neevaka:
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);
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-07 18:38:29
To w końcu stało się częścią biblioteki support v4, NestedScrollView . W większości przypadków nie są już potrzebne lokalne włamania.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-29 15:11:22
Rozwiązanie Neevka działa lepiej niż Joela na urządzeniach z systemem 3.2 i nowszym. W Androidzie jest błąd, który spowoduje Javę.lang.IllegalArgumentException: pointerIndex poza zasięgiem, jeśli w widoku scollview używany jest detektor gestów. Aby zduplikować problem, zaimplementuj niestandardowy widok scollview zgodnie z sugestią Joela i umieść w nim pager widoku. Jeśli przeciągniesz (nie podnosisz figury) w jednym kierunku (lewo/prawo), a następnie w przeciwnym kierunku, zobaczysz awarię. Również w rozwiązaniu Joela, jeśli przeciągniesz wyświetl pager przesuwając palec po przekątnej, gdy palec opuści obszar widoku zawartości pagera, pager powróci do poprzedniej pozycji. Wszystkie te problemy są bardziej związane z wewnętrznym projektem Androida lub jego brakiem niż implementacją Joela, która sama w sobie jest kawałkiem inteligentnego i zwięzłego kodu.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-01 20:53:32