2012-10-08 15 views
42

Il modo in cui scorre il ViewPager in questo momento è di un elemento per gesto. Tratta i gesti allo stesso modo, a prescindere che si tratti di full screen fast fling o slow dragging; alla fine la pagina avanza di un solo passo.Android: scrolling di ViewPager basato su velocità

C'è qualche progetto o esempio che aggiungerebbe il lancio basato sulla velocità che scorre più elementi in base alla velocità del lancio esistente (se ancora in corso) e scorre ulteriormente se il gesto di lancio è ampio e veloce?

E se non c'è nessuno da cui iniziare con qualcosa di simile?

P.S. La taglia è offerta. Nessuna risposta con riferimenti a Gallery o HorizontalScrollView

+0

Ho pensato che si comporta Galleria in questo modo, l'hai provato per vedere se è adatto alle tue esigenze? http://developer.android.com/reference/android/widget/Gallery.html –

+0

Potrebbe essere, ho bisogno di verificarlo – Bostone

+0

E no + Ian Warwick - utilizzando la Gallery è fuori questione – Bostone

risposta

39

La tecnica è quella di estende ViewPager e imitare la maggior parte di ciò che il pager farà internamente, insieme con lo scorrimento logica dal widget Gallery. L'idea generale è di monitorare il lancio (e la velocità e le pergamene di accompagnamento) e quindi inviarli come eventi di finto trascinamento al sottostante ViewPager. Se lo fai da solo, non funzionerà comunque (avrai comunque solo una pagina di scorrimento). Ciò accade perché il finto trascinamento implementa i limiti sui limiti in modo che lo scorrimento sia efficace. È possibile simulare i calcoli nel numero esteso ViewPager e rilevare quando ciò accadrà, quindi capovolgere la pagina e continuare come al solito. Il vantaggio dell'utilizzo di trascinamento falso indica che non è necessario eseguire lo snap alle pagine o gestire i bordi dello ViewPager.

Ho testato il seguente codice sull'esempio demo animazione, scaricabile da http://developer.android.com/training/animation/screen-slide.html sostituendo il ViewPager in ScreenSlideActivity con questo VelocityViewPager (sia nel layout activity_screen_slide e il campo all'interno della Attività).

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

} 

ci sono alcuni problemi minori con questo, che può essere risolto facilmente, ma lascio a voi, vale a dire le cose come se si scorre (trascinamento, non buttando) si può finire a metà strada tra le pagine (vorrai fare lo snap all'evento ACTION_UP). Inoltre, gli eventi di tocco vengono completamente ignorati al fine di eseguire questa operazione, pertanto è necessario inviare gli eventi pertinenti allo ViewPager sottostante, ove appropriato.

+0

Grazie @Dororo - Farò un tentativo per vedere cosa mi manca qui – Bostone

+0

Grazie @Dororo. È meraviglioso. – Noundla

+1

Ho provato questo, e funziona bene, ma se faccio un pager.setCurrentItem (m_CurrPage, false) dal codice per inizializzarlo in una pagina diversa da 0 all'avvio, fling non funziona, e ogni volta che torna al prima pagina, come posso inizializzare la pagina iniziale per farlo funzionare? – emax79

-1

È possibile eseguire l'override della classe ScrollView o HorizontalScrollView e aggiungere tale comportamento. Ci sono molti bug in Galleria, e mi ricordo che sia deprecato dal livello di API 14.

+1

Si prega di leggere la domanda si tratta di ViewPager, non di Gallery – Bostone

+0

Ho letto la domanda, e c'è un commento sulla Galleria. Il tuo scritto "Potrebbe essere, ho bisogno di verificarlo" sulla Gallery, non posso aggiungere commenti, ed ecco perché aggiunto come risposta. – hsafarya

+0

Mi sono riferito al codice Gallery come un possibile esempio su come migliorare lo scorrimento di ViewPager. La domanda è strettamente su ViewPager, sto guardando le alternative – Bostone

1

ViewPager è una classe della libreria di supporto. Scaricare il codice sorgente della libreria di supporto e modificare circa 10 righe di codice nel metodo onTouchEvent per aggiungere la funzionalità desiderata.

Io uso la libreria di supporto modificata nei miei progetti per circa un anno, perché a volte ho bisogno di modificare diverse righe di codice per fare un piccolo cambiamento o aggiungere un nuovo metodo e non voglio copiare il codice sorgente dei componenti. Uso la versione modificata di frammenti e viewpager.

Ma c'è un problema che si otterrà: una volta in circa 6 mesi devi unire libreria di supporto personalizzata con la nuova versione ufficiale se hai bisogno di nuove funzionalità. E fai attenzione alle modifiche, non vuoi rompere la compatibilità delle classi della libreria di supporto.

+0

Questo davvero non aiuta – protectedmember

4

Un'altra opzione è copiare un intero codice sorgente di implementazione ViewPager dalla libreria di supporto e personalizzare un metodo determineTargetPage(...). È responsabile di determinare a quale pagina scorrere i movimenti di lancio. Questo approccio non è super conveniente, ma funziona piuttosto bene. Vedere codice di implementazione di seguito:

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

Ha funzionato per me come un fascino! –

+3

Puoi darci il succo della tua versione del viewpager? – sn0ep

+0

È come il viewpager ... stessa cosa! –