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;
        }
    }
}

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);
        }
    }
}

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);
    }
}

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 .


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;
        }
    });

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;
        }
    }
}

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);

}


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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.