HorizontalScrollView ภายใน ScrollView Touch Handling


226

ฉันมี ScrollView ที่ล้อมรอบเค้าโครงทั้งหมดของฉันเพื่อให้หน้าจอทั้งหมดเลื่อนได้ องค์ประกอบแรกที่ฉันมีใน ScrollView นี้คือบล็อก HorizontalScrollView ที่มีคุณสมบัติที่สามารถเลื่อนดูได้ในแนวนอน ฉันได้เพิ่มออนทัชลิสเทอร์เนอร์ใน horizontalscrollview เพื่อจัดการกิจกรรมการสัมผัสและบังคับให้มุมมอง "สแน็ป" ไปยังภาพที่ใกล้ที่สุดในเหตุการณ์ ACTION_UP

ดังนั้นเอฟเฟกต์ที่ฉันจะทำก็เหมือนกับหน้าจอหลักของ Android ที่คุณสามารถเลื่อนจากหน้าจอหนึ่งไปอีกหน้าจอหนึ่งและมันจะล็อคหน้าจอเดียวเมื่อคุณยกนิ้วขึ้น

ทั้งหมดนี้ใช้งานได้ดียกเว้นปัญหาเดียว: ฉันต้องกวาดนิ้วจากซ้ายไปขวาเกือบจะสมบูรณ์ในแนวนอนเพื่อให้ ACTION_UP ลงทะเบียนเลย ถ้าฉันปัดในแนวตั้งอย่างน้อยที่สุด (ซึ่งฉันคิดว่าหลายคนมักจะทำบนโทรศัพท์ของพวกเขาเมื่อปัดไปด้านข้าง) ฉันจะได้รับ ACTION_CANCEL แทนที่จะเป็น ACTION_UP ทฤษฎีของฉันคือว่าสิ่งนี้เป็นเพราะ horizontalscrollview อยู่ภายใน scrollview และ scrollview นั้นถูกไฮแจ็คแตะแนวตั้งเพื่อให้สามารถเลื่อนแนวตั้งได้

ฉันจะปิดการใช้งานกิจกรรมการสัมผัสสำหรับ scrollview จากภายใน 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ห้องสมุด
Nomad

มีบทความที่มีรหัสที่คล้ายกัน ( HomeFeatureLayout extends HorizontalScrollView) ที่นี่velir.com/blog/index.php/2010/11/17/… มีความคิดเห็นเพิ่มเติมเกี่ยวกับสิ่งที่เกิดขึ้นเมื่อมีการสร้างคลาสเลื่อนที่กำหนดเอง
CJBS

คำตอบ:


280

อัปเดต: ฉันพบสิ่งนี้ บน ScrollView ของฉันฉันจำเป็นต้องแทนที่เมธอด onInterceptTouchEvent เพื่อดักจับเหตุการณ์การสัมผัสเท่านั้นหากการเคลื่อนไหว Y เป็น> การเคลื่อนไหว X ดูเหมือนว่าพฤติกรรมเริ่มต้นของ ScrollView จะขัดขวางเหตุการณ์การสัมผัสเมื่อใดก็ตามที่มีการเคลื่อนไหว Y ใด ๆ ดังนั้นเมื่อมีการแก้ไข ScrollView จะดักเหตุการณ์เท่านั้นหากผู้ใช้ตั้งใจเลื่อนไปในทิศทาง Y และในกรณีนั้นจะส่ง ACTION_CANCEL ไปให้เด็ก ๆ

นี่คือรหัสสำหรับคลาส Scroll View ของฉันที่มี 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);
        }
    }
}

ฉันใช้สิ่งนี้เหมือนกัน แต่ในขณะที่เลื่อนไปในทิศทาง x ฉันได้รับการยกเว้น NULL POINTER EXCEPTION ที่บูลีนสาธารณะ onInterceptTouchEvent (MotionEvent ev) {return super.onInterceptTouchEvent (ev) && mGestureDetector.onTouchEvent (ev); } ฉันใช้การเลื่อนในแนวนอนภายใน scrollview
Vipin Sahu

