Android: przewijanie ViewPager oparte na prędkości

Sposób przewijania Viewpagera jest teraz o jeden element na gest. Traktuje gest rzucania w ten sam sposób, bez względu na to, czy jest to szybki rzut pełnoekranowy, czy powolne przeciąganie; na stronie końcowej postępuje tylko o jeden krok.

Czy są jakieś projekty lub przykłady, które dodałyby flingowanie oparte na prędkości, które przewija wiele przedmiotów na podstawie prędkości istniejącego flingu (jeśli nadal jest w toku) i przewija dalej, jeśli gest flingowania jest szeroki i szybki?

I jeśli nie ma od czego zacząć?

P. S. nagroda jest oferowana. Proszę o Brak odpowiedzi z odniesieniami do galerii lub HorizontalScrollView

Author: Bostone, 2012-10-08

5 answers

Technika polega na rozszerzeniu ViewPager i naśladowaniu większości tego, co pager będzie robił wewnętrznie, w połączeniu z logiką przewijania z widżetu Gallery. Ogólna idea polega na monitorowaniu przelotu (oraz prędkości i towarzyszących im zwojów), a następnie podawaniu ich jako fałszywych zdarzeń przeciągania do bazowego ViewPager. Jeśli zrobisz to sam, nie będzie to jednak działać (nadal otrzymasz tylko jedną stronę przewijania). Dzieje się tak, ponieważ fałszywy przeciąga implementuje czapki na granicach, że przewijanie będzie skuteczne. Możesz naśladuj obliczenia w rozszerzonym ViewPager i wykryj, kiedy to nastąpi, a następnie po prostu odwróć stronę i kontynuuj jak zwykle. Korzyści płynące z używania fałszywego przeciągania oznaczają, że nie musisz zajmować się przyciąganiem do stron ani obchodzeniem się z krawędziami ViewPager.

Testowałem poniższy kod na przykładzie animacji, do pobrania z http://developer.android.com/training/animation/screen-slide.html poprzez zastąpienie Viewpagera w ScreenSlideActivity tym VelocityViewPager (zarówno w układzie activity_screen_slide, jak i pole działania).

/*
 * Copyright 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 * 
 * Author: Dororo @ StackOverflow
 * An extended ViewPager which implements multiple page flinging.
 * 
 */

package com.example.android.animationsdemo;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.GestureDetector;
import android.widget.Scroller;

public class VelocityViewPager extends ViewPager implements GestureDetector.OnGestureListener {

private GestureDetector mGestureDetector;
private FlingRunnable mFlingRunnable = new FlingRunnable();
private boolean mScrolling = false;

public VelocityViewPager(Context context) {
    super(context);
}

public VelocityViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    mGestureDetector = new GestureDetector(context, this);
}

// We have to intercept this touch event else fakeDrag functions won't work as it will
// be in a real drag when we want to initialise the fake drag.
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    // give all the events to the gesture detector. I'm returning true here so the viewpager doesn't
    // get any events at all, I'm sure you could adjust this to make that not true.
    mGestureDetector.onTouchEvent(event);
    return true;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
    mFlingRunnable.startUsingVelocity((int)velX);
    return false;
}

private void trackMotion(float distX) {

    // The following mimics the underlying calculations in ViewPager
    float scrollX = getScrollX() - distX;
    final int width = getWidth();
    final int widthWithMargin = width + this.getPageMargin();
    final float leftBound = Math.max(0, (this.getCurrentItem() - 1) * widthWithMargin);
    final float rightBound = Math.min(this.getCurrentItem() + 1, this.getAdapter().getCount() - 1) * widthWithMargin;

    if (scrollX < leftBound) {
        scrollX = leftBound;
        // Now we know that we've hit the bound, flip the page
        if (this.getCurrentItem() > 0) {
            this.setCurrentItem(this.getCurrentItem() - 1, false);
        }
    } 
    else if (scrollX > rightBound) {
        scrollX = rightBound;
        // Now we know that we've hit the bound, flip the page
        if (this.getCurrentItem() < (this.getAdapter().getCount() - 1) ) {
            this.setCurrentItem(this.getCurrentItem() + 1, false);
        }
    }

    // Do the fake dragging
    if (mScrolling) {
        this.fakeDragBy(distX);
    }
    else {
        this.beginFakeDrag();
        this.fakeDragBy(distX);
        mScrolling = true;
    }

}

private void endFlingMotion() {
    mScrolling = false;
    this.endFakeDrag();
}

// The fling runnable which moves the view pager and tracks decay
private class FlingRunnable implements Runnable {
    private Scroller mScroller; // use this to store the points which will be used to create the scroll
    private int mLastFlingX;

    private FlingRunnable() {
        mScroller = new Scroller(getContext());
    }

    public void startUsingVelocity(int initialVel) {
        if (initialVel == 0) {
            // there is no velocity to fling!
            return;
        }

        removeCallbacks(this); // stop pending flings

        int initialX = initialVel < 0 ? Integer.MAX_VALUE : 0;
        mLastFlingX = initialX;
        // setup the scroller to calulate the new x positions based on the initial velocity. Impose no cap on the min/max x values.
        mScroller.fling(initialX, 0, initialVel, 0, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);

        post(this);
    }

