1. ホーム
  2. アンドロイド

[解決済み】ScrollView内のHorizontalScrollViewのタッチハンドリング

2022-04-18 07:35:33

質問

画面全体がスクロールできるように、レイアウト全体を囲むScrollViewを持っています。 このScrollViewの最初の要素は、水平方向にスクロールできる機能を持つHorizontalScrollViewブロックです。 タッチ イベントを処理するために、ontouchlistener を horizontalscrollview に追加し、ACTION_UP イベントで最も近い画像にビューを強制的に "snap"しています。

つまり、アンドロイドの純正ホームスクリーンのように、次から次へとスクロールできて、指を離すと1つの画面に収まるような効果を狙っているのです。

これは、1つの問題を除いてすべてうまくいきます。ACTION_UPを登録するためには、ほぼ完全に水平に左から右へスワイプする必要があります。 最低でも縦にスワイプすると(多くの人が携帯電話で左右にスワイプするときにやりがちだと思いますが)、ACTION_UPの代わりにACTION_CANCELが表示されます。 これは、horizontalscrollviewが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;
        }
    }
}

解決方法は?

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

以下は、HorizontalScrollView を含む、私の 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);
        }
    }
}