ขอบคุณ @ ทั้งหมดก็ทำงานผมลืมที่จะเตรียมใช้เครื่องตรวจจับท่าทางในตัวสร้าง scrollview
Vipin Sahu

ฉันควรใช้รหัสนี้ในการปรับใช้ ViewPager ใน ScrollView
Harsha MV

ฉันมีปัญหาบางอย่างกับรหัสนี้เมื่อฉันมีมุมมองกริดที่ขยายเสมอในบางครั้งมันจะไม่เลื่อน ฉันต้องแทนที่เมธอด onDown (... ) ใน YScrollDetector เพื่อคืนค่าจริงตามที่แนะนำตลอดทั้งเอกสาร (เช่นที่นี่developer.android.com/training/custom-views/… ) ปัญหานี้แก้ไขได้
Nemanja Kovacevic

3
ฉันเพิ่งพบข้อบกพร่องเล็ก ๆ ที่ควรค่าแก่การกล่าวถึง ฉันเชื่อว่ารหัสใน onterceptTouchEvent ควรแยกการเรียกบูลีนสองครั้งเพื่อรับประกันว่าmGestureDetector.onTouchEvent(ev)จะได้รับการเรียก เหมือนตอนนี้มันจะไม่ถูกเรียกถ้าsuper.onInterceptTouchEvent(ev)เป็นเท็จ ฉันเพิ่งพบกรณีที่เด็ก ๆ ที่คลิกได้ใน scrollview สามารถคว้าเหตุการณ์การสัมผัสและ onScroll จะไม่ถูกเรียกเลย มิฉะนั้นขอบคุณคำตอบที่ดี!
GLee

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

ยอดเยี่ยมขอบคุณสำหรับ refactor นี้ ฉันได้รับปัญหาด้วยวิธีการข้างต้นเมื่อเลื่อนไปที่ด้านล่างของ listView เนื่องจากพฤติกรรมการแตะใด ๆ เหนือองค์ประกอบลูกเริ่มที่จะไม่ถูกดักไม่ว่าอัตราส่วนการเคลื่อนไหว Y / X จะเป็นเท่าไหร่ แปลก!
Dori

1
ขอบคุณ! ทำงานร่วมกับ ViewPager ภายใน ListView ด้วย ListView แบบกำหนดเอง
Sharief Shaik

2
เพิ่งแทนที่คำตอบที่ยอมรับได้ด้วยสิ่งนี้และมันก็ใช้ได้ดีกว่าสำหรับฉันแล้ว ขอบคุณ!
David Scott

1
@VipinSahu เพื่อบอกทิศทางของการเคลื่อนที่แบบสัมผัสคุณสามารถใช้เดลต้าของพิกัด X ปัจจุบันและ lastX หากมันมีค่ามากกว่า 0 การสัมผัสจะเคลื่อนจากซ้ายไปขวามิฉะนั้นจากขวาไปซ้าย จากนั้นคุณบันทึก X ปัจจุบันเป็น lastX สำหรับการคำนวณครั้งต่อไป
neevek

1
สิ่งที่เกี่ยวกับ scrollview แนวนอน?
Zin Win Htet

60

ฉันคิดว่าฉันพบวิธีที่ง่ายกว่าเพียงแค่ใช้ subclass ของ ViewPager แทน ScrollView (เป็น parent)

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 การเขียนมุมมองที่กำหนดเองสำหรับ AndroidAndroid

ปรับปรุง 2013/12/10 : วิธีการที่คล้ายกันนอกจากนี้ยังได้อธิบายไว้ในโพสต์จากคิริลล์ Grouchnikov เกี่ยวกับ (แล้ว) แอพพลิเค


บูลีน ret = super.onInterceptTouchEvent (ev); เคยกลับเท็จสำหรับฉัน ใช้ใน UninterceptableViewPager ใน scrollview
scottyab

