HorizontalScrollView no ScrollView Touch Handling

226

Eu tenho um ScrollView que envolve todo o meu layout, para que a tela inteira seja rolável. O primeiro elemento que tenho neste ScrollView é um bloco HorizontalScrollView que possui recursos que podem ser rolados horizontalmente. Adicionei um ouvinte ontouch à visualização horizontal de rolagem para manipular eventos de toque e forçar a exibição a "encaixar" na imagem mais próxima do evento ACTION_UP.

Portanto, o efeito que pretendo é como a tela inicial do Android, onde você pode rolar de um para o outro e ele se encaixa em uma tela quando você levanta o dedo.

Tudo isso funciona muito bem, exceto por um problema: preciso deslizar da esquerda para a direita quase perfeitamente na horizontal para que um ACTION_UP se registre. Se eu deslizar verticalmente no mínimo (o que eu acho que muitas pessoas costumam fazer em seus telefones quando deslizam de um lado para o outro), receberei um ACTION_CANCEL em vez de um ACTION_UP. Minha teoria é que isso ocorre porque a visualização horizontal de rolagem está dentro de uma visualização de rolagem e a visualização de rolagem está sequestrando o toque vertical para permitir a rolagem vertical.

Como posso desativar os eventos de toque da visualização de rolagem apenas dentro da visualização de rolagem horizontal, mas ainda permitir a rolagem vertical normal em outro local da visualização de rolagem?

Aqui está uma amostra do meu código:

   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;
        }
    }
}
Joel
fonte
Eu tentei todos os métodos neste post, mas nenhum deles funciona para mim. Eu estou usando MeetMe's HorizontalListViewbiblioteca.
The Nomad
Há um artigo com código semelhante ( HomeFeatureLayout extends HorizontalScrollView) aqui velir.com/blog/index.php/2010/11/17/… Existem alguns comentários adicionais sobre o que está acontecendo enquanto a classe de rolagem personalizada é composta.
CJBS

Respostas:

280

Atualização: Eu descobri isso. No meu ScrollView, eu precisava substituir o método onInterceptTouchEvent para interceptar apenas o evento de toque se o movimento Y for> o movimento X. Parece que o comportamento padrão de um ScrollView é interceptar o evento de toque sempre que houver QUALQUER movimento Y. Portanto, com a correção, o ScrollView só interceptará o evento se o usuário estiver rolando deliberadamente na direção Y e, nesse caso, passar o ACTION_CANCEL para os filhos.

Aqui está o código da minha classe Scroll View que contém o 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);
        }
    }
}
Joel
fonte
eu estou usando isso mesmo, mas no momento da rolagem na direção x, estou recebendo a exceção NULL POINTER EXCEPTION no public boolean onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } Eu tenho usado horizontall rolagem dentro scrollview
Vipin Sahu
@All graças que está funcionando, eu esqueça de inicializado o detector de gesto no construtor scrollview
Vipin Sahu
Como devo usar este código para implementar um ViewPager dentro ScrollView
Harsha MV
Eu tive alguns problemas com esse código quando tinha uma exibição de grade sempre expandida, às vezes ele não rolava. Eu tive que substituir o método onDown (...) no YScrollDetector para sempre retornar true, conforme sugerido em toda a documentação (como aqui developer.android.com/training/custom-views/… ) Isso resolveu meu problema.
Nemanja Kovacevic
3
Acabei de encontrar um pequeno bug que vale a pena mencionar. Acredito que o código no onInterceptTouchEvent deve dividir as duas chamadas booleanas, para garantir que mGestureDetector.onTouchEvent(ev)elas serão chamadas. Como é agora, não será chamado se super.onInterceptTouchEvent(ev)for falso. Acabei de encontrar um caso em que crianças clicáveis ​​na visualização de rolagem podem capturar os eventos de toque e o onScroll não será chamado. Caso contrário, obrigado, ótima resposta!
Glee
176

Obrigado Joel por me dar uma pista sobre como resolver esse problema.

Simplifiquei o código (sem a necessidade de um GestureDetector ) para obter o mesmo efeito:

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);
    }
}
neevek
fonte
excelente, obrigado por este refator. Eu estava tendo problemas com a abordagem acima ao rolar para a parte inferior do listView, pois qualquer comportamento de toque em elementos filho começou a não ser interceptado, independentemente da proporção de movimento Y / X. Esquisito!
Dori
1
Obrigado! Também trabalhou com um ViewPager dentro de um ListView, com um ListView personalizado.
Sharief Shaik
2
Acabei de substituir a resposta aceita por esta e está funcionando muito melhor para mim agora. Obrigado!
10119 David Scott
1
@VipinSahu, para informar a direção do movimento do toque, você pode pegar o delta da coordenada X atual e lastX, se for maior que 0, o toque está se movendo da esquerda para a direita, caso contrário, da direita para a esquerda. E então você salva o X atual como lastX para o próximo cálculo.
neevek
1
e quanto ao scrollview horizontal?
Zin Win Htet
60

