web-dev-qa-db-ja.com

ScrollViewタッチ処理内のHorizo​​ntalScrollView

画面全体をスクロールできるように、レイアウト全体を囲むScrollViewがあります。このScrollViewの最初の要素は、水平方向にスクロールできる機能を持つHorizo​​ntalScrollViewブロックです。 ontouchlistenerをhorizo​​ntalscrollviewに追加して、タッチイベントを処理し、ビューをACTION_UPイベントの最も近い画像に「スナップ」させます。

したがって、私がしようとしている効果は、ストックAndroidホーム画面のようなもので、ここでスクロールして、指を離すと1つの画面にスナップします。

これは、1つの問題を除いてすべてうまく機能します。ACTION_UPを登録するには、左から右にほぼ完全に水平にスワイプする必要があります。少なくとも垂直方向にスワイプすると(多くの人が携帯電話で左右にスワイプするときにそうする傾向があると思います)、ACTION_UPではなくACTION_CANCELを受け取ります。私の理論では、これはhorizo​​ntalscrollviewがscrollview内にあり、scrollviewが垂直タッチをハイジャックして垂直スクロールを可能にしているためだというものです。

水平スクロールビュー内からスクロールビューのタッチイベントを無効にしながら、スクロールビュー内の他の場所で通常の垂直スクロールを許可するにはどうすればよいですか?

これが私のコードのサンプルです:

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

}

223
Joel

更新:これを理解しました。 ScrollViewで、onInterceptTouchEventメソッドをオーバーライドして、YモーションがXモーションよりも大きい場合にのみタッチイベントをインターセプトする必要がありました。 ScrollViewのデフォルトの動作は、Yモーションが発生するたびにタッチイベントをインターセプトするようです。そのため、修正により、ScrollViewはユーザーが意図的にY方向にスクロールしている場合にのみイベントをインターセプトし、その場合はACTION_CANCELを子に渡します。

Horizo​​ntalScrollViewを含むScroll Viewクラスのコードは次のとおりです。

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

この問題を解決する方法について手がかりを与えてくれた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);
    }
}
175
neevek

私はよりシンプルなソリューションを見つけたと思いますが、これは(親)ScrollViewの代わりにViewPagerのサブクラスを使用するだけです。

UPDATE 2013-07-16onTouchEventのオーバーライドも追加しました。 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;
    }
}

これは Android.widget.GalleryのonScroll()で使用される手法 と似ています。 Google I/O 2013プレゼンテーション Android向けカスタムビューの作成 でさらに説明されています。

2013-12-10を更新:同様のアプローチは (その後)Androidマーケットアプリ

60
Giorgos Kylafas

ScrollViewの1つがフォーカスを取り戻し、もう1つがフォーカスを失うことがあることがわかりました。 scrollViewフォーカスの1つだけを許可することで、これを防ぐことができます。

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

私にとってはうまくいきませんでした。変更して、スムーズに動作するようになりました。興味がある人なら。

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

Neevekのおかげで彼の答えはうまくいきましたが、ユーザーが水平方向(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);

}
6
Saqib

これは、最終的にサポートv4ライブラリ NestedScrollView の一部になりました。したがって、私が推測するほとんどの場合、ローカルハックはもはや必要ありません。

5
Ebrahim Byagowi

Neevekのソリューションは、3.2以降を実行しているデバイスでJoelのソリューションよりもうまく機能します。 Androidにはバグがあり、Java.lang.IllegalArgumentExceptionが発生します:scollview内でジェスチャ検出器が使用されている場合、pointerIndexが範囲外です。問題を再現するには、Joelが提案したようにカスタムscollviewを実装し、ビューページャーを内部に配置します。ドラッグ(図を持ち上げないでください)を一方の方向(左/右)にドラッグし、次に反対の方向にドラッグすると、クラッシュが発生します。また、Joelのソリューションでは、指を斜めに動かしてページャーをドラッグすると、指がページャーのコンテンツビュー領域から離れると、ページャーは元の位置に戻ります。これらの問題はすべて、それ自体がスマートで簡潔なコードであるJoelの実装よりも、Androidの内部設計またはそれの欠如に関係しています。

http://code.google.com/p/Android/issues/detail?id=1899

1
Don