    private void endFling() {
        mScroller.forceFinished(true);
        endFlingMotion();
    }

    @Override
    public void run() {

        final Scroller scroller = mScroller;
        boolean animationNotFinished = scroller.computeScrollOffset();
        final int x = scroller.getCurrX();
        int delta = x - mLastFlingX;

        trackMotion(delta); 

        if (animationNotFinished) {
            mLastFlingX = x;
            post(this);
        }
        else {
            endFling();
        }

    }
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) {
    trackMotion(-distX);
    return false;
}

    // Unused Gesture Detector functions below

@Override
public boolean onDown(MotionEvent event) {
    return false;
}

@Override
public void onLongPress(MotionEvent event) {
    // we don't want to do anything on a long press, though you should probably feed this to the page being long-pressed.
}

@Override
public void onShowPress(MotionEvent event) {
    // we don't want to show any visual feedback
}

@Override
public boolean onSingleTapUp(MotionEvent event) {
    // we don't want to snap to the next page on a tap so ignore this
    return false;
}

}

Jest kilka drobnych problemów z tym, które można łatwo rozwiązać, ale pozostawiam to tobie, a mianowicie rzeczy takie jak przewijanie (przeciąganie, nie rzucanie) może skończyć się w połowie między stronami (będziesz chciał przyciągnąć na zdarzenie ACTION_UP). Ponadto, zdarzenia dotykowe są całkowicie nadpisywane, aby to zrobić, więc trzeba będzie karmić odpowiednie zdarzenia do bazowego ViewPager, gdzie to stosowne.

 40
Author: Dororo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-19 23:54:33

Inną opcją jest skopiowanie całego kodu źródłowego ViewPager z biblioteki wsparcia i dostosowanie metody determineTargetPage(...). Jest odpowiedzialny za określenie, do której strony przewijać na gest fling. Takie podejście nie jest zbyt wygodne, ale działa całkiem dobrze. Zobacz kod implementacji poniżej:

private int determineTargetPage(int curPage, float pageOffset, int velocity, int dx) {
    int target;
    if (Math.abs(dx) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
        target = calculateFinalPage(curPage, velocity);
    } else {
        final float truncator = curPage >= mCurItem ? 0.4f : 0.6f;
        target = (int) (curPage + pageOffset + truncator);
    }
    if (mItems.size() > 0) {
        final ItemInfo first = mItems.get(0);
        final ItemInfo last = mItems.get(mItems.size() - 1);

        // Only let the user target pages we have items for
        target = Math.max(first.position, Math.min(target, last.position));
    }
    return target;
}

private int calculateFinalPage(int curPage, int velocity) {
    float distance = Math.abs(velocity) * MAX_SETTLE_DURATION / 1000f;
    float normalDistance = (float) Math.sqrt(distance / 2) * 25;
    int step = (int) - Math.signum(velocity);
    int width = getClientWidth();
    int page = curPage;
    for (int i = curPage; i >= 0 && i < mAdapter.getCount(); i += step) {
        float pageWidth = mAdapter.getPageWidth(i);
        float remainingDistance = normalDistance - pageWidth * width;
        if (remainingDistance >= 0) {
            normalDistance = remainingDistance;
        } else {
            page = i;
            break;
        }
    }
    return page;
}
 4
Author: dominus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-12 15:10:03

Znalazłem dla mnie lepszą realizację niż w sprawdzonej odpowiedzi, Ten ViewPager zachowuje się lepiej z dotknięciami, gdy chcę przestać przewijać https://github.com/Benjamin-Dobell/VelocityViewPager

 2
Author: Max Makeichik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-19 10:02:52

ViewPager jest klasą z biblioteki wsparcia. Pobierz kod źródłowy biblioteki wsparcia i zmień około 10 linii kodu w metodzie onTouchEvent, aby dodać żądaną funkcję.

Używam zmodyfikowanej biblioteki wsparcia w moich projektach przez około rok, ponieważ czasami muszę zmodyfikować kilka linii kodu, aby dokonać małej zmiany lub dodać nową metodę i nie chcę kopiować kodu źródłowego komponentów. Używam zmodyfikowanej wersji fragmentów i viewpagera.

Ale jest jeden problem, który dostaniesz: raz w około 6 mounth musisz połączyć niestandardową bibliotekę wsparcia z nową oficjalną wersją, jeśli potrzebujesz nowych funkcji. I uważaj na zmiany, nie chcesz łamać kompatybilności klas bibliotek wsparcia.

 1
Author: Leonidos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-26 10:30:45

Możesz nadpisać klasę ScrollView lub HorizontalScrollView i dodać to zachowanie. Jest wiele błędów w galerii, i jak pamiętam jest przestarzały od poziomu api 14.

 -1
Author: hsafarya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-16 08:39:52