Acho que encontrei uma solução mais simples, só que isso usa uma subclasse do ViewPager em vez do ScrollView (seu pai).

ATUALIZAÇÃO 16-07-2013 : também adicionei uma substituição onTouchEvent. Possivelmente, poderia ajudar com os problemas mencionados nos comentários, embora o 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;
    }
}

Isso é semelhante à técnica usada no onScroll () do android.widget.Gallery . É explicado ainda mais pela apresentação do Google I / O 2013, Writing Views personalizadas para Android .

Atualização 10/10/2013 : Uma abordagem semelhante também é descrita em uma postagem de Kirill Grouchnikov sobre o aplicativo (então) Android Market .

Giorgos Kylafas
fonte
booleano ret = super.onInterceptTouchEvent (ev); só voltou falso para mim. Usando on UninterceptableViewPager em um ScrollView
scottyab
Isso não funciona para mim, embora eu goste da simplicidade. Eu uso um ScrollViewcom um LinearLayoutno qual o UninterceptableViewPageré colocado. Na verdade, retsempre é falso ... Alguma pista de como consertar isso?
Peterdk 27/05
@scottyab & @Peterdk: Bem, o meu está em um TableRowque está dentro de um TableLayoutque está dentro de um ScrollView(sim, eu sei ...), e está funcionando conforme o esperado. Talvez você possa tentar substituir em onScrollvez de onInterceptTouchEvent, como o Google faz (linha 1010)
Giorgos Kylafas
Acabei de codificar ret como verdadeiro e funciona bem. Ele estava retornando falso mais cedo, mas eu acredito que é porque o scrollview tem um layout linear que detém todas as crianças
Amanni
11

Descobri que algumas vezes um ScrollView recupera o foco e o outro perde o foco. Você pode impedir isso, concedendo apenas um dos focos 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;
        }
    });
Marius Hilarious
fonte
que adaptador vc esta passando?
Harsha MV
Verifiquei novamente e percebi que não é realmente um ScrollView que estou usando, mas um ViewPager e estou passando um FragmentStatePagerAdapter que inclui todas as imagens da galeria.
Marius Hilariante
8

Não estava funcionando bem para mim. Eu mudei e agora funciona sem problemas. Se alguém estiver interessado.

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;
        }
    }
}
snapix
fonte
Esta é a resposta para o meu projeto. Eu tenho um pager de exibição que funciona como uma galeria pode ser clicado em uma visualização de rolagem. Eu uso a solução fornecida acima, ela funciona na rolagem horizontal, mas depois que clico na imagem do pager que inicia uma nova atividade e volta, o pager não pode rolar. Isso funciona bem, tks!
longkai 29/08/14
Funciona perfeitamente para mim. Eu tinha uma visualização "deslizar para desbloquear" personalizada dentro de uma visualização de rolagem, o que estava me dando o mesmo problema. Esta solução resolveu o problema.
híbrido
6

Graças a Neevek, sua resposta funcionou para mim, mas não bloqueia a rolagem vertical quando o usuário começa a rolar a exibição horizontal (ViewPager) na direção horizontal e, sem levantar o dedo na vertical, ele começa a rolar a exibição do contêiner subjacente (ScrollView) . Corrigi-o fazendo uma pequena alteração no código de 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);

}
Saqib
fonte
5

Isso finalmente se tornou parte da biblioteca de suporte v4, NestedScrollView . Portanto, não é mais necessário usar hacks locais na maioria dos casos.

Ebrahim Byagowi
fonte
1

A solução de Neevek funciona melhor que a de Joel em dispositivos executando 3.2 e acima. Há um erro no Android que causa o java.lang.IllegalArgumentException: pointerIndex fora do alcance, se um detector de gestos for usado dentro de uma exibição de scoll. Para duplicar o problema, implemente um scollview personalizado, conforme sugerido por Joel, e coloque um pager de exibição dentro. Se você arrastar (não levante a figura) para uma direção (esquerda / direita) e depois para o oposto, você verá a falha. Também na solução de Joel, se você arrastar o pager da visualização movendo o dedo na diagonal, assim que sair da área de visualização do conteúdo do pager da visualização, o pager retornará à sua posição anterior. Todas essas questões têm mais a ver com o design interno do Android ou a falta dele do que a implementação de Joel, que por si só é um código inteligente e conciso.

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

Don
fonte