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


226

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

だから私がしようとしている効果は、あなたが一方から他方へスクロールすることができ、指を離すと一つの画面にスナップする、ストックのアンドロイドのホームスクリーンのようなものです。

これは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;
        }
    }
}

この投稿ではすべての方法を試しましたが、どれもうまくいきませんでした。MeetMe's HorizontalListViewライブラリを使用しています。
ノマド2014年

velir.com/blog/index.php/2010/11/17/…HomeFeatureLayout extends HorizontalScrollViewここに同様のコード()を含む記事 があります。カスタムスクロールクラスが構成されているときに何が行われているのかについて、いくつかの追加コメントがあります。
CJBS 2014

回答:


280

更新:私はこれを理解しました。私のScrollViewでは、YモーションがXモーションよりも大きい場合にのみ、タッチイベントをインターセプトするようにonInterceptTouchEventメソッドをオーバーライドする必要がありました。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);
        }
    }
}

これを同じに使用していますが、x方向にスクロールするときに、パブリックブール値で例外NULL POINTER EXCEPTIONを取得していますonInterceptTouchEvent(MotionEvent ev){return super.onInterceptTouchEvent(ev)&& mGestureDetector.onTouchEvent(ev); 私はscrollview内で水平スクロールを使用しました
Vipin Sahu

@すべてのおかげでそれが機能している、私はscrollviewコンストラクターでジェスチャー検出器を初期化するのを忘れていました
Vipin Sahu

ScrollView内にViewPagerを実装するためにこのコードをどのように使用すればよいですか
Harsha MV

グリッドビューが常に展開されていると、このコードに問題が発生しました。スクロールできない場合があります。ドキュメント全体で提案されているように、YScrollDetectorのonDown(...)メソッドをオーバーライドして常にtrueを返す必要がありました(例:developer.android.com/training/custom-views/…)これで問題が解決しました。
Nemanja Kovacevic 2013

3
言及する価値のある小さなバグに遭遇しました。onInterceptTouchEventのコードは、2つのブール呼び出しを分割して、mGestureDetector.onTouchEvent(ev)呼び出されることを保証する必要があると思います。現在のように、super.onInterceptTouchEvent(ev)がfalseの場合は呼び出されません。私はちょうど、scrollview内のクリック可能な子がタッチイベントを取得でき、onScrollがまったく呼び出されない場合に遭遇しました。それ以外の場合は、ありがとう、素晴らしい答えです!
GLee 2013年

176

この問題を解決するための手がかりをくれてくれて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);
    }
}

このリファクタリングをありがとう。Y / Xの移動率に関係なく、子要素に対するタッチ動作がインターセプトされなくなったため、listViewの下部までスクロールすると、上記のアプローチで問題が発生しました。おかしい!
通り

1
ありがとう!カスタムListViewを使用して、ListView内のViewPagerとも連携しました。
Sharief Shaik

2
受け入れられた答えをこれに置き換えただけで、今では私にとってずっとうまくいきます。ありがとう!
デビッドスコット

1
@VipinSahu、タッチの移動方向を示すために、現在のX座標とlastXのデルタを取得できます。0より大きい場合、タッチは左から右に移動し、それ以外の場合は右から左に移動します。そして、次の計算のために、現在のXをlastXとして保存します。
2012

1
水平スクロールビューはどうですか?
Zin Win Htet 14

60

私はもっ​​と簡単な解決策を見つけたと思います。これだけが(親)ScrollViewの代わりにViewPagerのサブクラスを使用します。

UPDATE 2013-07-16:オーバーライドも追加しましたonTouchEvent。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マーケットアプリに関するKirill Grouchnikovの投稿にも記載されています。


ブールret = super.onInterceptTouchEvent(ev); 私のために偽りを返しただけです。ScrollviewのUninterceptableViewPagerでの使用
scottyab '13

これは私にはうまくいきませんが、シンプルさは気に入っています。私が使用ScrollViewしてLinearLayout中にUninterceptableViewPager置かれています。確かに、ret常に偽です...これを修正する手がかりはありますか?
Peterdk 2013年

@scottyab&@Peterdk:まあ、鉱山は中であるTableRowの内側にあるTableLayoutの内側にあるScrollView(ええ、私は知っている...)、および意図したようにそれが働いています。たぶん、Googleのよう(1010行目)のonScroll代わりにオーバーライドを試すことができますonInterceptTouchEvent
Giorgos Kylafas 2013年

retをtrueとしてハードコードしたところ、問題なく動作しました。以前はfalseを返していましたが、それはスクロールビューがすべての子を保持する線形レイアウトを持っているためだと思います
Amanni

11

1つのScrollViewがフォーカスを取り戻し、もう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;
        }
    });

どんなアダプターを渡していますか?
Harsha MV

もう一度確認したところ、これは実際に使用しているScrollViewではなく、ViewPagerであり、ギャラリーのすべての画像を含むFragmentStatePagerAdapterを渡しています。
Marius Hilarious

8

うまくいかなかった。私はそれを変更し、今それはスムーズに動作します。興味があれば。

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

これが私のプロジェクトの答えです。ギャラリーとして機能するビューページャーがスクロールビューでクリックできます。私は上記のソリューションを使用しています。水平スクロールで機能しますが、新しいアクティビティを開始して戻るページャーの画像をクリックした後、ページャーがスクロールできません。これはうまくいきます、TKS!
ロングカイ2014

私にとっては完璧に機能します。スクロールビュー内にカスタムの「スワイプしてロックを解除する」ビューがあり、同じ問題が発生しました。このソリューションは問題を解決しました。
ハイブリッド

6

Neevekのおかげで彼の答えはうまくいきましたが、ユーザーが水平方向のビュー(ViewPager)を水平方向にスクロールし始めたときに垂直スクロールをロックせず、指のスクロールを垂直方向に持ち上げずに、下にあるコンテナービュー(ScrollView)をスクロールし始めます。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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.