android - HorizontalScrollView داخل ScrollView Touch Handling




ontouchlistener android-scrollview (6)

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

UPDATE 2013-07-16 : أضفت onTouchEvent لـ 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 () . ويوضح ذلك أيضًا العرض التقديمي "طرق عرض مخصصة" لنظام التشغيل Android في Google I / O لعام 2013.

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

لدي ScrollView الذي يحيط بي تخطيط كامل بحيث يمكن التمرير الشاشة بالكامل. العنصر الأول لدي في ScrollView هذا هو كتلة HorizontalScrollView يحتوي على الميزات التي يمكن تمريرها أفقيًا. لقد أضفت ontouchlistener إلى horizontalscrollview للتعامل مع أحداث اللمس وإجبار العرض على "انجذاب" إلى أقرب صورة في الحدث ACTION_UP.

لذا فإن التأثير الذي سأقوم به يشبه شاشة Android الرئيسية حيث يمكنك التمرير من واحد إلى الآخر ويستقر على شاشة واحدة عند رفع إصبعك.

يعمل كل هذا بشكل رائع باستثناء مشكلة واحدة: أحتاج إلى التمرير سريعًا من اليسار إلى اليمين تمامًا تقريبًا تمامًا للحصول على ACTION_UP للتسجيل أبدًا. في حالة التمرير السريع عموديًا على أقل تقدير (وهو ما أعتقد أن العديد من الأشخاص يميلون إلى فعله على هواتفهم عند التمرير من جانب إلى آخر) ، فسوف أتلقى ACTION_CANCEL بدلاً من ACTION_UP. نظريتي هي أن هذا يرجع إلى أن عرض 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;
    }
}

}


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

}

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

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


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

لقد قمت بتبسيط التعليمات البرمجية (دون الحاجة إلى 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);
    }
}

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

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

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





horizontalscrollview