มันใช้งานไม่ได้สำหรับฉัน แต่ฉันชอบความเรียบง่าย ผมใช้ScrollViewกับLinearLayoutที่UninterceptableViewPagerวางอยู่ แน่นอนretเสมอว่าเป็นเท็จ ... เบาะแสใด ๆ ที่จะแก้ไขปัญหานี้ได้อย่างไร
Peterdk

@scottyab & @Peterdk: ดีฉันอยู่ในTableRowที่TableLayoutที่อยู่ภายในScrollView(ใช่ฉันรู้ ... ) และมันทำงานตามที่ตั้งใจไว้ บางทีคุณอาจลองเอาชนะonScrollแทนonInterceptTouchEventเช่นเดียวกับGoogle (บรรทัด 1010)
Giorgos Kylafas

ฉันเพิ่ง hardcoded ret เป็นจริงและใช้งานได้ดี มันกลับมาผิดพลาดก่อนหน้านี้ แต่ฉันเชื่อว่าเป็นเพราะ scrollview มีรูปแบบเชิงเส้นซึ่งให้เด็กทุกคน
Amanni

11

ฉันพบว่าบางครั้งหนึ่ง ScrollView ฟื้นโฟกัสและอื่น ๆ สูญเสียโฟกัส คุณสามารถป้องกันสิ่งนั้นได้โดยให้สิทธิ์หนึ่งเดียวกับ scrollView focus:

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

นี่คือคำตอบสำหรับโครงการของฉัน ฉันมีเพจเจอร์ดูซึ่งทำหน้าที่เป็นแกลเลอรีสามารถคลิกใน scrollview ฉันใช้วิธีแก้ปัญหาที่ให้ไว้ข้างต้นมันทำงานบนการเลื่อนในแนวนอน แต่หลังจากฉันคลิกที่รูปภาพของเพจเจอร์ซึ่งเริ่มกิจกรรมใหม่และย้อนกลับไปเพจเจอร์ไม่สามารถเลื่อนได้ มันใช้งานได้ดี tks!
longkai

ทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน ฉันมีมุมมอง "ปัดเพื่อปลดล็อก" ที่กำหนดเองภายในมุมมองแบบสลิปซึ่งทำให้ฉันมีปัญหาเดียวกัน วิธีนี้แก้ไขปัญหาได้
ไฮบริด

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

}

5

นี้ในที่สุดก็กลายเป็นส่วนหนึ่งของห้องสมุดสนับสนุน v4 เป็นNestedScrollView ดังนั้นไม่จำเป็นต้องแฮ็กท้องถิ่นอีกต่อไปสำหรับกรณีส่วนใหญ่ที่ฉันเดา


1

โซลูชันของ Neevek ทำงานได้ดีกว่า Joel บนอุปกรณ์ที่ใช้งาน 3.2 ขึ้นไป มีข้อผิดพลาดใน Android ที่จะทำให้ java.lang.IllegalArgumentException: pointerIndex อยู่นอกขอบเขตหากใช้ตัวตรวจจับท่าทางใน scollview ในการทำซ้ำปัญหาให้ใช้ scollview ที่กำหนดเองตามที่ Joel แนะนำและวางเพจเจอร์มุมมองไว้ข้างใน หากคุณลาก (อย่ายกรูป) ไปยังทิศทางเดียว (ซ้าย / ขวา) แล้วไปทางตรงข้ามคุณจะเห็นความผิดพลาด นอกจากนี้ในโซลูชันของโจเอลหากคุณลากมุมมองเพจเจอร์โดยเลื่อนนิ้วของคุณตามแนวทแยงมุมเมื่อนิ้วของคุณออกจากพื้นที่มุมมองเนื้อหาของเพจเจอร์เพจเจอร์เพจเจอร์จะย้อนกลับไปยังตำแหน่งก่อนหน้า ปัญหาทั้งหมดนี้เกี่ยวข้องกับการออกแบบภายในของ Android มากกว่าการใช้งานของโจเอลซึ่งเป็นรหัสที่ฉลาดและรัดกุม

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.