web-dev-qa-db-fra.com

Android: défilement ViewPager basé sur la vélocité

La façon dont le ViewPager défile actuellement est d'un élément par geste. Il traite le geste de lancer de la même manière, qu'il s'agisse d'un jet rapide en plein écran ou d'un glissement lent; à la fin de la page avance d'une étape seulement.

Y a-t-il peut-être des projets ou des exemples qui ajouteraient des lancements basés sur la vitesse qui font défiler plusieurs éléments en fonction de la vitesse du lancer existant (s'il est toujours en cours) et qui défile plus loin si le geste de lancer est large et rapide?

Et s'il n'y a personne par où commencer avec quelque chose comme ça?

P.S. La prime est offerte. S'il vous plaît pas de réponses avec des références à Gallery ou HorizontalScrollView

43
Bostone

La technique consiste ici à étendre ViewPager et à imiter l'essentiel de ce que le pager fera en interne, couplé à une logique de défilement à partir du widget Gallery. L'idée générale est de surveiller le lancer (et la vélocité et les parchemins qui l'accompagnent), puis de les alimenter en tant que faux événements de glissement vers le ViewPager sous-jacent. Si vous faites cela seul, cela ne fonctionnera pas (vous n'aurez toujours qu'un seul défilement de page). Cela se produit car le faux glisser implémente des majuscules sur les limites que le défilement sera efficace. Vous pouvez imiter les calculs dans le ViewPager étendu et détecter quand cela se produira, puis retourner la page et continuer comme d'habitude. L'avantage d'utiliser un faux glisser signifie que vous n'avez pas à vous soucier de l'accrochage aux pages ou de la gestion des bords du ViewPager.

J'ai testé le code suivant sur l'exemple de démos d'animation, téléchargeable depuis http://developer.Android.com/training/animation/screen-slide.html en remplaçant le ViewPager dans ScreenSlideActivity avec ce VelocityViewPager (à la fois dans la mise en page activity_screen_slide et le champ de l'activité).

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

}

Il y a quelques problèmes mineurs avec cela, qui peuvent être résolus facilement mais je vous laisse le soin, à savoir des choses comme si vous faites défiler (en faisant glisser, pas en jetant) vous pouvez vous retrouver à mi-chemin entre les pages (vous aurez envie de vous accrocher l'événement ACTION_UP). De plus, les événements tactiles sont complètement remplacés pour ce faire, vous devrez donc alimenter les événements pertinents vers le ViewPager sous-jacent, le cas échéant.

40
Dororo

Une autre option consiste à copier tout le code source d'une implémentation ViewPager à partir de la bibliothèque de support et à personnaliser une méthode determineTargetPage(...). Il est chargé de déterminer jusqu'à quelle page faire défiler lors d'un geste de lancer. Cette approche n'est pas super pratique, mais fonctionne plutôt bien. Voir le code d'implémentation ci-dessous:

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
dominus

J'ai trouvé une meilleure réalisation pour moi que dans la réponse vérifiée, ce ViewPager se comporte mieux avec les touches lorsque je veux arrêter le défilement https://github.com/Benjamin-Dobell/VelocityViewPager

2
Max Makeichik

ViewPager est une classe de la bibliothèque de support. Téléchargez le code source de la bibliothèque de support et modifiez environ 10 lignes de code dans la méthode onTouchEvent pour ajouter la fonctionnalité souhaitée.

J'utilise la bibliothèque de support modifiée dans mes projets depuis environ un an, car parfois j'ai besoin de modifier plusieurs lignes de code pour faire un petit changement ou ajouter une nouvelle méthode et je ne veux pas copier le code source des composants. J'utilise une version modifiée des fragments et du viewpager.

Mais il y a un problème que vous obtiendrez: une fois dans environ 6 mois, vous devez fusionner la bibliothèque de support personnalisée avec la nouvelle version officielle si vous avez besoin de nouvelles fonctionnalités. Et soyez prudent avec les changements, vous ne voulez pas casser la compatibilité des classes de bibliothèque de support.

1
Leonidos