سؤال

لدي عرض تمرير يحيط بتخطيطي بالكامل بحيث تكون الشاشة بأكملها قابلة للتمرير. العنصر الأول الذي لدي في هذا scrollview هو كتلة أفقية CrollView تحتوي على ميزات يمكن تمريرها من خلال أفقيًا. لقد أضفت onTouchListener إلى The HorizontalScrollView للتعامل مع أحداث اللمس وإجبار العرض على "التقاط" إلى أقرب صورة في حدث Action_UP.

لذا فإن التأثير الذي سأذهب إليه هو مثل شاشة HomeScreen 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;
        }
    }
}
هل كانت مفيدة؟

المحلول

تحديث: لقد اكتشفت هذا. على ScrollView الخاص بي ، كنت بحاجة إلى تجاوز طريقة OnInterceptOuChevent لاعتراض حدث اللمس فقط إذا كانت حركة y> حركة x. يبدو أن السلوك الافتراضي لمرض Scrollview هو اعتراض حدث اللمس كلما كان هناك أي حركة y. لذلك مع الإصلاح ، لن يعترض ScrollView إلا الحدث إذا كان المستخدم يمرد عمداً في اتجاه Y وفي هذه الحالة ينقل Action_Cancel إلى الأطفال.

فيما يلي رمز فئة عرض التمرير الخاصة بي التي تحتوي على 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);
        }
    }
}

نصائح أخرى

شكرا لك جويل لإعطائي فكرة عن كيفية حل هذه المشكلة.

لقد قمت بتبسيط الكود (دون الحاجة إلى ملف إيماءات) لتحقيق نفس التأثير:

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

أعتقد أنني وجدت حلًا أبسط ، فقط يستخدم فئة فرعية من ViewPager بدلاً من (الوالد) Scrollview.

تحديث 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's onscroll (). يتم شرحه أيضًا من خلال عرض Google I/O 2013 كتابة طرق عرض مخصصة لنظام Android.

تحديث 2013-12-10: تم وصف نهج مماثل أيضًا في منشور من Kirill Grouchnikov حول تطبيق سوق Android (ثم).

لقد اكتشفت أن Scrollview في بعض الأحيان يستعيد التركيز والآخر يفقد التركيز. يمكنك منع ذلك ، من خلال منح واحد فقط من التركيز على 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;
        }
    });

لم يكن يعمل بشكل جيد بالنسبة لي. لقد غيرته والآن يعمل بسلاسة. إذا كان أي شخص مهتم.

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

بفضل 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);

}

أصبح هذا أخيرًا جزءًا من مكتبة دعم V4 ، NETTERSCROLLVIEW. لذلك ، لم تعد هناك حاجة إلى الاختراقات المحلية لمعظم الحالات التي أعتقد.

يعمل حل Neevek بشكل أفضل من جويل على الأجهزة التي تعمل 3.2 وما فوق. هناك خطأ في Android من شأنه أن يتسبب في java.lang.IlegalArgumentException: FinInterIndex خارج النطاق إذا تم استخدام كاشف لفتة داخل scollview. لتكرار المشكلة ، قم بتنفيذ Scollview المخصص كما اقترح جويل ووضع Pager View Inside. إذا قمت بسحب (لا ترفعك) إلى اتجاه واحد (يسار/يمين) ثم إلى العكس ، فسترى الحادث. أيضًا في حل Joel ، إذا قمت بسحب Pager View عن طريق تحريك إصبعك قطريًا ، بمجرد أن تترك إصبعك منطقة عرض محتوى Pager ، فسوف يعود Pager إلى موقعه السابق. كل هذه المشكلات تتعلق بتصميم Android الداخلي أو عدم وجودها أكثر من تطبيق Joel ، وهو ما يعتبر بحد ذاته رمزًا ذكيًا وموجزًا.

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top